Diferencia entre revisiones de «PHP UD3 OP CONSULTA»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 68 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 />
 +
* 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]
 +
 +
 +
* 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 />
 
<br />
−
* Para conectarnos primeiro temos que ter a consulta SQL.
 
−
* Indicar que o [https://es.wikipedia.org/wiki/SQL SQL (Structure Query Language)] é a linguaxe utilizada para obter datos dunha base de datos relacional.
 
  
 +
=== 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 {
  
−
<br />
+
    echo htmlspecialchars(
−
:* '''Paso 2: Chamar ao procedemento query enviando como parámetro a consulta a realizar e a conexión aberta previamente.'''
+
        $fila['col1'],
 +
        ENT_QUOTES | ENT_SUBSTITUTE,
 +
        'UTF-8'
 +
    );
  
−
::* Versión procedimental:
+
    echo ' - ';
−
:::<syntaxhighlight lang="java" line 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:
+
    echo htmlspecialchars(
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
        $fila['col2'],
−
$result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
+
        ENT_QUOTES | ENT_SUBSTITUTE,
−
</syntaxhighlight>
+
        'UTF-8'
 +
    );
  
−
::* $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.
+
    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) {
  
−
<br />
+
    echo htmlspecialchars(
−
:* '''Paso 3: Comprobar se temos datos ou se houbo algún tipo de erro na chamada.'''
+
        $fila['col1'],
 +
        ENT_QUOTES | ENT_SUBSTITUTE,
 +
        'UTF-8'
 +
    );
  
 +
    echo ' - ';
  
−
::* Se estamos a <u>empregar unha versión PHP superior ou igual á 8.1</u>, os erros deben ser capturados con try-catch:
+
    echo htmlspecialchars(
−
:::* Versión orientada a obxectos:
+
        $fila['col2'],
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="38-43" >
+
        ENT_QUOTES | ENT_SUBSTITUTE,
−
<?php
+
        'UTF-8'
−
/* 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 {
+
    echo '<br>';
 +
}
 +
} else {
 +
echo 'Non hai datos que amosar';
 +
}
 +
</syntaxhighlight>
 +
 
 +
 
 +
* [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.
 +
: 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:
 +
 
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT COUNT(*)
 +
FROM libros'
 +
);
 +
$numLibros = $stmt->fetchColumn();
 +
</syntaxhighlight>
 +
 
 +
 
 +
 
 +
 
 +
 
 +
<br />
 +
::* 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:
 +
:::* Versión orientada a obxectos:
 +
::::<syntaxhighlight lang="php" enclose="div" highlight="38-43" >
 +
<?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 = new mysqli('SERVIDOR','USUARIO','PASSWORD','BD');   
 
     $conex->set_charset('utf8mb4');
 
     $conex->set_charset('utf8mb4');
Línea 115: Línea 246:
  
 
</syntaxhighlight>  
 
</syntaxhighlight>  
−
 
  
  
  
 
<br />
 
<br />
−
:::* Versión PDO:
+
::* 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.
−
::::<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
  
 +
::: 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.
  
  
−
 
−
 
−
<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)].  
 
::: 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'.
 
−
::: Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false.
 
−
 
−
 
 
::* 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>
  
  
−
::* Versión PDO:
+
* É especialmente recomendable indicar explicitamente o tipo cando este sexa relevante para a consulta.
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="3-5" >
+
: Por exemplo, nos parámetros empregados en LIMIT ou OFFSET:
−
        $stmt = $conex->query('SELECT col1, col2 FROM TABOA_1');
+
::<syntaxhighlight lang="php">
−
        // Comprobar se hai filas
+
$stmt = $conex->prepare(
−
        if ($result && $result->rowCount() > 0) {
+
'SELECT id, titulo
−
          // Procesamos os datos
+
FROM libros
−
          while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) { // tamén pode ser PDO::FETCH_OBJ
+
ORDER BY titulo
−
              echo htmlentities($fila['col1']) . ' - ' . htmlentities($fila['col2']) . "<br>";
+
LIMIT :inicio,:cantidade'
−
          }
+
);
−
        } else {
+
$stmt->bindValue(
−
          echo "Non hai datos que amosar!!!!";
+
':inicio',
−
        }
+
$inicio,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->bindValue(
 +
':cantidade',
 +
$cantidade,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->execute();
 +
</syntaxhighlight>
  
−
</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 temos poucas filas podemos obter todas as filas de vez empregando [https://www.php.net/manual/es/pdostatement.fetchall.php fetchall()].
 
  
 +
* 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>
  
−
<br />
 
−
:* '''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:
+
* Non debemos escribir:
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="6" >
+
::<syntaxhighlight lang="sql">
−
if ($result && (mysqli_num_rows($result)>0)){
+
WHERE apelidos LIKE '%:apelidos%'
−
  // Veñen datos
+
</syntaxhighlight>
−
  while($row = mysqli_fetch_assoc($result)) {
+
: 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).
−
    printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
 
−
  }
 
−
  mysqli_free_result($result);  // Liberamos da memoria os recursos
 
−
}
 
−
else {
 
−
  echo 'Non hai datos ou a consulta está mal feita';
 
−
}
 
−
</syntaxhighlight>  
 
  
 +
=== 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
 +
);
  
−
::* Versión orientada a obxectos con fetch_assoc:
+
$stmt->bindValue(
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="6" >
+
     ':libro_id',
−
if ($result && ($result->num_rows>0)){
+
    $libroId,
−
  // Veñen datos
+
    PDO::PARAM_INT
−
  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>
 
  
 +
$stmt->execute();
  
−
::* Versión PDO:
+
// Procesamos os resultados
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="7" >
 
−
// 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!!!!";
 
 
}
 
}
 +
</syntaxhighlight>
 +
=== Caso práctico: consultar exemplares dun libro ===
 +
'''Arquivo bd.php'''
 +
::<syntaxhighlight lang="php">
  
 +
define('CARTAFOL_IMAXES_WEB', 'http://localhost/imaxes/');
  
−
</syntaxhighlight>  
+
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>
  
  
  
 
<br />
 
<br />
−
:* '''Paso 6: Pechamos a conexión.'''.
+
* '''Código PHP'''
 +
::<syntaxhighlight lang="php">
 +
<html>
  
−
:: [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.]
+
<head>Páxina de exemplo
 +
</head>
  
−
=== Caso Práctico 1 ===
+
<body>
−
: Imos crear unha consulta que nos devolva todos os libros da táboa LIBROS dentro dunha táboa:
+
 
−
: 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.
+
    <?php
 +
    ini_set('display_errors', 1);
 +
    ini_set('display_startup_errors', 1);
 +
    error_reporting(E_ALL);
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
    require('/var/www/confBD/bd.php');
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
+
    try {
−
</syntaxhighlight>
 
  
 +
        $dsn = sprintf(
 +
            'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
            SERVIDOR,
 +
            BD
 +
        );
  
−
* '''Normas nos nomes das constantes''':
+
        $opciones = [
−
:* Comenzarán cun verbo que vai poder ser: CONSULTAR, INSERIR, MODIFICAR e ELIMINAR
+
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
:* Seguirá cun guión baixo.
+
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
:* 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.
+
            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
 +
            );
  
−
* Aplicado ao exemplo que estamos a seguir...
+
            $stmt->bindValue(
−
:<u>Nota</u> Lembrar activar as directivas para amosar erros e quitalas cando rematedes a páxina.
+
                ':libro_id',
 +
                $libroId,
 +
                PDO::PARAM_INT
 +
            );
  
−
==== Solución Caso Práctico 1 Orientado a Obxectos====
+
            $stmt->execute();
  
−
'''Arquivo: consultar_libros.php'''
+
            $fila = $stmt->fetch();
−
::<syntaxhighlight lang="java" enclose="div" highlight="27-40" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
            if ($fila !== false) {
  
−
try {
+
                echo "<table border='1'>";
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD);   // Non indicamos o nome da bd
+
                echo '<tr>';
−
    $conex->set_charset('utf8mb4');
+
                echo '<th>Num. exemplar</th>';
−
    $conex->select_db(BD); // O parámetro indica o nome da base de datos
+
                echo '<th>Imaxe</th>';
 +
                echo '<th>Prezo</th>';
 +
                echo '<th>Estado</th>';
 +
                echo '</tr>';
  
−
?>
+
                do {
  
−
<!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>%d</td>',
−
       
+
                        (int) $fila['id_exemplar']
−
        $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 {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
       
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
                    printf(
−
}    // Pecha o try do principio da páxina
+
                        "<td><img alt='%s' src='%s' width='42' height='42'></td>",
−
catch (mysqli_sql_exception $e) {
+
                        htmlspecialchars(
−
    die("Erro mysql: " . $e->getMessage());
+
                            $fila['imaxe'] ?? '',
−
}
+
                            ENT_QUOTES | ENT_SUBSTITUTE,
−
catch (Expcetion $e) {
+
                            'UTF-8'
−
    die("Erro xeral: " . $e->getMessage());
+
                        ),
−
}
+
                        htmlspecialchars(
−
finally{
+
                            CARTAFOL_IMAXES_WEB . ($fila['imaxe'] ?? ''),
−
  if(!empty($conex)){
+
                            ENT_QUOTES | ENT_SUBSTITUTE,
−
      $conex->close();
+
                            'UTF-8'
−
  }
+
                        )
−
}
+
                    );
−
?>
 
  
−
</syntaxhighlight>
+
                    printf(
 +
                        '<td>%.2f</td>',
 +
                        (float) ($fila['prezo'] ?? 0)
 +
                    );
  
 +
                    printf(
 +
                        '<td>%d</td>',
 +
                        (int) $fila['estado_id']
 +
                    );
  
−
* Resultado:
+
                    echo '</tr>';
−
[[Imagen:Php_BD_8.jpg|300px|center]]
+
                } while ($fila = $stmt->fetch());
  
 +
                echo '</table>';
 +
            } else {
  
−
* 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.
+
                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>
 +
</syntaxhighlight>
  
−
* O método fetch_assoc() move un punteiro interno á seguinte fila de resultados.
+
== Procedementos almacenados empregando PDO ==
−
: 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]·
+
Os procedementos almacenados execútanse mediante a sentenza SQL CALL.
−
 
+
: Por exemplo:
 +
::<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'
 +
);
 +
}
 +
</syntaxhighlight>
 +
=== 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
 +
}
 +
} while ($stmt->nextRowset());
 +
</syntaxhighlight>
  
  
 +
+ 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>
 +
: Isto é especialmente importante despois de chamadas a procedementos almacenados en MySQL.
  
−
<br />
 
−
==== Solución Caso Práctico 1 empregando PDO====
 
  
−
'''Arquivo: consultar_libros.php'''
+
: Por exemplo:
−
::<syntaxhighlight lang="java" enclose="div" highlight="27-40" >
+
::<syntaxhighlight lang="php">
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
// 1. Executamos o procedemento almacenado que devolve varios SELECTs
−
ini_set('display_errors', 1);
+
$stmt = $conex->prepare('CALL PROCEDEMENTO_CON_VARIOS_SELECT()');
−
ini_set('display_startup_errors', 1);
+
$stmt->execute();
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
// 2. Procesamos só o primeiro conxunto de resultados (por exemplo, os primeiros 3 datos)
 +
if ($fila = $stmt->fetch()) {
 +
    echo "Primeiro rexistro: " . $fila['nome'] . "<br>";
 +
}
  
−
try {
+
// 3. Pechamos o cursor para liberar a conexión
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",SERVIDOR,BD);
+
// Isto descarta calquera outra fila ou segundo resultset pendente
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
+
$stmt->closeCursor();
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
 +
// 4. Agora xa podemos executar outra consulta na mesma conexión sen erros
 +
$outraConsulta = $conex->query('SELECT * FROM outra_taboa');
 +
$datos = $outraConsulta->fetchAll();
 
?>
 
?>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
</syntaxhighlight>
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
=== Procedementos con parámetros de entrada ===
−
    <head>
+
Os parámetros de entrada enviaranse empregando marcadores:
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
::<syntaxhighlight lang="php">
−
        <title>Operacións BD</title>
+
$stmt = $conex->prepare(
−
    </head>
+
'CALL PROCEDEMENTO_CON_PARAMETROS(
 +
:dato1,:dato2
  
−
    <body>
+
)'
−
    <?php       
+
);
−
       
+
$stmt->bindValue(
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
':dato1',
−
        // Comprobar se hai filas
+
$valorNum,
−
        if ($result && $result->rowCount() > 0) {
+
PDO::PARAM_INT
−
            echo "<table border='1'>";
+
);
−
            while ($fila = $result->fetch(PDO::FETCH_ASSOC)) {
+
$stmt->bindValue(
−
                echo '<tr>';
+
':dato2',
−
                printf("<td>%s</td>",htmlentities($fila["titulo"]));
+
$valorTexto,
−
                echo '</tr>';
+
PDO::PARAM_STR
−
            }
+
);
−
            echo '</table>';
+
$stmt->execute();
−
            $result = null; 
+
</syntaxhighlight>
−
        } else {
+
: Desta forma non necesitamos construír a chamada mediante sprintf() nin concatenar os valores dentro da sentenza SQL.
−
          echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
     
 
−
    ?>      
 
−
    </body>  
 
−
</html>
 
  
−
<?php
+
=== Procedementos con parámetros de saída en MySQL ===
−
}    // Pecha o try do principio da páxina
+
PDO define mecanismos xenéricos para parámetros de saída, pero o controlador '''PDO_MYSQL non xestiona correctamente PDO::PARAM_INPUT_OUTPUT'''.
−
catch (PDOException $e) {
+
Por este motivo, con MySQL podemos empregar unha variable de usuario de MySQL para recuperar un parámetro OUT.
−
  die("Erro PDO: " . $e->getMessage());
+
Supoñamos este procedemento:
−
} finally {
+
::<syntaxhighlight lang="sql">
−
  $conex = null; // Pechar a conexión
+
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>
 
</syntaxhighlight>
 +
: Esta consulta devolvería ata 10 rexistros empezando desde o rexistro situado na posición 20.
  
  
−
* Resultado:
 
−
[[Imagen:Php_BD_8.jpg|300px|center]]
 
  
 +
<br />
 +
'''Número total de rexistros'''
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT COUNT(*)
 +
FROM libros'
 +
);
 +
$numLibros = (int) $stmt->fetchColumn();
 +
</syntaxhighlight>
  
−
* 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.
 
  
  
 
<br />
 
<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.
  
−
=== 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>
+
<br />
−
: Sendo XXX a columna id_autor.
+
'''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>
  
−
: <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 />
 +
'''Calcular o desprazamento'''
 +
::<syntaxhighlight lang="php">
 +
$inicio = (
 +
$paxinaActual - 1
 +
) * CONST_NUM_LIBROS_PAXINA;
 +
</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 />
−
==== Solución Caso Práctico 2 orientado a obxectos====
+
'''Consulta paxinada'''
 +
Neste caso indicaremos explicitamente que os parámetros son enteiros:
 +
::<syntaxhighlight lang="php">
 +
$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();
 +
</syntaxhighlight>
  
−
: No '''arquivo bd.php''' creado no punto anterior.
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
 
−
</syntaxhighlight>
 
  
 +
* Despois podemos amosar os libros:
 +
::<syntaxhighlight lang="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'
 +
        )
 +
    );
  
−
<br />
+
    echo '</tr>';
−
'''Páxina consultar_autores.php'''
+
}
−
:<syntaxhighlight lang="java" enclose="div" highlight="26-40" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
echo '</table>';
 +
} else {
 +
echo 'Non hai datos que amosar';
 +
}
 +
</syntaxhighlight>
  
−
try {
 
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);    // Indico nome da 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>
+
<br />  
−
    <?php      
+
'''Enlaces da paxinación'''
−
       
+
::<syntaxhighlight lang="php">
−
        $result=$conex->query(CONSULTAR_AUTORES);
+
echo '<div>';
−
        if ($result && $result->num_rows>0){
+
for (
−
            // Veñen datos
+
$cont = 1;
−
            echo "<ul>";
+
$cont <= $totalPaxinas;
−
            while($row = mysqli_fetch_assoc($result)) {
+
$cont++
−
                echo "<li>";
+
) {
−
                printf("<a href='detalle_autor.php?id=%s'>%s</a>",$row["id"],urlencode($row["apelidos"]) .', '. urlencode($row['nome']));
+
if ($cont === $paxinaActual) {
−
                echo "</li>";
+
 
−
            }
+
    printf(
−
            echo "</ul>";
+
        '<strong>%d</strong> ',
−
            $result->free();  // Liberamos da memoria os recursos
+
        $cont
−
        }
+
    );
−
        else {
+
 
−
            echo "Non hai datos que amosar!!!!";
+
} else {
−
        }
 
−
       
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
    printf(
 +
        "<a href='?paxina=%d'>%d</a> ",
 +
        $cont,
 +
        $cont
 +
    );
 
}
 
}
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
 
}
 
}
−
catch (Expcetion $e) {
+
echo '</div>';
−
    die("Erro xeral: " . $e->getMessage());
+
</syntaxhighlight>
−
}
+
 
−
finally{
+
 
−
  if(!empty($conex)){
+
 
−
      $conex->close();
+
<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
 +
    ]);
  
−
<br />
+
    // Operacións coa base de datos...
−
==== Solución Caso Práctico 2 empregando PDO====
+
 
 +
} 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.');
 +
}
  
−
: No '''arquivo bd.php''' creado no punto anterior.
+
// Non fai falta "finally {$conex = null;}":
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
// Se o script continúa, destruirasie cando cumpra o seu ciclo;
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
+
// se entra nos catch, o die() xa finaliza o proceso e libera a memoria.
−
</syntaxhighlight>  
+
</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 />
−
'''Páxina consultar_autores.php'''
+
== Exercicios prácticos ==
−
:<syntaxhighlight lang="java" enclose="div" highlight="27-41" >
+
 
−
<?php
+
* 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`.
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
* '''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.
  
−
try {
+
* Os exemplos con `mysqli` poden consultarse como información adicional, pero os exemplos de referencia dos apuntes serán os realizados mediante PDO.
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",SERVIDOR,BD);
 
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
?>
+
* Nos exemplos completos empregaremos o seguinte esquema:
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
: No arquivo bd.php que se atopa fora da ruta /var/www/html gardaremos os datos da conexión e as consultar a empregar:
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
::<syntaxhighlight lang="php">
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
<?php
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
define('SERVIDOR', 'localhost');
−
    <?php       
+
define('USUARIO', 'user_php');
−
       
+
define('PASSWORD', 'user_php');
−
        $result=$conex->query(CONSULTAR_AUTORES);
+
define('BD', 'php');
−
        // Comprobar se hai filas
 
−
        if ($result && $result->rowCount() > 0) {
 
−
            echo "<ul>";
 
−
            while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
 
−
                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 = null; 
+
define(
−
        } else {
+
     'NOME_DA_CONSULTA',
−
          echo "Non hai datos que amosar!!!!";
+
     'SELECT col1,col2,...
−
        }
+
    FROM taboa
−
     
+
    ORDER BY col1'
−
     ?>       
+
);
−
     </body>   
 
−
</html>
 
−
 
 
−
<?php
 
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
  die("Erro PDO: " . $e->getMessage());
 
−
} finally {
 
−
  $conex = null; // Pechar a conexión
 
−
}
 
−
 
 
−
?>
 
 
</syntaxhighlight>
 
</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';
  
−
<br />
+
try {
  
−
=== Consulta enviando parámetros de tipo numérico ===
+
    $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
 +
        ]
 +
    );
  
−
====Empregando PDO====
+
    // Operacións coa base de datos...
  
−
* No caso de PDO, non necesitamos empregar a función [https://www.php.net/manual/es/mysqli.real-escape-string.php función mysqli_real_escape_string] para evitar ataques que poidan facerse.
 
−
: O proceso de envía de parámetros en PDO faise dunha forma moi parecido a o envío de parámetros empregando procedementos almacenados de Mysql (o veremos despois).
 
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
+
} catch (PDOException $e) {
−
: 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= ''':valor'''
 
  
−
: No exemplo, col1 é de tipo numérico.
+
// Rexistrar o erro nun log.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
// Nunha aplicación real tamén poderían empregarse
−
define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1= :valor1');
+
// ferramentas como Monolog.
−
// define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1= ?'); => Tamén vale con poñer unha interrogación
+
error_log($e->getMessage());
  
−
// Preparar a consulta
+
die('Erro relacionado coa base de datos.');
−
$stmt = $conex->prepare(CONSULTA_CON_PARAM_NUMERICO);
 
  
−
// Executar a consulta substituíndo o parámetro polo valor que queremos enviar
+
} catch (Throwable $e) {
−
$stmt->execute(['valor' => 5]);
 
−
// Se emprego interrogación se cambia por orde. Se houbera 2 interrogacións, ao chamar a execute([5,7]) a primeira interrogación tería de valor 5 e a segunda 7.
 
−
// $stmt->execute([5]);
 
  
−
</syntaxhighlight>
+
die('Produciuse un erro na aplicación.');
  
 +
} </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 />
−
====Orientado a obxectos====
 
−
<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].
 
  
−
: 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.
+
=== 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.
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
+
* Para non ter as consultas SQL no medio do código das diferentes páxinas, definirémolas no arquivo `bd.php`.
−
: 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'''
+
'''Arquivo: bd.php'''
  
−
: No exemplo, col1 é de tipo numérico.
+
::<syntaxhighlight lang="php">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
define(
−
define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1=%d');
+
'CONSULTAR_LIBROS',
 +
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo'
 +
); </syntaxhighlight>
  
−
$query = sprintf(CONSULTA_CON_PARAM_NUMERICO,valor_numérico_a_enviar);
+
* Como norma para os nomes das constantes que conteñan sentenzas SQL:
−
</syntaxhighlight>
 
  
−
: Agora facemos o mesmo código que no exemplo anterior.
+
:* Comezarán cun verbo que poderá ser `CONSULTAR`, `INSERIR`, `MODIFICAR` ou `ELIMINAR`.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
:* A continuación empregarase un guión baixo.
−
......
+
:* Despois indicarase o obxectivo da operación.
−
$result=$conex->query(CONSULTA_CON_PARAM_NUMERICO);
 
−
......
 
−
</syntaxhighlight>
 
  
 +
==== Solución Caso Práctico 1 empregando PDO ====
  
 +
'''Arquivo: consultar_libros.php'''
  
−
<br />
+
::<syntaxhighlight lang="php">
 +
 
 +
<?php
  
−
==== Caso práctico: Enviando parámetros de tipo numérico ====
+
ini_set('display_errors', '1');
 +
error_reporting(E_ALL);
  
−
* Seguindo con exemplo, imos facer que cando o usuario seleccione un libro dentro dunha combobox, aparezan os exemplares do mesmo.
+
require '/var/www/confBD/bd.php';
−
: Para iso teremos que:
 
−
:* Crear a sentencia SQL correspondente enviando un parámetro que vai ser o código do libro seleccionado.
 
−
:* 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).
 
−
:* 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.
 
  
 +
try {
  
−
<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.
+
    $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);
  
−
<br />
+
    $libros = $stmt->fetchAll();
−
=====Orientado a obxectos=====
 
−
* Crearemos unha nova páxina de nome '''consultar_exemplares_por_libro.php'''.
 
  
−
: Modificamos o código para que aparezan os libros dentro da etiqueta <select> de html:
+
} catch (PDOException $e) {
−
: 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="" >
+
    error_log($e->getMessage());
−
<?php
+
    die('Erro relacionado coa base de datos.');
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
} catch (Throwable $e) {
  
−
try {
+
     die('Produciuse un erro na aplicación.');
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da 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">
+
<!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>
+
 
−
    <?php       
+
<body>
−
       
 
−
        $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) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</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.
+
if ($libros) {
−
: 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...
 
  
−
: Resultado:
+
    echo "<table border='1'>";
−
[[Imagen:Php_BD_9.jpg|300px|center]]
 
  
 +
    foreach ($libros as $libro) {
  
 +
        echo '<tr>';
  
 +
        printf(
 +
            '<td>%s</td>',
 +
            htmlspecialchars(
 +
                $libro['titulo'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            )
 +
        );
  
−
* '''Arquivo bd.php'''
+
        echo '</tr>';
−
: Definimos nunha constate a consulta que vai obter os exemplares do libro seleccionado:
+
    }
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
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>
 
−
:: 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.
 
  
 +
    echo '</table>';
  
 +
} else {
  
−
* Para facer que cando se seleccione un elemento do lista se 'active' o formulario, existen múltiples formas, unha delas pode ser:
+
    echo 'Non hai datos que amosar.';
−
:::            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="java" line enclose="div" highlight="42,44,47-56" >
+
?>
−
<?php
+
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
</body>
−
ini_set('display_errors', 1);
+
</html>
−
ini_set('display_startup_errors', 1);
+
</syntaxhighlight>
−
error_reporting(E_ALL);
+
 
 +
* 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:
 +
 
 +
::<syntaxhighlight lang="php">
 +
while ($libro = $stmt->fetch()) {
 +
 
 +
echo htmlspecialchars(
 +
    $libro['titulo'],
 +
    ENT_QUOTES | ENT_SUBSTITUTE,
 +
    'UTF-8'
 +
);
  
−
require ('/var/www/confBD/bd.inc.php');
+
} </syntaxhighlight>
  
−
try {
+
* '''IMPORTANTE:''' Non empregaremos `rowCount()` para comprobar cantas filas devolveu unha sentenza `SELECT`.
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da 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">
+
: Se realmente necesitamos coñecer o número de rexistros, empregaremos `COUNT(*)`.
−
<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>
+
<br />
−
    <?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()) {
 
−
                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>   
+
=== Caso Práctico 2 ===
−
</html>
 
  
−
<?php
+
* Imos recuperar todos os autores ordenados polos seus apelidos e nome.
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?> 
 
−
</syntaxhighlight>
 
  
−
::* 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).
+
* Cada autor aparecerá como un enlace co seguinte formato:
−
::* 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.
 
  
−
::* 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="html"> <a href="detalle_autor.php?id=5">
 +
Apelidos, Nome </a> </syntaxhighlight>
  
 +
* 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.
  
−
<br />
+
'''Arquivo: bd.php'''
−
* Intenta agora modificar o código anterior e fai que o select manteña seleccionado o libro cando aparecen os exemplares.
 
  
 +
::<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 ====
  
 +
'''Arquivo: consultar_autores.php'''
  
 +
::<syntaxhighlight lang="php">
  
−
<br />
+
<?php
−
: 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"]));
+
ini_set('display_errors', '1');
−
            }
+
error_reporting(E_ALL);
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result->free(); // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
  
−
</syntaxhighlight>
+
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();
  
−
<br />
+
} catch (PDOException $e) {
−
* O código completo sería o seguinte:
 
  
−
'''Arquivo: bd.php''':
+
    error_log($e->getMessage());
 +
    die('Erro relacionado coa base de datos.');
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
} catch (Throwable $e) {
−
<?php
 
  
−
define ('SERVIDOR','localhost');
+
    die('Produciuse un erro na aplicación.');
−
define ('USUARIO','user_php');
 
−
define ('PASSWORD','user_php');
 
−
define ('BD','php');
 
  
 +
}
  
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
+
?>
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
 
−
define ('CONSULTAR_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>
+
<!DOCTYPE html>
  
 +
<html lang="gl">
 +
<head>
 +
    <meta charset="UTF-8">
 +
    <title>Autores</title>
 +
</head>
  
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
<body>
  
−
:::<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_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
if ($autores) {
 +
 
 +
    echo '<ul>';
  
−
try {
+
     foreach ($autores as $autor) {
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
  
−
?>
+
        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'
 +
            )
 +
        );
 +
    }
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
    echo '</ul>';
−
<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>
+
} else {
−
    <?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"]));
+
    echo 'Non hai datos que amosar.';
−
            }
 
−
            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>
 
</html>
 +
</syntaxhighlight>
 +
 +
* 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.
 +
 +
<br />
  
−
<?php
+
=== Consulta enviando parámetros de tipo numérico ===
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
* Cando unha consulta necesita recibir datos externos empregaremos unha '''sentenza preparada''' mediante:
  
 +
::<syntaxhighlight lang="php">
 +
prepare()
 +
execute() </syntaxhighlight>
  
 +
* Por exemplo:
  
−
<br />
+
::<syntaxhighlight lang="sql">
−
* '''<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.
+
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:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
CONSULTA_CON_PARAM_NUMERICO
 +
);
  
 +
$stmt->execute([
 +
'valor' => 5
 +
]); </syntaxhighlight>
  
−
<br />
+
* O nome do parámetro debe coincidir co indicado na sentenza SQL.
−
* 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''':
+
: Se a consulta emprega:
−
:::<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>
 
  
 +
::<syntaxhighlight lang="sql">
 +
:valor </syntaxhighlight>
  
 +
: debemos empregar:
  
−
<br />
+
::<syntaxhighlight lang="php">
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
$stmt->execute([
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="14" >
+
'valor' => 5
−
        ..................
+
]); </syntaxhighlight>
  
−
    <?php       
+
==== Indicar explicitamente o tipo de dato ====
−
        // 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>";
 
−
        }
 
−
       
 
−
    ?>   
 
−
 
 
−
        ..................
 
  
−
</syntaxhighlight>
+
* Cando enviamos os valores directamente mediante o array de `execute()`, PDO trátaos por defecto como `PDO::PARAM_STR`.
  
−
* 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).
+
: Na maioría das consultas MySQL realizará as conversións necesarias segundo o contexto.
  
 +
* Se queremos indicar explicitamente o tipo empregaremos `bindValue()`:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
CONSULTA_CON_PARAM_NUMERICO
 +
);
  
−
* Resultado:
+
$stmt->bindValue(
 +
':valor',
 +
5,
 +
PDO::PARAM_INT
 +
);
  
−
[[Imagen:Php_BD_12.jpg|400px|center]]
+
$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 />
−
=====Empregando PDO=====
 
−
* Crearemos unha nova páxina de nome '''consultar_exemplares_por_libro.php'''.
 
  
−
: Modificamos o código para que aparezan os libros dentro da etiqueta <select> de html:
+
==== Caso práctico: Enviando parámetros de tipo numérico ====
−
: 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...
+
 
 +
* 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'.
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
[[Imagen:Php_BD_9.jpg|800px|center]]
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require('/var/www/confBD/bd.php');
 
  
−
try {
 
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
 
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
?>
 
  
 +
* Primeiro definimos as consultas.
  
−
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
'''Arquivo: bd.php'''
−
    <html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
  
−
    <head>
+
::<syntaxhighlight lang="php">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
define(
−
        <title>Operacións BD</title>
+
'CONSULTAR_LIBROS',
−
    </head>
+
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo'
 +
);
  
−
    <body>
+
define(
−
        <?php
+
'CONSULTAR_EXEMPLARES_POR_LIBRO',
 +
'SELECT libro_id,
 +
id_exemplar,
 +
imaxe,
 +
prezo,
 +
estado_id
 +
FROM exemplares
 +
WHERE libro_id = :libro_id
 +
ORDER BY estado_id'
 +
);
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
+
define(
−
        if ($result && $result->rowCount() > 0) {
+
'CARTAFOL_IMAXES_WEB',
−
            echo "<select name='lstLibros'>";
+
'http://localhost/IMAXES/'
−
            echo "<option value='-1'></option>";
+
); </syntaxhighlight>
−
            while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
 
−
                printf("<option value='%d'>%s</option>", htmlentities($row["id"]), htmlentities($row["titulo"]));
 
−
            }
 
−
            echo "</select>";
 
  
−
            $result = null;
+
===== Solución empregando PDO =====
−
        } else {
+
: Fixarse que  
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
  
  
−
        ?>
+
'''Arquivo: consultar_exemplares_por_libro.php'''
−
    </body>
 
  
−
    </html>
+
::<syntaxhighlight lang="php">
  
 
<?php
 
<?php
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
    die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
−
:* 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.
+
ini_set('display_errors', '1');
−
: 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...
+
error_reporting(E_ALL);
  
−
: Resultado:
+
require '/var/www/confBD/bd.php';
−
[[Imagen:Php_BD_9.jpg|300px|center]]
 
  
 +
$libros = [];
 +
$exemplares = [];
  
 +
$libroSeleccionado = filter_input(
 +
    INPUT_POST,
 +
    'lstLibros',
 +
    FILTER_VALIDATE_INT
 +
);
  
 +
try {
  
−
* '''Arquivo bd.php'''
+
    $dsn = sprintf(
−
: Definimos nunha constate a consulta que vai obter os exemplares do libro seleccionado:
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
        SERVIDOR,
−
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');
+
        BD
−
</syntaxhighlight>
+
    );
−
:: Como vemos imos enviar o código por medio dun parámetro de tipo numérico.
 
−
: 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.
 
  
 +
    $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);
  
−
* Para facer que cando se seleccione un elemento do lista se 'active' o formulario, existen múltiples formas, unha delas pode ser:
+
    $libros = $stmt->fetchAll();
−
:::            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="java" line enclose="div" highlight="46,54-63,49-50" >
+
    /*
−
<?php
+
    * Se o usuario seleccionou un libro válido,
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
    * consultamos os seus exemplares.
−
ini_set('display_errors', 1);
+
    */
−
ini_set('display_startup_errors', 1);
+
    if (
−
error_reporting(E_ALL);
+
        $libroSeleccionado !== false &&
 +
        $libroSeleccionado !== null &&
 +
        $libroSeleccionado !== -1
 +
    ) {
  
−
require('/var/www/confBD/bd.php');
+
        $stmt = $conex->prepare(
 +
            CONSULTAR_EXEMPLARES_POR_LIBRO
 +
        );
  
−
try {
+
        $stmt->bindValue(
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
+
            ':libro_id',
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
+
            $libroSeleccionado,
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
            PDO::PARAM_INT
 +
        );
  
−
?>
+
        $stmt->execute();
  
 +
        $exemplares = $stmt->fetchAll();
 +
    }
  
−
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
} catch (PDOException $e) {
−
    <html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
  
−
     <head>
+
     error_log($e->getMessage());
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
     die('Erro relacionado coa base de datos.');
−
        <title>Operacións BD</title>
 
−
     </head>
 
  
−
    <body>
+
} catch (Throwable $e) {
−
        <?php
 
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
+
    die('Produciuse un erro na aplicación.');
−
        if ($result && $result->rowCount() > 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(PDO::FETCH_ASSOC)) {
 
−
                printf("<option value='%d'>%s</option>", htmlentities($row["id"]), htmlentities($row["titulo"]));
 
−
            }
 
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result = null;
 
−
        } 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'>";
 
  
−
            $stmt=$conex->prepare(CONSULTAR_EXEMPLARES_POR_LIBRO);    // Temos que poñer prepare
+
?>
−
            $result = $stmt->execute(['libro_id' => $_POST['lstLibros']]);
 
  
−
            if ($result) {
+
<!DOCTYPE html>
−
                while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
 
−
                    echo "<table border='1'>";
 
−
                    echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                    while ($row = $stmt->fetch(PDO::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"] ?? 0),htmlentities($row["estado_id"]));
 
−
                        echo '</tr>';
 
−
                    }
 
−
                    echo '</table>';
 
−
   
 
−
                }
 
−
                $stmt = null;  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
       
 
−
    ?>  
 
  
−
     </body>
+
<html lang="gl">
 +
<head>
 +
    <meta charset="UTF-8">
 +
     <title>Exemplares por libro</title>
 +
</head>
  
−
    </html>
+
<body>
  
−
<?php
+
<form method="post">
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
    die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
−
::* Liña 46: 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).
+
<select
−
::* Liña 49-50: 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.
+
    name="lstLibros"
−
::* Liñas 54-63: 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.
+
    onchange="this.form.submit()"
 +
>
  
−
::* Liña 49: 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.
+
    <option value="-1">
 +
        -- Escolle un libro --
 +
    </option>
  
−
:: Liña 60-61: Empregamos o operador ?? que ven indicar que se o valor é nulo dalle o valor que ten ao carón da ?. Como imaxe e prezo poden valor null desta forma aseguramos que htmlentities non dea un warning se o valor é nulo.
+
    <?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>
  
−
<br />
+
</form>
−
* Intenta agora modificar o código anterior e fai que o select manteña seleccionado o libro cando aparecen os exemplares.
 
  
 +
<?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>
  
−
<br />
+
        <?php foreach ($exemplares as $exemplar): ?>
−
: Posible solución:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="7-11" >
 
−
        $result = $conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->rowCount() > 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(PDO::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"]));
 
−
            }
 
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result = null;
 
−
        } else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
</syntaxhighlight>
 
  
 +
            <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>
−
* O código completo sería o seguinte:
 
  
−
'''Arquivo: bd.php''':
+
        <?php endforeach; ?>
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
    </table>
−
<?php
 
  
−
define ('SERVIDOR','localhost');
+
<?php else: ?>
−
define ('USUARIO','user_php');
 
−
define ('PASSWORD','user_php');
 
−
define ('BD','php');
 
  
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
+
    <p>Non hai exemplares que amosar.</p>
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
 
−
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://www.exames.es/IMAXES/');
 
−
</syntaxhighlight>
 
  
 +
<?php endif; ?>
  
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
<?php endif; ?>
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
</body>
−
<?php
+
</html>
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
</syntaxhighlight>
−
ini_set('display_errors', 1);
+
 
−
ini_set('display_startup_errors', 1);
+
* Neste exemplo empregamos `filter_input()` con `FILTER_VALIDATE_INT` para comprobar que o dato recibido é un número enteiro válido.
−
error_reporting(E_ALL);
 
  
−
require('/var/www/confBD/bd.php');
+
* Despois utilizamos unha sentenza preparada e:
  
−
try {
+
::<syntaxhighlight lang="php">
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
+
PDO::PARAM_INT </syntaxhighlight>
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
?>
+
: para indicar explicitamente que o identificador é numérico.
  
 +
* Non necesitamos empregar `mysqli_real_escape_string()` nin concatenar o dato dentro da consulta.
  
−
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
==== Amosar a descrición do estado ====
−
    <html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
  
−
    <head>
+
* Podemos mellorar a consulta realizando un `JOIN` coa táboa `estados` para amosar o estado do exemplar.
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
[[Imagen:Php_BD_12.jpg|800px|center]]
−
        <?php
 
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->rowCount() > 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(PDO::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"]));
 
−
            }
 
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result = null;
 
−
        } else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
        ?>
 
  
−
        <?php       
+
'''Arquivo: bd.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'>";
 
  
−
            $stmt=$conex->prepare(CONSULTAR_EXEMPLARES_POR_LIBRO);    // Temos que poñer prepare
+
::<syntaxhighlight lang="php">
−
            $result = $stmt->execute(['libro_id' => $_POST['lstLibros']]);
+
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>
  
−
            if ($result) {
+
* Para amosar o estado:
−
                while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
 
−
                    echo "<table border='1'>";
 
−
                    echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                    while ($row = $stmt->fetch(PDO::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"] ?? 0),htmlentities($row["estado_id"]));
 
−
                        echo '</tr>';
 
−
                    }
 
−
                    echo '</table>';
 
−
   
 
−
                }
 
−
                $stmt = null;  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
       
 
−
    ?>
 
  
−
    </body>
+
::<syntaxhighlight lang="php">
 +
echo htmlspecialchars(
 +
$exemplar['desc_estado'],
 +
ENT_QUOTES | ENT_SUBSTITUTE,
 +
'UTF-8'
 +
); </syntaxhighlight>
  
−
    </html>
+
<br />
  
−
<?php
+
==== Exercicio proposto ====
−
}    // Pecha o try do principio da páxina
+
 
−
catch (PDOException $e) {
+
* Crea unha páxina chamada `consultar_exemplares_por_estado.php`.
−
    die("Erro PDO: " . $e->getMessage());
+
 
−
} catch (Exception $e) {
+
* Debe:
−
    die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
:* 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.
  
  
  
−
<br />
+
[[Imagen:Php_BD_13.jpg|800px|center]]
−
* '''<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.
 
  
 +
:<u>Nota:</u> Podedes darlle un formato máis 'bonito' con CSS.
  
  
 +
* Consultas propostas:
  
 +
::<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>
  
 
<br />
 
<br />
−
* 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.php''':
+
=== Caso práctico: Enviando parámetros de tipo cadea ===
−
:::<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'
+
* Imos crear unha páxina que permita '''buscar libros polo nome dun autor'''.
−
            . ' 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
+
: O que se pretende programar é algo parecido a isto:
−
            . ' where libro_id=:libro_id order by desc_estado');  //  Necesita un espazo en branco antes do where para concatenar có anterior
+
[[Imagen:Php_BD_11.jpg|800px|center]]
−
</syntaxhighlight>
+
 
  
  
 +
: O usuario introducirá nunha caixa de texto o nome, os apelidos ou unha parte deles.
  
−
<br />
+
: Por exemplo, se introduce:
−
'''Arquivo: consultar_exemplares_por_libro.php''':
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="17,19" >
 
−
        ..................
 
  
−
    <?php       
+
::<syntaxhighlight lang="text">
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
+
gar </syntaxhighlight>
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
 
−
            echo "<div style='margin-top:40px'>";
 
  
−
            $stmt=$conex->prepare(CONSULTAR_EXEMPLARES_POR_LIBRO);    // Temos que poñer prepare
+
: poderían atoparse autores como `García`, `Vargas`, etc., dependendo dos datos existentes na base de datos.
−
            $result = $stmt->execute(['libro_id' => $_POST['lstLibros']]);
 
  
−
            if ($result) {
+
* Para realizar a busca empregaremos:
−
                while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
 
−
                    echo "<table border='1'>";
 
−
                    echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                    while ($row = $stmt->fetch(PDO::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"] ?? 0),htmlentities($row["desc_estado"]));
 
−
                        echo '</tr>';
 
−
                    }
 
−
                    echo '</table>';
 
−
   
 
−
                }
 
−
                $stmt = null;  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
    ?>   
 
  
−
        ..................
+
:* 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.
  
−
</syntaxhighlight>
+
* A consulta devolverá:
  
−
* Liña 17: 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).
+
:* 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:
  
−
* Resultado:
+
::<syntaxhighlight lang="text">
 +
detalle_libro.php </syntaxhighlight>
  
−
[[Imagen:Php_BD_12.jpg|400px|center]]
+
: á 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á:
  
 +
::<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 =====
  
 +
* Engadimos a consulta que permite buscar libros polo nome ou apelidos dun autor:
  
 +
::<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>
  
−
<br />
+
* A consulta une as táboas:
−
=====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.
+
::<syntaxhighlight lang="text">
−
: 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).
+
libros
−
: O radiobutton debe manterse seleccionado.
+
libro_autor
 +
autores </syntaxhighlight>
  
 +
: para obter os libros correspondentes a cada autor.
  
 +
* Mediante:
  
 +
::<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:
  
 +
::<syntaxhighlight lang="sql">
 +
LIKE :autor </syntaxhighlight>
  
 +
: sendo `:autor` o parámetro que enviaremos posteriormente desde PDO.
  
 +
* O parámetro debe representar o valor completo.
  
−
<br />
+
: Polo tanto, non escribiremos:
−
'''POSIBLE SOLUCIÓN ORIENTADA A OBXECTOS''':
 
  
−
:<u>Nota</u>: Queda para vos facer a solución empregando PDO.
+
::<syntaxhighlight lang="sql">
 +
LIKE '%:autor%' </syntaxhighlight>
  
−
'''Arquivo bd.php'''
+
: senón que os caracteres `%` serán engadidos ao valor desde 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>  
+
::<syntaxhighlight lang="php">
 +
'%' . $textoBuscar . '%' </syntaxhighlight>
  
−
: 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)
+
===== Solución completa =====
  
 +
'''Arquivo: buscar_libros_por_autor.php'''
  
−
'''Arquivo consultar_exemplares_por_estado.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_startup_errors', 1);
+
* 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);
 
error_reporting(E_ALL);
  
−
require ('/var/www/confBD/bd.php');
+
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 {
 
try {
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da 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">
+
    * Creamos o DSN.
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
    */
−
     <head>
+
     $dsn = sprintf(
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
         <title>Operacións BD</title>
+
        SERVIDOR,
−
    </head>
+
        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.';
  
−
    <body>
+
            /*
−
    <body>
+
        * Como regra da aplicación esiximos
−
         <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
+
        * polo menos 3 caracteres.
 +
        */
 +
         } elseif (mb_strlen($textoBuscar) < 3) {
  
−
        <?php       
+
             $erro =
−
        $cont=0;    // Conta o número de estados impresos e cando leve múltiplo de dous salta de liña
+
                 'A cadea a buscar debe ter polo menos 3 caracteres.';
−
        $result=$conex->query(CONSULTAR_ESTADOS);
+
         } else {
−
        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>
 
  
 +
            /*
 +
            * Preparamos a consulta.
 +
            */
 +
            $stmt = $conex->prepare(
 +
                CONSULTAR_LIBROS_POR_AUTOR
 +
            );
  
−
<?php
 
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
            /*
 +
            * 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();
  
−
* Resultado:
 
  
−
[[Imagen:Php_BD_13.jpg|400px|center]]
+
            /*
 +
            * 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 />
+
    /*
 +
    * Rexistramos a información técnica no log.
 +
    * Non debemos amosala ao usuario.
 +
    */
 +
    error_log(
 +
        $e->getMessage()
 +
    );
  
−
=== Consulta enviando parámetros de tipo cadea ===
+
    die('Erro relacionado coa base de datos.');
 +
} catch (Throwable $e) {
  
−
* Cando utilicemos datos de tipo cadea, debemos lembrar poñelos entre comillas simples ('dato') na orde SQL.
+
    die('Produciuse un erro na aplicación.');
 +
}
  
 +
?>
  
−
<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].
+
<!DOCTYPE html>
  
 +
<html lang="gl">
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
+
    <head>
−
: 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''''
+
        <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) {
  
−
: No exemplo, col2 é de tipo cadea (por exemplo, varchar ou char).
+
                window.open(
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
                    url,
−
define ('CONSULTA_CON_PARAM_CADEA',"SELECT col1, col2 FROM TABOA_1 WHERE col2='%s'");
+
                    '_blank',
 +
                    'popup,width=600,height=400,noopener'
 +
                );
 +
            }
 +
        </script>
 +
    </head>
  
−
$query = sprintf(CONSULTA_CON_PARAM_CADEA,'valor_cadea_a_enviar');
+
<body>
−
</syntaxhighlight>  
 
−
: Fixarse como o parámetro col2 vai entrecomillado e o formato do parámetro é '%s'.
 
  
 +
    <h1>Buscar libros por autor</h1>
 +
    <form method="post">
  
−
: Agora facemos o mesmo código que no exemplo anterior.
+
        <label for="txtAutor">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
            Introduce o nome ou apelidos do autor:
−
......
+
        </label>
−
$result=$conex->query(CONSULTA_CON_PARAM_CADEA);
+
        <input
−
......
+
            type="text"
−
</syntaxhighlight>  
+
            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; ?>
  
−
<br />
+
    <?php if ($libros): ?>
−
==== Caso práctico: Enviando parámetros de tipo cadea ====
+
        <table border="1">
 +
            <tr>
 +
                <th>Autor</th>
 +
                <th>Libro</th>
 +
            </tr>
 +
            <?php
  
−
* Imos a aplicalo ao noso exemplo e imos crear unha páxina no que buscaremos libros en base a un nome do autor.  
+
        /*
−
:: 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.
+
        * Esta variable permite saber cal foi
−
:: 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 último autor que amosamos.
−
:: 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].
+
        */
 +
            $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 {
  
−
* O usuario introducirá nunha caixa de texto o nome (ou parte del) do autor.
+
                    $autorAmosar =
 +
                        $libro['nome_autor'];
 +
                }
  
−
: <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].
+
            ?>
 +
                <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;">
  
−
:: 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'.
+
                            <?= htmlspecialchars(
−
:: Por exemplo:
+
                                $libro['titulo'],
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
                                ENT_QUOTES | ENT_SUBSTITUTE,
−
$cidade = "'s Hertogenbosch";
+
                                'UTF-8'
 +
                            ) ?>
 +
                        </a>
 +
                    </td>
 +
                </tr>
 +
            <?php
  
−
/* esta consulta fallará debido a que no escapa $ciudad */
+
            /*
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
+
            * Gardamos o autor actual para comparalo
−
    printf("Erro: %s\n", $mysqli->sqlstate);
+
            * co autor da seguinte fila.
−
}
+
            */
 +
                $ultimoAutor =
 +
                    $libro['nome_autor'];
  
−
$cidade = $mysqli->real_escape_string($cidade);
+
            endforeach;
 +
            ?>
 +
        </table>
 +
    <?php endif; ?>
 +
</body>
 +
</html>
 +
</syntaxhighlight>
  
−
/* esta consulta con $ciudad escapada funcionará */
+
===== Explicación da solución =====
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
 
−
    printf("Fila inxerida.\n");
 
−
}
 
  
−
</syntaxhighlight>
+
* Recuperamos o texto enviado polo formulario mediante:
−
::Exemplo obtido de php.net
 
  
 +
::<syntaxhighlight lang="php">
 +
$textoBuscar = trim(
 +
$_POST['txtAutor'] ?? ''
 +
); </syntaxhighlight>
  
−
* 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:
+
* Non empregamos `htmlspecialchars()` neste momento.
  
−
[[Imagen:Php_BD_10.jpg|500px|center]]
+
: Neste punto estamos '''validando e procesando un dato de entrada'''.
  
−
:: 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.
+
: `htmlspecialchars()` empregarase posteriormente cando o dato se introduza dentro do HTML.
  
−
:: 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);
+
<br />
 +
'''Validación do dato'''
  
−
                        $last_autor=$row["nome_autor"];  // Gardamos o nome do autor para comparar có autor que veña na seguinte fila de datos.
+
* Comprobamos primeiro que a cadea non estea baleira:
−
                    }
 
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php">
 +
if ($textoBuscar === '') { </syntaxhighlight>
  
 +
* Como exemplo dunha regra de validación da aplicación, esiximos tamén unha lonxitude mínima:
  
 +
::<syntaxhighlight lang="php">
 +
elseif (mb_strlen($textoBuscar) < 3) { </syntaxhighlight>
  
−
* 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).
+
: A esixencia de tres caracteres é unha regra da nosa aplicación.
  
−
:: Para facelo imos facer uso de javascript e da [http://www.w3schools.com/jsref/met_win_open.asp función window.open].
+
: Non é unha medida para evitar inxección SQL.
−
:: 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>
 
  
  
−
:: 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" >
 
−
<a href='#' onclick="javascript:abrirVentana('detalle_libro.php?libro=1');">Título Libro</a>
 
−
</syntaxhighlight>
 
  
−
:: <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>.
+
<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:
  
 +
::<syntaxhighlight lang="php">
 +
PDO::PARAM_STR </syntaxhighlight>
  
 +
: porque estamos enviando unha cadea.
  
  
  
 
<br />
 
<br />
−
* '''SOLUCIÓN'''
+
'''Funcionamento de LIKE'''
  
−
'''Arquivo: bd.inc.php'''
+
* Os caracteres `%` actúan como comodíns.
  
−
<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''')
+
: Se o usuario introduce:
−
::<syntaxhighlight lang="java" enclose="div">
 
−
<?php
 
  
−
define ('CONSULTAR_LIBROS_POR_AUTOR',"select CONCAT(apelidos,',',nome) as nome_autor,libros.id ,titulo from libros " .
+
::<syntaxhighlight lang="text">
−
        ' INNER JOIN libro_autor ON (libro_autor.libro_id=libros.id) ' .
+
gar </syntaxhighlight>
−
        ' 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.
+
: PHP enviará:
−
: É 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 %%.
 
  
 +
::<syntaxhighlight lang="text">
 +
%gar% </syntaxhighlight>
  
 +
* Desta forma poderán coincidir cadeas nas que `gar` apareza ao principio, no medio ou ao final.
  
  
 
<br />
 
<br />
−
'''Arquivo buscar_libros_por_autor.php'''
+
'''Execución e recuperación dos resultados'''
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
−
<?php
+
* Executamos:
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
 
−
ini_set('display_errors', 1);
+
::<syntaxhighlight lang="php">
−
ini_set('display_startup_errors', 1);
+
$stmt->execute(); </syntaxhighlight>
−
error_reporting(E_ALL);
+
 
 +
* E recuperamos os resultados:
 +
 
 +
::<syntaxhighlight lang="php">
 +
$libros = $stmt->fetchAll(); </syntaxhighlight>
  
−
require ('/var/www/confBD/bd.inc.php');
+
* Como na conexión configuramos:
  
−
try {
+
::<syntaxhighlight lang="php">
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
+
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC </syntaxhighlight>
−
    $conex->set_charset('utf8mb4');
 
  
−
$erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
: cada fila recuperada será un array asociativo.
−
?>
 
  
−
<!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>
 
−
        <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>
+
<br />
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
+
'''Evitar repetir o nome do autor'''
−
            Introduce o nome do autor a buscar:
+
 
−
            <input type='text' size='50' maxlength="100" required name="txtAutor" value='<?php echo (isset($_POST['txtAutor']) ? htmlentities($_POST["txtAutor"]):""); ?>'/>
+
* Pode suceder que un mesmo autor teña varios libros.
−
            <input type="submit" value="BUSCAR" />
+
 
−
            <input type='button' onclick="javascript:this.form['txtAutor'].value='';" value="LIMPAR" />
+
: Por exemplo:
−
        <?php       
+
 
−
        if (!empty($_POST['txtAutor'])){  // Premimos o botón de busca
+
::<syntaxhighlight lang="text">
−
            $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
+
García, Ana | Libro A
−
            if (strlen($textoBuscar)<3){  // É un exemplo de control de erros. Indico que o texto a buscar ten que ter a lo menos 3 caracteres
+
García, Ana | Libro B
−
                $erro="<h2>A cadea a buscar non pode ser menor de 3 caracteres</h2>";
+
García, Ana | Libro C
−
            }
+
Pérez, Luís | Libro D </syntaxhighlight>
−
            else {
+
 
−
                $query=sprintf(CONSULTAR_LIBROS_POR_AUTOR,$conex->real_escape_string($textoBuscar));
+
* Queremos amosalo desta forma:
−
                $result=$conex->query($query);
 
−
                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='';  // O autor é o mesmo e non queremos que se repita na táboa
 
−
                        }
 
−
                        else {
 
−
                            $autor=htmlentities($row["nome_autor"]);
 
−
                        }
 
−
                        echo '<tr>';
 
−
                        $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']));
 
−
                        echo '</tr>';
 
−
                        $last_autor=$row["nome_autor"];
 
−
                    }
 
−
                    $result->free();
 
−
                    echo "</table>";
 
−
                }
 
−
                else {
 
−
                    $erro = "<br />Non hai resultados coa cadea $textoBuscar";
 
−
                }
 
−
               
 
−
               
 
−
            }
 
−
        }
 
−
       
 
−
        ?>       
 
−
        </form>
 
−
        <?php       
 
−
        if ($erro!='') {
 
−
            printf("<h3>%s</h3>",$erro);
 
−
        }
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
::<syntaxhighlight lang="text">
−
}  // Pecha o try da parte superior da páxina
+
García, Ana | Libro A
−
catch (mysqli_sql_exception $e) {
+
            | Libro B
−
    die("Erro mysql: " . $e->getMessage());
+
            | Libro C
−
}
+
Pérez, Luís | Libro D </syntaxhighlight>
−
catch (Expcetion $e) {
+
 
−
    die("Erro xeral: " . $e->getMessage());
+
* Para iso gardamos o último autor procesado:
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
−
* 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.
+
::<syntaxhighlight lang="php">
−
:: Polo tanto a conexión e o peche da base de datos se podería poñer dentro do else da liña 43.
+
$ultimoAutor = ''; </syntaxhighlight>
−
* 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.
 
  
 +
* En cada iteración comparamos:
  
−
* '''Resultado:'''
+
::<syntaxhighlight lang="php">
−
[[Imagen:Php_BD_11.jpg|500px|center]]
+
if ($ultimoAutor ===$libro['nome_autor']) {
  
 +
  $autorAmosar = '';
  
 +
} else {
 +
    $autorAmosar = $libro['nome_autor'];
 +
}
 +
</syntaxhighlight>
  
 +
* E ao rematar gardamos o autor actual:
  
 +
::<syntaxhighlight lang="php">
 +
$ultimoAutor =
 +
$libro['nome_autor']; </syntaxhighlight>
  
  
 
<br />
 
<br />
 +
'''Abrir o detalle dun libro'''
  
−
=== Consulta chamando a procedementos almacenados ===
+
* Cada título será un enlace á páxina:
  
−
* 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.
+
::<syntaxhighlight lang="text">detalle_libro.php </syntaxhighlight>
  
−
* 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.
+
* O identificador enviarase mediante `GET`:
  
−
: 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.
+
::<syntaxhighlight lang="text">detalle_libro.php?libro=5 </syntaxhighlight>
−
: 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.
 
  
  
−
* Máis información [http://php.net/manual/es/mysqli.quickstart.stored-procedures.php neste enlace].
 
  
−
* O primeiro que temos que facer é crear o procedemento almacenado.
+
* O enlace xerado será:
−
* A orde SQL é [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html '''CREATE PROCEDURE'''].
+
::<syntaxhighlight lang="php">
 +
<a href="detalle_libro.php?libro=<?= (int) $libro['id_libro'] ?>" onclick="abrirVentana(this.href); return false;">
 +
</syntaxhighlight>
  
−
* Creamos o procedemento almacenado:
+
* Mediante:
−
:* 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].
 
  
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
+
::<syntaxhighlight lang="javascript">this.href </syntaxhighlight>
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `NOME_PROCEDEMENTO` ()
+
: obtemos a URL real almacenada no atributo `href`.
−
BEGIN
 
  
−
  select col1,col2
+
* Chamamos despois á función:
−
  from TABOA_1
 
−
  order by col1;
 
  
 +
::<syntaxhighlight lang="javascript">abrirVentana(this.href); </syntaxhighlight>
  
−
END
+
* Finalmente:
−
$$
 
−
</syntaxhighlight>
 
  
−
: Como vemos o procedemento está formado por unha consulta SELECT (pode poñerse calquera)
+
::<syntaxhighlight lang="javascript">return false; </syntaxhighlight>
  
 +
: evita que o navegador abra tamén o enlace na xanela actual.
  
−
* Agora dende PHP temos que facer uso do comando CALL 'NOME_PROCEDEMENTO'.
 
−
: Isto é o que teriamos que poñer se dende Mysql queredes chamar ao procedemento....
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="12" >
 
−
<?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)
+
<br />
 +
'''Función JavaScript'''
  
−
try {
+
* A función empregada é:
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
−
    $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
  
−
} 
+
::<syntaxhighlight lang="javascript">
−
catch (mysqli_sql_exception $e) {
+
function abrirVentana(url) {
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
  
 +
window.open(
 +
    url,
 +
    '_blank',
 +
    'popup,width=600,height=400,noopener'
 +
);
  
−
</syntaxhighlight>  
+
} </syntaxhighlight>
 +
 
 +
* `window.open()` solicita ao navegador a apertura dunha nova xanela.
  
 +
* Indicamos aproximadamente:
 +
::<syntaxhighlight lang="text">
 +
width=600
 +
height=400 </syntaxhighlight>
  
 +
* A opción:
 +
::<syntaxhighlight lang="text">
 +
noopener </syntaxhighlight>
 +
: evita que a nova xanela poida acceder á xanela orixinal mediante `window.opener`.
  
−
* A diferenza do caso anterior, cando chamemos a un procedemento almacenado teremos que facer uso dunha das seguintes funcións:
+
'''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.
−
:: [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="java" line enclose="div" highlight="3,4" >
 
−
        ........................
 
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
  
−
        }
 
  
−
</syntaxhighlight>  
+
<br />
 +
'''Vantaxe de empregar un href real'''
  
 +
* Non empregamos:
 +
::<syntaxhighlight lang="html"> <a href="#"> </syntaxhighlight>
  
−
: Unha vez temos o conxunto de resultados ($result) xa operamos como nos casos anteriores.
+
* Empregamos:
 +
::<syntaxhighlight lang="html"> <a href="detalle_libro.php?libro=5"> </syntaxhighlight>
  
 +
: 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.
  
−
*<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.
 
−
: A resposta a ese erro a tedes [https://dev.mysql.com/doc/refman/8.0/en/commands-out-of-sync.html neste enlace].
 
  
  
 +
<br />
 +
'''Páxina detalle_libro.php'''
  
−
* 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.
+
* A páxina de detalle recibirá o identificador do libro mediante:
−
: 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());
 
  
−
        }
+
::<syntaxhighlight lang="text">$_GET['libro'] </syntaxhighlight>
  
−
</syntaxhighlight>
+
* Como é un dato externo, debemos validalo:
  
 +
::<syntaxhighlight lang="php">
 +
$libroId = filter_input(
 +
INPUT_GET,
 +
'libro',
 +
FILTER_VALIDATE_INT
 +
);
  
 +
if (
 +
  $libroId === false ||
 +
  $libroId === null ||
 +
  $libroId < 1
 +
) {
  
 +
  die('Identificador de libro non válido.');
  
−
* 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>
  
−
::<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();
 
−
            }
 
  
−
        }
 
  
−
</syntaxhighlight>  
+
* A consulta posterior tamén deberá ser preparada:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
  'SELECT id, titulo
 +
  FROM libros
 +
  WHERE id = :libro_id'
 +
);
  
−
: Se imos procesar todos os conxuntos de resultados da mesma forma poderíamos utilizar un while{}...
+
$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
 +
$stmt->execute();
  
 +
$libro = $stmt->fetch();
 +
</syntaxhighlight>
  
 +
* Se queremos recuperar tamén os exemplares dese libro faremos outra consulta preparada:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT id_exemplar,imaxe,prezo,estado_id
 +
  FROM exemplares
 +
  WHERE libro_id = :libro_id
 +
  ORDER BY id_exemplar'
 +
);
  
−
<br />
+
$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
−
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
+
$stmt->execute();
  
−
* 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.
+
$exemplares = $stmt->fetchAll();
 +
</syntaxhighlight>
  
 +
===== Resumo do proceso =====
  
−
* Creamos o procedemento almacenado:
+
::<syntaxhighlight lang="text">
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
+
$_POST['txtAutor']
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
↓
−
DELIMITER $$
+
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>
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS` ()
+
</syntaxhighlight>
−
BEGIN
 
  
−
  select ìd,titulo
 
−
  from libros
 
−
  order by titulo;
 
  
 +
* Debemos diferenciar claramente:
  
−
END
+
::<syntaxhighlight lang="text">
−
$$
 
−
</syntaxhighlight>  
 
  
 +
! 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`.            |
 +
| }                                                      |
 +
</syntaxhighlight>
  
−
* Definimos nunha constante o nome do procedemento almacenado dentro do arquivo bd.inc.php.
 
  
−
:'''Arquivo: bd.inc.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_LIBROS','CONSULTAR_LIBROS()');  // Se levara parámetros faríamos o mesmo que nos casos anteriores coas sentencias select, empregando %s, %d...
 
−
</syntaxhighlight>
 
  
 +
<br />
  
 +
=== Consulta chamando a procedementos almacenados ===
  
−
* 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]
+
* Un procedemento almacenado permite gardar no propio xestor de base de datos un conxunto de sentenzas SQL que poden ser executadas posteriormente.
−
:: 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'''
+
* Desde PDO chamarémolo mediante a sentenza:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="24-26" >
+
 
−
<?php
+
::<syntaxhighlight lang="sql">
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
CALL NOME_PROCEDEMENTO() </syntaxhighlight>
−
ini_set('display_errors', 1);
+
 
−
ini_set('display_startup_errors', 1);
+
* Por exemplo, en MySQL:
−
error_reporting(E_ALL);
+
 
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
 +
 
 +
CREATE PROCEDURE CONSULTAR_LIBROS()
 +
BEGIN
 +
 
 +
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo;
 +
 
 +
END$$
  
−
require ('/var/www/confBD/bd.inc.php');
+
DELIMITER ; </syntaxhighlight>
  
−
try {
+
==== Empregando PDO ====
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
?>
+
::<syntaxhighlight lang="php">
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
$stmt = $conex->prepare(
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
'CALL CONSULTAR_LIBROS()'
−
<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()
+
$stmt->execute();
−
:* 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.
 
  
 +
$libros = $stmt->fetchAll();
  
 +
$stmt->closeCursor(); </syntaxhighlight>
  
−
<br />
+
* `closeCursor()` libera os resultados pendentes asociados á chamada.
−
=== Consulta chamando a procedementos almacenados con parámetros de entrada ===
 
  
−
* Podemos chamar a un procedemento almacenado enviando datos ao mesmo en forma de parámetros.
+
: É especialmente recomendable ao traballar con procedementos almacenados en MySQL antes de executar outra consulta na mesma conexión.
−
: 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'''.
 
−
::* 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.
 
−
::* 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.
 
−
::* Os <u>parámetros de entrada/entrada</u> (INOUT) son unha combinación dos dous anteriores.
 
  
−
: Máis información [http://www.mysqltutorial.org/stored-procedures-parameters.aspx neste enlace].
+
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
  
−
* Por defecto, se non especificamos nada, todos os parámetros dun procedemento almacenado son considerados 'de entrada'
+
* Definimos o nome do procedemento:
  
−
* Primeiro teremos que definir o procedemento en Mysql:
+
::<syntaxhighlight lang="php">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
define(
−
DELIMITER $$
+
'PROCEDEMENTO_CONSULTAR_LIBROS',
 +
'CONSULTAR_LIBROS'
 +
); </syntaxhighlight>
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100))
+
* Realizamos a chamada:
−
BEGIN
 
  
−
    SELECT col1,col2
+
::<syntaxhighlight lang="php">
−
    FROM TABOA_1
+
$stmt = $conex->prepare(
−
    WHERE col2=_param1;
+
'CALL ' .
 +
PROCEDEMENTO_CONSULTAR_LIBROS .
 +
'()'
 +
);
  
−
END
+
$stmt->execute();
−
</syntaxhighlight>  
 
  
−
: Como vemos, xunto ao nome do procedemento hai que especificar o tipo (dentro dos tipos de mysql).
+
$libros = $stmt->fetchAll();
−
: Unha vez definidos podemos utilizalos dentro do procedemento almacenado.
 
−
: 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.
 
  
 +
$stmt->closeCursor(); </syntaxhighlight>
  
−
: Podemos definir máis parámetros, separando por comas cada un deles, por exemplo:
+
* Non necesitamos comprobar o resultado de `execute()` para saber se o `SELECT` devolveu filas.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="3,8,9" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100),_param2 int)
+
: `execute()` indica se a sentenza puido executarse.
−
BEGIN
 
  
−
    SELECT col1,col2
+
: Para saber se temos resultados comprobamos os datos recuperados:
−
    FROM TABOA_1
 
−
    WHERE col2=_param1 and
 
−
          col1=_param2
 
  
−
END
+
::<syntaxhighlight lang="php">
−
</syntaxhighlight>  
+
if ($libros) {
  
  
−
* Unha  vez definido teremos que enviar os datos que queiramos dende PHP desta forma:
+
// Hai resultados
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
 
−
</syntaxhighlight>
 
−
: 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.
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="5,6" >
+
} else {
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
 
  
−
        ........................
+
// Non hai resultados
  
−
        $proc = sprintf(CHAMAR_PROCEDEMENTO_CON_PARAMETROS,valor_numérico,'valor_cadea');
+
} </syntaxhighlight>
−
        $proc = sprintf("CALL %s",$proc);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
        }
 
−
 
 
−
</syntaxhighlight>  
 
−
 
 
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
 
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
  
 +
<br />
  
 +
=== Consulta chamando a procedementos almacenados con parámetros de entrada ===
  
 +
* Os parámetros dun procedemento almacenado poden ser:
  
−
<br />
+
:* `IN`: parámetros de entrada.
−
==== Caso Práctico: Consulta chamando a procedementos almacenados con parámetros de entrada ====
+
:* `OUT`: parámetros de saída.
 +
:* `INOUT`: funcionan como entrada e saída.
  
−
* 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.
+
* Por exemplo:
  
−
'''Solución:'''
+
::<syntaxhighlight lang="sql">
−
* '''Procedemento almacenado CONSULTAR_LIBROS_POR_AUTOR en Mysql'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
DELIMITER $$
 
DELIMITER $$
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS_POR_AUTOR`(IN p_autor_buscar varchar(100))
+
CREATE PROCEDURE PROCEDEMENTO_CON_PARAMETROS(
 +
IN p_num INT,
 +
IN p_text VARCHAR(100)
 +
)
 
BEGIN
 
BEGIN
  
−
select CONCAT(apelidos,',',nome) as nome_autor,libros.id as id_libro,titulo from libros
+
SELECT col1, col2
−
            INNER JOIN libro_autor ON (libro_autor.libro_id=libros.id)
+
FROM TABOA_1
−
            INNER JOIN autores ON (libro_autor.autor_id=autores.id)
+
WHERE col1 = p_num
−
where CONCAT(autores.nome,' ',autores.apelidos) like  CONCAT('%', p_autor_buscar , '%')
+
  AND col2 = p_text;
−
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 ====
 +
 
 +
* Preparamos a chamada igual que calquera outra sentenza parametrizada:
 +
 
 +
::<syntaxhighlight lang="php">
 +
$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
 +
);
  
−
* Definimos a constante en PHP no que está definido o nome do procedemento e o parámetro que vai levar.
+
$stmt->execute();
−
:'''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.
+
$filas = $stmt->fetchAll();
  
 +
$stmt->closeCursor(); </syntaxhighlight>
  
 +
* Desta forma non necesitamos empregar `sprintf()` nin concatenar os valores dentro da chamada SQL.
  
−
* '''Arquivo buscar_libros_por_autor_proc.php'''
+
==== Caso Práctico: buscar libros por autor ====
−
::<syntaxhighlight lang="java" line enclose="div" highlight="44-47" >
 
−
<?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');
+
* Lembrar que o usuario que se conecta á base de datos dende php ten que ter permiso execute sobre a base de datos.
 +
::<syntaxhighlight lang="sql">
 +
grant execute on php.* to 'user_php'@'localhost';
 +
flush privileges;
 +
</syntaxhighlight>
  
−
try {
 
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
    $erro="";  // Variable que a usaremos para amosar os posibles erros que poidan darse
+
* Procedemento almacenado (isto tedes que executalo en Mysql empregando a ferramenta Mysql WorkBench ou PhpMyAdmin):
  
−
?>
+
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
CREATE PROCEDURE CONSULTAR_LIBROS_POR_AUTOR(
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
IN p_autor_buscar VARCHAR(100)
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
)
−
     <head>
+
BEGIN
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
 
−
        <title>Operacións BD</title>
+
SELECT CONCAT(
−
        <script type="text/javascript">
+
          autores.apelidos,
−
            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');
+
          autores.nome
−
            }
+
      ) AS nome_autor,
−
        </script>
+
      libros.id AS id_libro,
−
       
+
      libros.titulo
−
    </head>
+
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>
  
−
    <body>
+
* Chamada desde PDO cambiando no exercicio anterior a chamada ao SELECT pola chamada a este procedemento almacenado:
−
        <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" highlight="23-25,58">
−
}    // Pecha o try do principio da páxina
+
     if ($_SERVER['REQUEST_METHOD'] === 'POST') {
−
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)
+
        /*
 +
        * 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)'
 +
            );
  
−
<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.
+
            /*
 +
            * 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
 +
            );
  
−
* Primeiro teremos que definir o procedemento en Mysql:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="3,7" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(OUT _param1 varchar(100))
+
            /*
−
BEGIN
+
            * Executamos a consulta.
 +
            */
 +
            $stmt->execute();
  
−
    SELECT col2
 
−
    INTO _param1
 
−
    FROM TABOA_1
 
−
    WHERE col1=1;
 
  
−
END
+
            /*
−
</syntaxhighlight>  
+
            * Recuperamos todos os resultados.
 +
            */
 +
            $libros = $stmt->fetchAll();
  
−
: Agora especificamos que o parámetro é de saída antepoñendo a palabra OUT ao seu nomo.
+
            $stmt->closeCursor();
−
: 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;
 
  
  
 +
            /*
 +
            * Se non hai filas gardamos unha mensaxe
 +
            * para amosala posteriormente.
 +
            */
 +
            if (!$libros) {
  
−
* Unha  vez definido teremos que construír a chamada ao procedemento enviando o nome dunha variable (para recoller o valor modificado).
+
                $erro =
−
: O nome da variable terá este formato: '''@'''nomeVariable:
+
                    'Non se atoparon libros para ese autor.';
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
            }
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS("@nomeVariable")');
+
        }
−
</syntaxhighlight>  
+
    }
−
: Fixarse como a variable é de tipo cadea e vai entre comiñas.
+
</syntaxhighlight>
  
 +
<br />
  
−
* 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.
+
=== Consulta chamando a procedementos almacenados con parámetros de saída ===
−
: 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" >
+
* Un parámetro `OUT` permite que un procedemento devolva un valor.
−
<?php
 
−
        define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS("@nomeVariable")');
 
  
−
        ........................
+
* Por exemplo:
  
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
+
::<syntaxhighlight lang="sql">
−
        if ($conex->multi_query($proc)){
+
DELIMITER $$
−
            $result = $conex->store_result();
 
  
−
            if (!($resultado = $conex->query("SELECT @nomeVariable as valorDevolto"))) {
+
CREATE PROCEDURE CONSULTAR_NUM_LIBROS(
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
+
OUT p_num_libros INT
−
            }
+
)
−
            else {
+
BEGIN
−
                $fila = $resultado->fetch_assoc();
 
−
                echo $fila['valorDevolto']);
 
−
            }
 
  
−
        }
+
SELECT COUNT(*)
−
        ........................
+
INTO p_num_libros
 +
FROM libros;
  
 +
END$$
  
−
?>
+
DELIMITER ; </syntaxhighlight>
  
−
</syntaxhighlight>
+
* Con PDO e MySQL podemos empregar unha variable de usuario de MySQL:
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor') // 5 e valor son datos de exemplo
+
::<syntaxhighlight lang="php">
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
+
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_NUM_LIBROS(
 +
@numero_libros
 +
)'
 +
);
  
 +
$stmt->execute();
  
 +
/*
  
−
: <u>Nota:</u> Non fai falla facer un alias do nome da variable. Podemos empregar o propio nome no fetch_assoc().
+
* É importante liberar os resultados
−
:: Podemos poñer:
+
* da chamada antes de executar outro SELECT.
 +
  */
 +
  $stmt->closeCursor();
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="5,10" >
+
$stmt = $conex->query(
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
+
'SELECT @numero_libros'
−
        if ($conex->multi_query($proc)){
+
);
−
            $result = $conex->store_result();
 
  
−
            if (!($resultado = $conex->query("SELECT @nomeVariable"))) {
+
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
−
            else {
 
−
                $fila = $resultado->fetch_assoc();
 
−
                echo $fila['@nomVariable']);
 
−
            }
 
  
−
        }
+
* Podemos amosar o resultado:
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="php">
 +
printf(
 +
'<h1>Número de libros á venda: %d</h1>',
 +
$numLibros
 +
); </syntaxhighlight>
  
 +
<br />
  
 +
=== Consulta chamando a funcións ===
  
−
<br />
+
* MySQL tamén permite definir funcións almacenadas.
−
==== 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.
+
* Unha función, a diferenza dun procedemento, devolve directamente un valor e pode empregarse dentro dunha expresión SQL.
  
 +
* Por exemplo:
  
−
* '''Procedemento almacenado CONSULTAR_NUM_LIBROS en Mysql'''
+
::<syntaxhighlight lang="sql">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
DELIMITER $$
 
DELIMITER $$
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_NUM_LIBROS`(OUT p_num_libros int)
+
CREATE FUNCTION CONSULTAR_NUM_LIBROS()
 +
RETURNS INT
 +
READS SQL DATA
 
BEGIN
 
BEGIN
−
SELECT COUNT(*)
 
−
INTO p_num_libros
 
−
FROM libros;
 
  
−
END
+
DECLARE num_libros INT;
−
$$
+
 
−
</syntaxhighlight>  
+
SELECT COUNT(*)
 +
INTO num_libros
 +
FROM libros;
 +
 
 +
RETURN num_libros;
 +
 
 +
END$$
 +
 
 +
DELIMITER ; </syntaxhighlight>
  
 +
==== Empregando PDO ====
  
−
: 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.
+
* Podemos chamar á función mediante un `SELECT`:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT CONSULTAR_NUM_LIBROS()'
 +
);
  
−
* '''Arquivo bd.inc.php''':
+
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
−
* Definimos a chamada ao procedemento cunha variable no seu interior que vai ser a que garde o valor modificado de dentro do procedemento.
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_NUM_LIBROS','CONSULTAR_NUM_LIBROS(@numero_libros)');
 
−
</syntaxhighlight>
 
  
 +
* `fetchColumn()` resulta especialmente útil cando unha consulta devolve un único valor.
  
 +
==== Funcións con parámetros ====
  
−
* '''Arquivo consultar_num_libros.php'''
+
* Supoñamos unha función:
−
: 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.
 
−
::<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');
+
::<syntaxhighlight lang="sql">
 +
SELECT Suma(5, 7) </syntaxhighlight>
  
−
try {
+
* Desde PDO:
−
    $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.
+
::<syntaxhighlight lang="php">
−
?>
+
$stmt = $conex->prepare(
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
'SELECT Suma(:a, :b)'
−
    "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>
+
$stmt->bindValue(
−
 
+
':a',
−
        <?php
+
5,
−
            $proc= sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
+
PDO::PARAM_INT
−
            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>
 
  
 +
$stmt->bindValue(
 +
':b',
 +
7,
 +
PDO::PARAM_INT
 +
);
  
 +
$stmt->execute();
  
 +
$resultado = $stmt->fetchColumn(); </syntaxhighlight>
  
 
<br />
 
<br />
−
=== Consulta chamando a funcións ===
 
  
−
* Dende Mysql pódense definir funcións, que son procedementos almacenados que devolven un valor dun tipo de datos.
+
=== Paxinación de resultados ===
  
−
: [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html Definicións de funcións dende Mysql].
+
* 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.
  
−
* Vexamos un exemplo:
+
* En MySQL podemos empregar `LIMIT` e `OFFSET`.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE FUNCTION `EXEMPLO_FUNCION`() RETURNS int
+
* Por exemplo:
−
BEGIN
 
−
    declare variable int;
 
  
−
SELECT col2
+
::<syntaxhighlight lang="sql">
−
INTO variable
+
SELECT id, titulo
−
FROM TABLA_1
+
FROM libros
−
        WHERE col1=1;
+
ORDER BY titulo
 +
LIMIT 2 OFFSET 0; </syntaxhighlight>
 +
 
 +
: Devolverá os dous primeiros rexistros.
  
−
return variable;
+
::<syntaxhighlight lang="sql">
 +
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT 2 OFFSET 2; </syntaxhighlight>
  
 +
: Devolverá os dous seguintes.
  
−
END
+
==== Pasos necesarios para realizar a paxinación ====
−
$$
 
−
</syntaxhighlight>
 
  
−
: Como vemos, esta función vai devolver un número de tipo integer.
+
* 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.
  
−
* Para chamar á función dende Mysql teremos que 'engadila' dentro dunha cláusula SELECT.
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('FUNCION_EXEMPLO_MYSQL','select EXEMPLO_FUNCION() as valor_devolto');
 
−
</syntaxhighlight>
 
  
−
: Como vemos, a tratamos coma dunha cláusula SELECT calquera, dándolle un nome (alias) ao resultado da execución da función.
+
<br />
 +
'''Número total de rexistros'''
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT COUNT(*)
 +
FROM libros'
 +
);
  
−
* 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.
+
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
  
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="19" >
 
  
−
<?php
+
<br />
 +
'''Número de rexistros por páxina'''
  
−
define('FUNCION_EXEMPLO_MYSQL','select EXEMPLO_FUNCION() as valor_devolto');
+
::<syntaxhighlight lang="php">
 +
define(
 +
'CONST_NUM_LIBROS_PAXINA',
 +
10
 +
); </syntaxhighlight>
  
−
$conex = new mysqli('SERVIDOR','USUARIO','PASSWORD','BD');
 
−
$conex->set_charset('utf8');
 
  
−
if ($conex->connect_error){
+
<br />
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
+
'''Obter e validar a páxina actual'''
−
}
 
  
−
?>
+
* O número de páxina chegará normalmente mediante `GET`.
  
 +
::<syntaxhighlight lang="php">
 +
$paxinaActual = filter_input(
 +
INPUT_GET,
 +
'paxina',
 +
FILTER_VALIDATE_INT
 +
);
  
−
        <?php
+
if (
−
            $result=$conex->query(FUNCION_EXEMPLO_MYSQL);
+
$paxinaActual === false ||
−
            if ($result || ($result->num_rows>0)){
+
$paxinaActual === null ||
−
                $row = $result->fetch_assoc();
+
$paxinaActual < 1
−
                printf("<h1>Dato devolto:%d</h1>",$row['valor_devolto']);
+
) {
−
                $result->free();
 
−
            }
 
−
            else {
 
−
                echo "Fallou a chamada á función: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
  
−
       
+
$paxinaActual = 1;
−
            $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.
+
} </syntaxhighlight>
  
 +
* `htmlspecialchars()` non serve para validar que un dato sexa numérico.
  
 +
: Neste caso empregamos `FILTER_VALIDATE_INT`.
  
  
 
<br />
 
<br />
−
==== Caso Práctico: Consulta chamando a funcións ====
+
'''Número total de páxinas'''
 +
 
 +
::<syntaxhighlight lang="php">
 +
$totalPaxinas = (int) ceil(
 +
$numLibros /
 +
CONST_NUM_LIBROS_PAXINA
 +
); </syntaxhighlight>
  
−
* Imos facer un exemplo aplicado ao noso proxecto.
+
* Podemos evitar que o usuario solicite unha páxina superior á existente:
−
: 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.
 
  
−
* '''Creamos a función en Mysql'''
+
::<syntaxhighlight lang="php">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
if (
−
DELIMITER $$
+
$totalPaxinas > 0 &&
 +
$paxinaActual > $totalPaxinas
 +
) {
 +
 
 +
$paxinaActual = $totalPaxinas;
  
−
CREATE FUNCTION `CONSULTAR_NUM_LIBROS`() RETURNS int READS SQL DATA
+
} </syntaxhighlight>
−
BEGIN
 
−
    declare num_libros int;
 
  
−
SELECT COUNT(*)
 
−
INTO num_libros
 
−
FROM libros;
 
  
−
return num_libros;
+
<br />
−
END$$
+
'''Calcular o OFFSET'''
−
</syntaxhighlight>  
 
  
 +
::<syntaxhighlight lang="php">
 +
$inicio = (
 +
$paxinaActual - 1
 +
) * CONST_NUM_LIBROS_PAXINA; </syntaxhighlight>
  
  
−
* '''Aqruivo bd.inc.php'''
 
−
: Definimos a constante que vai chamar á función.
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('FUNCION_CONSULTAR_NUM_LIBROS','select CONSULTAR_NUM_LIBROS() as num_libros');
 
−
</syntaxhighlight>
 
  
−
: Fixarse no alias da chamada á función.
+
<br />
 +
'''Consulta paxinada'''
  
 +
* Como `LIMIT` e `OFFSET` necesitan valores numéricos, indicaremos explicitamente o tipo mediante `PDO::PARAM_INT`.
 +
 +
::<syntaxhighlight lang="php">
 +
$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(); </syntaxhighlight>
  
−
* '''Creamos unha páxina consultar_num_libros_con_funcionMYSQL.php'''
 
−
::<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');
+
<br />
 +
'''Amosar os resultados'''
  
−
try {
+
::<syntaxhighlight lang="php">
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
+
if ($libros) {
−
    $conex->set_charset('utf8mb4');
 
  
−
$erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
echo "<table border='1'>";
−
?>
 
−
<!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>
+
foreach ($libros as $libro) {
  
−
        <?php
+
    echo '<tr>';
−
        $result=$conex->query(FUNCION_CONSULTAR_NUM_LIBROS);
+
 
−
        if ($result && ($result->num_rows>0)){
+
    printf(
−
            $row = $result->fetch_assoc();
+
        '<td>%s</td>',
−
            printf("<h1>N&uacute;mero de libros:%d</h1>",$row['num_libros']);
+
        htmlspecialchars(
−
             $result->free();
+
            $libro['titulo'],
−
        }
+
             ENT_QUOTES | ENT_SUBSTITUTE,
−
        else {
+
            'UTF-8'
−
            echo "Fallou a chamada á función.: (" . $conex->errno . ") " . $conex->error;
+
         )
−
         }
+
     );
−
        ?>       
+
 
−
       
+
     echo '</tr>';
−
       
 
−
    </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>
 
  
 +
echo '</table>';
  
 +
} else {
  
 +
echo 'Non hai datos que amosar.';
  
 +
} </syntaxhighlight>
  
−
<br />
 
−
=== Paxinación de resultados ===
 
  
−
* En moitas ocasións imos necesitar paxinar os resultados dunha consulta.
 
−
: Isto é necesario cando o resultado pode devolver unha cantidade moi grande de rexistros que obrigaría ao usuario a facer demasiado scroll.
 
  
 +
<br />
 +
'''Amosar os enlaces ás páxinas'''
  
−
* 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].
+
::<syntaxhighlight lang="php">
−
: Dita cláusula pode levar dous parámetros separados por coma: LIMIT numero_fila,tamaño e vai ao final da orde SELECT.
+
echo '<div>';
  
−
: Por exemplo:
+
for (
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4" >
+
$cont = 1;
−
SELECT id,titulo
+
$cont <= $totalPaxinas;
−
FROM libros
+
$cont++
−
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)  
+
if ($cont === $paxinaActual) {
  
−
: Se modifico a orde e poño:
+
    printf(
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4" >
+
        '<strong>%d</strong> ',
−
SELECT id,titulo
+
        $cont
−
FROM libros
+
    );
−
ORDER by titulo
 
−
LIMIT 2,2
 
−
</syntaxhighlight>
 
  
−
:: Devolverá os rexistros 2 e 3 do conxunto de resultados. Estamos paxinando os resultados de dous en dous !!!!
+
} else {
  
 +
    printf(
 +
        "<a href='?paxina=%d'>%d</a> ",
 +
        $cont,
 +
        $cont
 +
    );
 +
}
  
−
* Polo tanto para paxinar os datos dunha consulta terei que:
+
}
−
:* 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.
 
  
 +
echo '</div>'; </syntaxhighlight>
  
  
  
−
<br />
+
==== Exemplo completo de paxinación ====
−
==== Caso Práctico: Paxinación de resultados ====
 
  
  
−
* 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.
 
  
−
: 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).
+
'''Aquivo bd.php'''
 +
::<syntaxhighlight lang="php">
 +
define(
 +
    'CONST_NUM_LIBROS_PAXINA',
 +
    10
 +
);
 +
::</syntaxhighlight>
  
  
−
* Aplicado ao noso exemplo, imos engadir máis datos para poder facer unha paxinación.
+
'''Páxina php a cargar'''
 +
::<syntaxhighlight lang="php">
  
−
: '''Táboa autores'''
+
<?php
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
ini_set('display_errors', '1');
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('ABBOTT', 'RACHEL');
+
error_reporting(E_ALL);
−
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>
 
  
 +
require '/var/www/confBD/bd.php';
  
−
: '''Táboa libros'''
 
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
try {
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('COMO UNA EXTRAÑA');
+
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('CRÍMENES DUPLICADOS');
+
    $dsn = sprintf(
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('A SALVO CON SU CAPTOR');
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('MI ERROR FUE NO SER YO MISMA. PARTE II');
+
        SERVIDOR,
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('EL AMOR NO ES UN INVENTO DE LOS POETAS');
+
        BD
−
</syntaxhighlight>
+
    );
 +
 
 +
    $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();
  
−
: '''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>
 
  
 +
    /*
 +
    * Número total de páxinas.
 +
    */
 +
    $totalPaxinas = (int) ceil(
 +
        $numLibros /
 +
        CONST_NUM_LIBROS_PAXINA
 +
    );
  
−
: '''Táboa exemplares'''
 
−
:: Teredes que ter en conta os identificadores autonuméricos xerados nos insert´s da táboa libros.
 
−
::<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>
 
  
 +
    /*
 +
    * Páxina solicitada polo usuario.
 +
    */
 +
    $paxinaActual = filter_input(
 +
        INPUT_GET,
 +
        'paxina',
 +
        FILTER_VALIDATE_INT
 +
    );
  
−
* Creamos o procedemento almacenado en Mysql para que devolva os libros con paxinación.
+
    if (
−
: 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...
+
        $paxinaActual === false ||
 +
        $paxinaActual === null ||
 +
        $paxinaActual < 1
 +
    ) {
  
−
'''Procedemento almacenado CONSULTAR_LIBROS_PAXINACION en Mysql'''
+
        $paxinaActual = 1;
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
    }
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_LIBROS_PAXINACION`(IN p_num_rexistro int,IN p_tam_paxina INT)
 
−
BEGIN
 
−
SELECT id,titulo
 
−
FROM libros
 
−
ORDER BY titulo
 
−
LIMIT p_num_rexistro,p_tam_paxina;
 
  
−
END$$
+
    if (
−
</syntaxhighlight>
+
        $totalPaxinas > 0 &&
 +
        $paxinaActual > $totalPaxinas
 +
    ) {
  
 +
        $paxinaActual = $totalPaxinas;
 +
    }
  
  
−
* Creamos a constante que vai facer uso deste procedemento.
+
    /*
−
'''Arquivo: bd.inc.php'''
+
    * Primeiro rexistro da páxina.
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
    */
−
define('PROCEDEMENTO_CONSULTAR_LIBROS_PAXINACION','CONSULTAR_LIBROS_PAXINACION(%d,%d)');
+
    $inicio = (
−
</syntaxhighlight>
+
        $paxinaActual - 1
 +
    ) * CONST_NUM_LIBROS_PAXINA;
  
−
:: Como vemos imos enviar dous parámetros.
 
  
 +
    /*
 +
    * 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
 +
    );
  
−
* 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'''
+
    $stmt->execute();
−
: 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
 
  
 +
    $libros = $stmt->fetchAll();
  
−
: '''Arquivo: consultar_libros_con_paxinacion.php'''
+
} catch (PDOException $e) {
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="8,31-42,48-68,71-72" >
+
    error_log($e->getMessage());
−
<?php
+
    die('Erro relacionado coa base de datos.');
−
/* 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');
+
} catch (Throwable $e) {
−
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
 
  
 +
    die('Produciuse un erro na aplicación.');
  
−
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
 
  
 
?>
 
?>
  
−
<!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>
+
 
−
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
<head>
−
        <title>Operacións BD</title>
+
    <meta charset="UTF-8">
−
    </head>
+
    <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>
 +
</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 />
  
−
    <body>
+
=== Exercicio proposto ===
−
        <?php
 
−
                // 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'];
 
−
                    }
 
−
                }
 
−
            ?>
 
  
−
           
+
* Crea unha páxina chamada `buscar_libros.php` que permita buscar libros empregando os seguintes criterios:
−
            <?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>   
 
−
</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.
 
−
* 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.
 
−
* 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.
 
  
 +
:* '''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 />
−
===== Optimización =====
 
−
 
−
* O exercicio anterior poderíamos optimizalo para non ter que repetir a chamada a consultar o número de rexistros totais.
 
−
: 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.
 
−
 
−
 
−
 
−
* 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.
 
−
* <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.
 
−
 
−
 
  
−
* Posible solución enviando o número total de libros na URL:
+
== Resumo das funcións PDO empregadas ==
  
−
'''Arquivo consultar_libros_con_paxinacion.php'''
+
::<syntaxhighlight lang="php">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="9-11,47" >
+
! 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>
  
 +
== Regras que empregaremos nos exames ==
  
−
            <?php
+
# As operacións contra a base de datos realizaranse mediante '''PDO'''.
−
                // 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>";
 
−
            ?>
 
  
−
</syntaxhighlight>
+
# 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.
  
−
<br />
+
# Os datos recibidos mediante formularios ou URL deberán validarse.
−
* 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'''
+
# Cando o tipo sexa relevante empregarase `bindValue()` indicando `PDO::PARAM_INT`, `PDO::PARAM_STR`, etc.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="1-4,9,21,29" >
 
−
<?php
 
−
  session_start();  // Iría ao principio de todo
 
  
−
?>
+
# Non se empregará `rowCount()` para coñecer o número de filas devoltas por un `SELECT`.
−
.......................
 
−
 
 
−
 
 
−
            <?php
 
−
                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>
 
  
 +
# Se necesitamos contar filas empregaremos `COUNT(*)`.
  
 +
# Para amosar cadeas dentro de HTML empregarase `htmlspecialchars()`.
  
 +
# Os erros PDO capturaranse mediante `PDOException`.
  
 +
# Os detalles técnicos dos erros rexistraranse mediante `error_log()` e non se amosarán ao usuario final.
  
 +
# Os demais erros ou excepcións poderán capturarse mediante `Throwable`.
  
 +
# Os exemplos realizaranse tomando como referencia '''PHP 8.5'''.
  
 
<br />
 
<br />
−
=== 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:
 
−
:* 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).
 
−
 
−
* O usuario vai poder escoller calquera das opcións combinándoas como prefira.
 
−
* 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
 
−
* 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.
 
−
: Podedes consultar como facelo [https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html neste enlace].
 
−
 
  
  
Línea 3271: 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).