Diferencia entre revisiones de «PHP UD3 OP CONSULTA»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 52 ediciones intermedias del mismo usuario)
Línea 3: Línea 3:
  
 
* '''<u>Requerimentos previos:</u>''' Ter creada a base de datos indicada [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Creando_as_t.C3.A1boas_en_Mysql nunha sección anterior] e ter gardados os parámetros de conexión nun arquivo separado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro como foi indicado nunha sección anterior]
 
* '''<u>Requerimentos previos:</u>''' Ter creada a base de datos indicada [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Creando_as_t.C3.A1boas_en_Mysql nunha sección anterior] e ter gardados os parámetros de conexión nun arquivo separado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro como foi indicado nunha sección anterior]
 +
 +
 +
'''IMPORTANTE:''' Nos exames, exercicios e prácticas avaliables empregarase '''PDO''' para realizar as operacións contra a base de datos, salvo que no enunciado se indique expresamente o contrario.
 +
Os exemplos con mysqli poden manterse nos apuntes a nivel informativo, pero os exemplos de referencia serán os realizados con PDO.
  
  
  
 
<br />
 
<br />
−
* Para conectarnos primeiro temos que ter a consulta SQL.  
+
* O proceso para realizar unha consulta é o seguinte:
−
* Indicar que o [https://es.wikipedia.org/wiki/SQL SQL (Structure Query Language)] é a linguaxe utilizada para obter datos dunha base de datos relacional.
+
:* '''Paso 1: Abrimos a conexión á base de datos''': [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Apertura_da_conexi.C3.B3n Isto xa o vimos nunha sección anterior]
 +
 
 +
 
 +
* Necesitamos ter unha consulta SQL para recuperar datos.  
 +
: Indicar que o [https://es.wikipedia.org/wiki/SQL SQL (Structure Query Language)] é a linguaxe utilizada para obter datos dunha base de datos relacional.
 +
: Indicar que estos contidos xa deben estar aprendidos do módulo de Base de datos e saber facer joins entre táboas, subconsultas, agrupacións,...
 +
 
  
 +
* '''CONSIDERACIÓNS:'''
 +
:* Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos)
 +
:* Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro vimos nun punto anterior desta sección].
 +
:* Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas.
 +
:* En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes.
 +
:* Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...).
 +
:* Lembrar que tamén podedes activar o debugger como está indicado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Habilitar_depuraci%C3%B3n_en_VS_Code nesta sección da wiki].
 +
 +
 +
 +
<br />
 +
 +
=== Realización dunha consulta SELECT ===
 +
 +
* '''Paso 2: Chamar ao procedemento query enviando como parámetro a consulta a realizar e a conexión aberta previamente.'''
 
: Consulta SQL: '''SELECT col1, col2 FROM TABOA_1 ORDER BY col'''
 
: Consulta SQL: '''SELECT col1, col2 FROM TABOA_1 ORDER BY col'''
  
 +
* Se a consulta SQL '''non leva datos externos''' podemos empregar directamente o método query():
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
ORDER BY col1'
 +
);
 +
</syntaxhighlight>
 +
: query() devolve un obxecto da clase PDOStatement.
  
−
* Funcións para realizar consultas a base de datos e funcións relacionadas:
+
 
 +
* Se a consulta leva datos procedentes do usuario ou valores que queremos enviar mediante parámetros empregaremos:
 +
::<syntaxhighlight lang="php">
 +
prepare()
 +
execute()
 +
</syntaxhighlight>
 +
: Por exemplo:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor'
 +
);
 +
$stmt->execute([
 +
'valor' => 5
 +
]);
 +
</syntaxhighlight>
 +
 
 +
 
 +
 
 +
 
 +
<br />
 +
* '''Version procedimental'''
 +
: Funcións para realizar consultas a base de datos e funcións relacionadas:
  
 
:* [http://php.net/manual/es/mysqli.query.php mysqli::query]: Realiza a consulta
 
:* [http://php.net/manual/es/mysqli.query.php mysqli::query]: Realiza a consulta
Línea 29: Línea 86:
  
  
 +
::* Versión procedimental:
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
$result=mysqli_query($conex, 'SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
 +
</syntaxhighlight>
 +
::<u>Nota:</u> $conex é a conexión aberta feita no paso anterior.
  
 +
::* Versión orientada a obxectos e PDO:
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
$result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1');
 +
</syntaxhighlight>
  
 +
::* $result vai ter o conxunto de filas que devolva o resultado. Podemos velo como unha táboa formada polas filas do resultado e polas columnas indicadas no select.
  
  
  
 
<br />
 
<br />
−
* O proceso para realizar unha consulta é o seguinte:
 
−
:* '''Paso 1: Abrimos a conexión á base de datos''': [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Apertura_da_conexi.C3.B3n Isto xa o vimos nunha sección anterior]
 
  
 +
=== Comprobar se unha consulta SELECT devolveu datos ===
  
 +
* '''Non debemos empregar rowCount() para comprobar o número de filas devoltas por un SELECT'''.
 +
: rowCount() está pensado principalmente para coñecer o número de filas afectadas por operacións INSERT, UPDATE ou DELETE.
 +
: Nun SELECT o seu comportamento depende do controlador da base de datos e non está garantido.
  
  
 +
* Unha forma sinxela de comprobar se hai datos consiste en realizar un primeiro fetch():
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
ORDER BY col1'
 +
);
 +
$fila = $stmt->fetch();
 +
if ($fila !== false) {
 +
do {
  
−
<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" 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" 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 ' - ';
 +
 +
    echo htmlspecialchars(
 +
        $fila['col2'],
 +
        ENT_QUOTES | ENT_SUBSTITUTE,
 +
        'UTF-8'
 +
    );
 +
 +
    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>
  
−
::* 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="java" 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->set_charset('utf8mb4');
+
 
 +
<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->set_charset('utf8mb4');
  
 
?>
 
?>
Línea 118: Línea 249:
  
  
 +
<br />
 +
::* Como vemos, en caso de erro comprobamos se o xestor devolveu un código de erro, indicativo de que a sentenza SQL está mal feita. A información do código de erro non debe amosarse a un usuario final. Normalmente, se pon o echo de 'Non hai datos que amosar' e se estamos a desenvolver a aplicación e non devolve datos, faríamos un echo temporal do código de erro do xestor ou un echo da sentenza SQL, podendo executala no xestor Mysql para ver o erro.
  
−
<br />
 
−
:::* Versión PDO:
 
−
::::<syntaxhighlight lang="java" 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" 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" enclose="div" highlight="1,5-16" >
 
−
if ($result && ($result->num_rows>0)){
 
−
  // Veñen datos
 
  
 +
: O primeiro fetch() xa consume unha fila e o segundo bucle comezaría na seguinte.
 +
: A forma correcta sería:
 +
::<syntaxhighlight lang="php">
 +
while ($row = $stmt->fetch()) {
 +
// Procesamos a fila
 
}
 
}
−
else {
+
</syntaxhighlight>
−
    if($conex->errno){
 
−
      echo "Houbo un erro na chamada ao xestor...";
 
−
    // O seguinte non tería que amosarse ao usuario final da aplicación. Posto para desenvolvemento.
 
−
      echo "<div>";
 
−
      printf("Erro no Mysql. Número %d. Mensaxe: %s",  $conex->errno,  $conex->error);
 
−
      echo "<div>";
 
−
    }
 
−
    else{
 
−
      echo "Non hai datos que amosar!!!!";
 
−
    }
 
−
}
 
−
</syntaxhighlight>  
 
−
 
 
  
  
Línea 225: Línea 310:
  
 
<br />
 
<br />
−
::* Como vemos, en caso de erro comprobamos se o xestor devolveu un código de erro, indicativo de que a sentenza SQL está mal feita. A información do código de erro non debe amosarse a un usuario final. Normalmente, se pon o echo de 'Non hai datos que amosar' e se estamos a desenvolver a aplicación e non devolve datos, faríamos un echo temporal do código de erro do xestor ou un echo da sentenza SQL, podendo executala no xestor Mysql para ver o erro.
+
:* Versión procedimental
−
 
 
−
 
 
−
 
 
−
 
 
  
−
<br />
 
−
:* '''Paso 4: Agora necesitaremos percorrer dende o primeiro rexistro ata o último amosando o resultado.'''
 
−
::: Isto o faremos cun bucle, chamando á [http://php.net/manual/es/mysqli-result.fetch-assoc.php función mysqli_result::fetch_assoc(void)].
 
 
::: Debemos entender que, inicialmente, despois de facer a consulta, temos un punteiro que está 'apuntando' xusto antes do primeiro rexistro de resultados. O que fai a función anterior é mover ese punteiro ao seguinte rexistro (a primeira vez apuntará ao primeiro do conxunto de resultados) e devolve nun array a fila á que está 'apuntando'.  
 
::: Debemos entender que, inicialmente, despois de facer a consulta, temos un punteiro que está 'apuntando' xusto antes do primeiro rexistro de resultados. O que fai a función anterior é mover ese punteiro ao seguinte rexistro (a primeira vez apuntará ao primeiro do conxunto de resultados) e devolve nun array a fila á que está 'apuntando'.  
 
::: Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false.
 
::: Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false.
  
  
 +
::: Isto o faremos cun bucle, chamando á [http://php.net/manual/es/mysqli-result.fetch-assoc.php función mysqli_result::fetch_assoc(void)].
 
::* Versión procedimental:
 
::* Versión procedimental:
−
::::<syntaxhighlight lang="java" 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" 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" 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" 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>
 +
 
 +
=== Parámetros de tipo cadea ===
 +
Para enviar unha cadea empregaremos tamén unha sentenza preparada:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT id, apelidos, nome
 +
FROM autores
 +
WHERE apelidos = :apelidos'
 +
);
 +
$stmt->execute([
 +
'apelidos' => $apelidos
 +
]);
 +
</syntaxhighlight>
 +
 
 +
 
 +
* Se queremos facer unha busca mediante LIKE, o marcador debe representar o '''valor completo''':
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT id, apelidos, nome
 +
FROM autores
 +
WHERE apelidos LIKE :apelidos'
 +
);
 +
$stmt->execute([
 +
'apelidos' => '%' . $apelidos . '%'
 +
]);
 +
</syntaxhighlight>
  
−
</syntaxhighlight>
 
  
−
::: Se temos poucas filas podemos obter todas as filas de vez empregando [https://www.php.net/manual/es/pdostatement.fetchall.php fetchall()].
+
* Non debemos escribir:
 +
::<syntaxhighlight lang="sql">
 +
WHERE apelidos LIKE '%:apelidos%'
 +
</syntaxhighlight>
 +
: xa que o marcador debe ocupar o lugar dun valor completo (PHP/PDO interpretará :apelidos como texto literal dentro das comiñas e non o substituirá pola variable).
  
 +
=== 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
 +
);
  
−
<br />
+
$stmt->bindValue(
−
:* '''Paso 5: Ao rematar de percorrer o resultado, liberaremos a memoria chamando á [http://php.net/manual/es/mysqli-result.free.php función free()]'''
+
    ':libro_id',
−
 
+
     $libroId,
−
::* Versión procedimental:
+
    PDO::PARAM_INT
−
:::<syntaxhighlight lang="java" enclose="div" highlight="6" >
+
);
−
if ($result && (mysqli_num_rows($result)>0)){
 
−
  // Veñen datos
 
−
  while($row = mysqli_fetch_assoc($result)) {
 
−
     printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
 
−
  }
 
−
  mysqli_free_result($result); // Liberamos da memoria os recursos
 
−
}
 
−
else {
 
−
  echo 'Non hai datos ou a consulta está mal feita';
 
−
}
 
−
</syntaxhighlight>
 
  
 +
$stmt->execute();
  
−
::* Versión orientada a obxectos con fetch_assoc:
+
// Procesamos os resultados
−
:::<syntaxhighlight lang="java" enclose="div" highlight="6" >
 
−
if ($result && ($result->num_rows>0)){
 
−
  // Veñen datos
 
−
  while($row = $result->fetch_assoc()) {
 
−
    printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
 
−
  }
 
−
  $result->free();  // Liberamos da memoria os recursos
 
 
}
 
}
−
else {
+
</syntaxhighlight>
−
  echo 'Non hai datos ou a consulta está mal feita';
+
=== Caso práctico: consultar exemplares dun libro ===
−
}
+
'''Arquivo bd.php'''
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php">
  
 +
define('CARTAFOL_IMAXES_WEB', 'http://localhost/imaxes/');
  
−
::* Versión PDO:
+
define(
−
:::<syntaxhighlight lang="java" enclose="div" highlight="7" >
+
'CONSULTAR_EXEMPLARES_POR_LIBRO',
−
// Comprobar se hai filas
+
'SELECT libro_id,
−
if ($result && $result->rowCount() > 0) {
+
id_exemplar,
−
  // Procesamos os datos
+
imaxe,
−
  while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) { // tamén pode ser PDO::FETCH_OBJ
+
prezo,
−
        echo htmlentities($fila['col1']) . ' - ' . htmlentities($fila['col2']) . "<br>";
+
estado_id
−
  }
+
FROM exemplares
−
  $result = null;
+
WHERE libro_id = :libro_id
−
} else {
+
ORDER BY estado_id'
−
  echo "Non hai datos que amosar!!!!";
+
);
−
}
+
</syntaxhighlight>
  
  
−
</syntaxhighlight>
 
  
 +
<br />
 +
* '''Código PHP'''
 +
::<syntaxhighlight lang="php">
 +
<html>
  
 +
<head>Páxina de exemplo
 +
</head>
  
 +
<body>
  
−
<br />
+
    <?php
−
:* '''Paso 6: Pechamos a conexión.'''.
+
    ini_set('display_errors', 1);
 +
    ini_set('display_startup_errors', 1);
 +
    error_reporting(E_ALL);
  
−
:: [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.]
+
    require('/var/www/confBD/bd.php');
 +
    try {
  
−
=== Caso Práctico 1 ===
+
        $dsn = sprintf(
−
: Imos crear unha consulta que nos devolva todos os libros da táboa LIBROS dentro dunha táboa:
+
            'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
: 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.
+
            SERVIDOR,
 +
            BD
 +
        );
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
        $opciones = [
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
+
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
</syntaxhighlight>
+
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_EMULATE_PREPARES  => false
 +
        ];
  
 +
        $conex = new PDO(
 +
            $dsn,
 +
            USUARIO,
 +
            PASSWORD,
 +
            $opciones
 +
        );
  
−
* '''Normas nos nomes das constantes''':
+
        $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...
−
:* Comenzarán cun verbo que vai poder ser: CONSULTAR, INSERIR, MODIFICAR e ELIMINAR
+
        $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)
−
:* Seguirá cun guión baixo.
 
−
:* En caso de consultar os datos dunha única táboa se indicará o nome da táboa. En caso de consultar datos de varias táboas se indicará con varias palabras separadas con guión baixo que indiquen o obxectivo da consulta.
 
  
 +
        if (
 +
            $libroId !== false &&
 +
            $libroId !== null &&
 +
            $libroId !== -1
 +
        ) {
 +
            $stmt = $conex->prepare(
 +
                CONSULTAR_EXEMPLARES_POR_LIBRO
 +
            );
  
 +
            $stmt->bindValue(
 +
                ':libro_id',
 +
                $libroId,
 +
                PDO::PARAM_INT
 +
            );
  
 +
            $stmt->execute();
  
−
* Aplicado ao exemplo que estamos a seguir...
+
            $fila = $stmt->fetch();
−
:<u>Nota</u> Lembrar activar as directivas para amosar erros e quitalas cando rematedes a páxina.
 
  
−
==== Solución Caso Práctico 1 Orientado a Obxectos====
+
            if ($fila !== false) {
  
−
'''Arquivo: consultar_libros.php'''
+
                echo "<table border='1'>";
−
::<syntaxhighlight lang="java" enclose="div" highlight="27-40" >
+
                echo '<tr>';
−
<?php
+
                echo '<th>Num. exemplar</th>';
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
                echo '<th>Imaxe</th>';
−
ini_set('display_errors', 1);
+
                echo '<th>Prezo</th>';
−
ini_set('display_startup_errors', 1);
+
                echo '<th>Estado</th>';
−
error_reporting(E_ALL);
+
                echo '</tr>';
 +
 
 +
                do {
 +
 
 +
                    echo '<tr>';
 +
 
 +
                    printf(
 +
                        '<td>%d</td>',
 +
                        (int) $fila['id_exemplar']
 +
                    );
  
−
require ('/var/www/confBD/bd.php');
+
                    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'
 +
                        )
 +
                    );
  
−
try {
+
                    printf(
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD);  // Non indicamos o nome da bd
+
                        '<td>%.2f</td>',
−
    $conex->set_charset('utf8mb4');
+
                        (float) ($fila['prezo'] ?? 0)
−
    $conex->select_db(BD); // O parámetro indica o nome da base de datos
+
                    );
  
−
?>
+
                    printf(
 +
                        '<td>%d</td>',
 +
                        (int) $fila['estado_id']
 +
                    );
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
                    echo '</tr>';
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
                } while ($fila = $stmt->fetch());
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
                echo '</table>';
−
    <?php       
+
             } else {
−
       
+
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
                 echo 'Non hai datos que amosar';
−
        if ($result && $result->num_rows>0){
 
−
            echo "<table border='1'>";
 
−
             while($row = $result->fetch_assoc()) {
 
−
                echo '<tr>';
 
−
                printf("<td>%s</td>",htmlentities($row["titulo"]));
 
−
                 echo '</tr>';
 
 
             }
 
             }
−
            echo '</table>';
 
−
            $result->free();  // Liberamos da memoria os recursos
 
 
         }
 
         }
−
         else {
+
    } catch (PDOException $e) {
−
            echo "Non hai datos que amosar!!!!";
+
         die("Error PDO: " . $e->getMessage());
−
        }
+
    } catch (Throwable $e) {
−
       
+
        die("Error general: " . $e->getMessage());
−
     ?>      
+
    }
−
    </body>  
+
 
 +
    $conex = null;  // Poderíamos non poñer nada
 +
     ?>
 +
</body>
 +
 
 
</html>
 
</html>
 +
</syntaxhighlight>
  
−
<?php
+
== Procedementos almacenados empregando PDO ==
−
}    // Pecha o try do principio da páxina
+
Os procedementos almacenados execútanse mediante a sentenza SQL CALL.
−
catch (mysqli_sql_exception $e) {
+
: Por exemplo:
−
     die("Erro mysql: " . $e->getMessage());
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_LIBROS()'
 +
);
 +
$stmt->execute();
 +
</syntaxhighlight>
 +
Se o procedemento devolve un conxunto de resultados podemos percorrelo da forma habitual:
 +
::<syntaxhighlight lang="php">
 +
while ($fila = $stmt->fetch()) {
 +
echo htmlspecialchars(
 +
     $fila['titulo'],
 +
    ENT_QUOTES | ENT_SUBSTITUTE,
 +
    'UTF-8'
 +
);
 
}
 
}
−
catch (Expcetion $e) {
+
</syntaxhighlight>
−
    die("Erro xeral: " . $e->getMessage());
+
=== Procedementos con varios conxuntos de resultados ===
−
}
+
Un procedemento almacenado pode devolver máis dun conxunto de resultados.
−
finally{
+
Para pasar ao seguinte conxunto empregaremos:
−
  if(!empty($conex)){
+
::<syntaxhighlight lang="php">
−
      $conex->close();
+
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>
 
</syntaxhighlight>
  
  
−
* Resultado:
+
+ Antes de executar outra consulta na mesma conexión debemos asegurarnos de que non queden resultados pendentes.
−
[[Imagen:Php_BD_8.jpg|300px|center]]
+
: Podemos liberar os resultados restantes mediante:
 +
::<syntaxhighlight lang="php">
 +
$stmt->closeCursor();
 +
</syntaxhighlight>
 +
: Isto é especialmente importante despois de chamadas a procedementos almacenados en MySQL.
  
  
−
* 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.
+
: Por exemplo:
 +
::<syntaxhighlight lang="php">
 +
<?php
 +
// 1. Executamos o procedemento almacenado que devolve varios SELECTs
 +
$stmt = $conex->prepare('CALL PROCEDEMENTO_CON_VARIOS_SELECT()');
 +
$stmt->execute();
  
 +
// 2. Procesamos só o primeiro conxunto de resultados (por exemplo, os primeiros 3 datos)
 +
if ($fila = $stmt->fetch()) {
 +
    echo "Primeiro rexistro: " . $fila['nome'] . "<br>";
 +
}
  
 +
// 3. Pechamos o cursor para liberar a conexión
 +
// Isto descarta calquera outra fila ou segundo resultset pendente
 +
$stmt->closeCursor();
  
−
* O método fetch_assoc() move un punteiro interno á seguinte fila de resultados.
+
// 4. Agora xa podemos executar outra consulta na mesma conexión sen erros
−
: 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]·
+
$outraConsulta = $conex->query('SELECT * FROM outra_taboa');
 +
$datos = $outraConsulta->fetchAll();
 +
?>
  
 +
</syntaxhighlight>
  
 +
=== Procedementos con parámetros de entrada ===
 +
Os parámetros de entrada enviaranse empregando marcadores:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL PROCEDEMENTO_CON_PARAMETROS(
 +
:dato1,:dato2
  
 +
)'
 +
);
 +
$stmt->bindValue(
 +
':dato1',
 +
$valorNum,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->bindValue(
 +
':dato2',
 +
$valorTexto,
 +
PDO::PARAM_STR
 +
);
 +
$stmt->execute();
 +
</syntaxhighlight>
 +
: Desta forma non necesitamos construír a chamada mediante sprintf() nin concatenar os valores dentro da sentenza SQL.
  
 +
=== Procedementos con parámetros de saída en MySQL ===
 +
PDO define mecanismos xenéricos para parámetros de saída, pero o controlador '''PDO_MYSQL non xestiona correctamente PDO::PARAM_INPUT_OUTPUT'''.
 +
Por este motivo, con MySQL podemos empregar unha variable de usuario de MySQL para recuperar un parámetro OUT.
 +
Supoñamos este procedemento:
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
 +
CREATE PROCEDURE CONSULTAR_NUM_LIBROS(
 +
OUT p_num_libros INT
 +
)
 +
BEGIN
 +
SELECT COUNT(*)
 +
INTO p_num_libros
 +
FROM libros;
 +
END$$
 +
DELIMITER ;
 +
</syntaxhighlight>
 +
Desde PHP podemos facer:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_NUM_LIBROS(
 +
@numero_libros
 +
)'
 +
);
 +
$stmt->execute();
 +
/*
 +
Liberamos todos os resultados da chamada
 +
antes de executar outra sentenza SQL.
 +
*/
 +
$stmt->closeCursor();
 +
$stmt = $conex->query(
 +
'SELECT @numero_libros'
 +
);
 +
$numLibros = $stmt->fetchColumn();
 +
printf(
 +
'<h1>Número de libros: %d</h1>',
 +
(int) $numLibros
 +
);
 +
</syntaxhighlight>
 +
: É importante chamar a closeCursor() antes do segundo SELECT, xa que MySQL pode manter conxuntos de resultados pendentes da chamada ao procedemento.
 +
== Funcións almacenadas de MySQL ==
 +
As funcións de MySQL poden empregarse dentro dunha instrución SELECT.
 +
Se a función non leva parámetros podemos facer:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT CONSULTAR_NUM_LIBROS()'
 +
);
 +
$numLibros = $stmt->fetchColumn();
 +
</syntaxhighlight>
 +
Se leva parámetros empregaremos unha sentenza preparada:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT Suma(:dato1,:dato2 )'
 +
);
 +
$stmt->bindValue(
 +
':dato1',
 +
5,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->bindValue(
 +
'':dato2,
 +
7,
 +
PDO::PARAM_INT
 +
);
 +
$stmt->execute();
 +
$resultado = $stmt->fetchColumn();
 +
</syntaxhighlight>
 +
: fetchColumn() resulta especialmente cómodo cando a consulta devolve un único valor.
 +
 +
== Paxinación de resultados empregando PDO ==
 +
Para paxinar resultados empregaremos:
 +
:* COUNT(*) para coñecer o número total de rexistros.
 +
:* LIMIT para indicar a cantidade de rexistros que queremos recuperar.
 +
:* OFFSET, ou o primeiro parámetro de LIMIT, para indicar desde que rexistro queremos comezar.
 +
Por exemplo:
 +
::<syntaxhighlight lang="sql">
 +
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT 10 OFFSET 20
 +
</syntaxhighlight>
 +
: Esta consulta devolvería ata 10 rexistros empezando desde o rexistro situado na posición 20.
  
−
<br />
 
−
==== Solución Caso Práctico 1 empregando PDO====
 
  
−
'''Arquivo: consultar_libros.php'''
 
−
::<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');
+
<br />
 +
'''Número total de rexistros'''
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->query(
 +
'SELECT COUNT(*)
 +
FROM libros'
 +
);
 +
$numLibros = (int) $stmt->fetchColumn();
 +
</syntaxhighlight>
  
−
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);
 
  
−
?>
 
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
<br />
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
'''Páxina actual'''
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
O número de páxina recibido mediante GET é un dato externo e debe validarse.
−
    <head>
+
::<syntaxhighlight lang="php">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
$paxinaActual = filter_input(
−
        <title>Operacións BD</title>
+
INPUT_GET,
−
    </head>
+
'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.
  
−
    <body>
 
−
    <?php       
 
−
       
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
 
−
        // Comprobar se hai filas
 
−
        if ($result && $result->rowCount() > 0) {
 
−
            echo "<table border='1'>";
 
−
            while ($fila = $result->fetch(PDO::FETCH_ASSOC)) {
 
−
                echo '<tr>';
 
−
                printf("<td>%s</td>",htmlentities($fila["titulo"]));
 
−
                echo '</tr>';
 
−
            }
 
−
            echo '</table>';
 
−
            $result = null; 
 
−
        } else {
 
−
          echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
     
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
<br />
−
}    // Pecha o try do principio da páxina
+
'''Cálculo do número de páxinas'''
−
catch (PDOException $e) {
+
::<syntaxhighlight lang="php">
−
  die("Erro PDO: " . $e->getMessage());
+
define(
−
} finally {
+
'CONST_NUM_LIBROS_PAXINA',
−
  $conex = null; // Pechar a conexión
+
10
 +
);
 +
$totalPaxinas = (int) ceil(
 +
$numLibros / CONST_NUM_LIBROS_PAXINA
 +
);
 +
</syntaxhighlight>
 +
Podemos evitar que se solicite unha páxina superior á existente:
 +
::<syntaxhighlight lang="php">
 +
if (
 +
$totalPaxinas > 0 &&
 +
$paxinaActual > $totalPaxinas
 +
) {
 +
$paxinaActual = $totalPaxinas;
 
}
 
}
 +
</syntaxhighlight>
  
−
?>
 
  
 +
<br />
 +
'''Calcular o desprazamento'''
 +
::<syntaxhighlight lang="php">
 +
$inicio = (
 +
$paxinaActual - 1
 +
) * CONST_NUM_LIBROS_PAXINA;
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
: Por exemplo, se estamos:
 +
:* Na páxina 1 => inicio = 0
 +
:* Na páxina 2 => inicio = 10
 +
:* Na páxina 3 => inicio = 20
  
−
* Resultado:
 
−
[[Imagen:Php_BD_8.jpg|300px|center]]
 
  
 +
<br />
 +
'''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>
 +
 +
 +
* Despois podemos amosar os libros:
 +
::<syntaxhighlight lang="php">
 +
if ($libros) {
 +
echo "<table border='1'>";
 +
 +
foreach ($libros as $libro) {
  
−
* 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 '<tr>';
  
 +
    printf(
 +
        '<td>%s</td>',
 +
        htmlspecialchars(
 +
            $libro['titulo'],
 +
            ENT_QUOTES | ENT_SUBSTITUTE,
 +
            'UTF-8'
 +
        )
 +
    );
  
−
<br />
+
    echo '</tr>';
 +
}
  
−
=== Caso Práctico 2 ===
+
echo '</table>';
 +
} else {
 +
echo 'Non hai datos que amosar';
 +
}
 +
</syntaxhighlight>
  
−
* Agora imos facer unha consulta á táboa AUTORES recuperando todos os autores ordenados por apelidos e os amosaremos en forma de lista desordenada no que cada elemento da lista será enlace có seguinte formato: <enlace href='detalle_autor.php?id=XXX'>Apelidos , nome </enlace>
 
−
: Sendo XXX a columna id_autor.
 
  
−
: <u>Nota:</u> Se queremos facer máis seguro o paso de información entre páxinas, poderíamos empregar un formulario e que cada autor tivera un button con un value igual á clave primaria. De tal forma que ao premer nel, o formulario envaría á seguinte páxina o id do autor premido e dita información non ser vería na URL. Por exemplo: <button type="submit" name="enviar" value="valor_a_enviar">Texto a amosar</button>
 
  
  
 +
<br />
 +
'''Enlaces da paxinación'''
 +
::<syntaxhighlight lang="php">
 +
echo '<div>';
 +
for (
 +
$cont = 1;
 +
$cont <= $totalPaxinas;
 +
$cont++
 +
) {
 +
if ($cont === $paxinaActual) {
  
 +
    printf(
 +
        '<strong>%d</strong> ',
 +
        $cont
 +
    );
  
−
<br />
+
} else {
−
==== Solución Caso Práctico 2 orientado a obxectos====
 
  
−
: No '''arquivo bd.php''' creado no punto anterior.
+
    printf(
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
        "<a href='?paxina=%d'>%d</a> ",
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
+
        $cont,
−
</syntaxhighlight>  
+
        $cont
 +
    );
 +
}
 +
}
 +
echo '</div>';
 +
</syntaxhighlight>
  
  
  
 
<br />
 
<br />
−
'''Páxina consultar_autores.php'''
+
'''Sobre a optimización do número total de rexistros'''
−
:<syntaxhighlight lang="java" enclose="div" highlight="26-40" >
+
En xeral non deberiamos enviar o número total de rexistros na URL confiando despois nese valor:
−
<?php
+
::<syntaxhighlight lang="text">
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
?paxina=2&numtotal=50
−
ini_set('display_errors', 1);
+
</syntaxhighlight>
−
ini_set('display_startup_errors', 1);
+
: Os datos enviados mediante GET poden ser modificados polo usuario.
−
error_reporting(E_ALL);
+
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>
 +
: 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 ==
  
−
require ('/var/www/confBD/bd.php');
 
  
 +
* Nos exemplos completos empregaremos o seguinte esquema:
 +
::<syntaxhighlight lang="php">
 
try {
 
try {
−
     $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);    // Indico nome da bd
+
     $conex = new PDO($dsn, $usuario, $contrasinal, [
−
     $conex->set_charset('utf8mb4');
+
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
     ]);
  
−
?>
+
    // Operacións coa base de datos...
 +
 
 +
} catch (PDOException $e) {
 +
    // Rexistrar o erro nun log (monolog, etc.)
 +
    error_log($e->getMessage());
 +
   
 +
    die('Erro relacionado coa base de datos.');
 +
} catch (Throwable $e) {
 +
    die('Produciuse un erro na aplicación.');
 +
}
 +
 
 +
// Non fai falta "finally {$conex = null;}":
 +
// Se o script continúa, destruirasie cando cumpra o seu ciclo;
 +
// se entra nos catch, o die() xa finaliza o proceso e libera a memoria.
 +
</syntaxhighlight>
 +
: PDOException permite tratar especificamente os erros de PDO.
 +
: Throwable permite capturar outros erros ou excepcións producidos durante a execución.
 +
: Os bloques catch deben colocarse desde o máis específico ao máis xeral.
  
−
<!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(CONSULTAR_AUTORES);
 
−
        if ($result && $result->num_rows>0){
 
−
            // Veñen datos
 
−
            echo "<ul>";
 
−
            while($row = mysqli_fetch_assoc($result)) {
 
−
                echo "<li>";
 
−
                printf("<a href='detalle_autor.php?id=%s'>%s</a>",$row["id"],urlencode($row["apelidos"]) .', '. urlencode($row['nome']));
 
−
                echo "</li>";
 
−
            }
 
−
            echo "</ul>";
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
       
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
<br />
−
}
+
== Exercicios prácticos ==
−
catch (mysqli_sql_exception $e) {
+
 
−
    die("Erro mysql: " . $e->getMessage());
+
* 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`.
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
* '''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:
  
−
<br />
+
: No arquivo bd.php que se atopa fora da ruta /var/www/html gardaremos os datos da conexión e as consultar a empregar:
−
==== Solución Caso Práctico 2 empregando PDO====
+
::<syntaxhighlight lang="php">
 +
<?php
  
−
: No '''arquivo bd.php''' creado no punto anterior.
+
define('SERVIDOR', 'localhost');
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
define('USUARIO', 'user_php');
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
+
define('PASSWORD', 'user_php');
−
</syntaxhighlight>
+
define('BD', 'php');
  
 +
define(
 +
    'NOME_DA_CONSULTA',
 +
    'SELECT col1,col2,...
 +
    FROM taboa
 +
    ORDER BY col1'
 +
);
 +
</syntaxhighlight>
  
  
−
<br />
+
Nas páxinas PHP que van acceder á base de datos terán este esquema:
−
'''Páxina consultar_autores.php'''
+
::<syntaxhighlight lang="php">
−
:<syntaxhighlight lang="java" enclose="div" highlight="27-41" >
+
try {
−
<?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');
+
require '/var/www/confBD/bd.php';
  
 
try {
 
try {
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",SERVIDOR,BD);
 
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
?>
+
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
    $conex = new PDO(
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
        $dsn,
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
        USUARIO,
−
    <head>
+
        PASSWORD,
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
        [
−
         <title>Operacións BD</title>
+
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
     </head>
+
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
         ]
 +
     );
  
−
     <body>
+
     // Operacións coa base de datos...
−
    <?php       
 
−
       
 
−
        $result=$conex->query(CONSULTAR_AUTORES);
 
−
        // 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; 
 
−
        } else {
 
−
          echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
     
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
} catch (PDOException $e) {
−
}   // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
  die("Erro PDO: " . $e->getMessage());
 
−
} finally {
 
−
  $conex = null; // Pechar a conexión
 
−
}
 
  
−
?>
 
−
</syntaxhighlight>
 
  
 +
// 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) {
  
−
<br />
+
die('Produciuse un erro na aplicación.');
  
−
=== Consulta enviando parámetros de tipo numérico ===
+
} </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.
  
−
====Orientado a obxectos====
+
<br />
−
<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" 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" 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">
−
====Empregando PDO====
 
  
−
* 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.
+
<?php
−
: 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.
+
ini_set('display_errors', '1');
−
: A forma de facelo é utilizando a consulta SQL cun parámetro e substituíndo dito parámetro polo valor que queiramos enviar.
+
error_reporting(E_ALL);
  
−
* Así: SELECT col1, col2 FROM TABOA_1 WHERE col1= ''':valor'''
+
require '/var/www/confBD/bd.php';
  
−
: No exemplo, col1 é de tipo numérico.
+
try {
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1= :valor1');
 
−
// define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1= ?');  => Tamén vale con poñer unha interrogación
 
  
−
// Preparar a consulta
+
    $dsn = sprintf(
−
$stmt = $conex->prepare(CONSULTA_CON_PARAM_NUMERICO);
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
// Executar a consulta substituíndo o parámetro polo valor que queremos enviar
+
    $conex = new PDO(
−
$stmt->execute(['valor' => 5]);
+
        $dsn,
−
// 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.
+
        USUARIO,
−
// $stmt->execute([5]);
+
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
−
</syntaxhighlight>  
+
    $stmt = $conex->query(CONSULTAR_LIBROS);
  
−
: Tamén podemos indicar doutra forma o tipo de datos a enviar. Neste tipo de consulta non é necesario xa que vai facer a conversión no execute e mysql o acepta, pero pode ser que noutros xestores a comprobación de tipos sexa máis estricta e nese caso necesitaredes envialo desta forma:
+
    $libros = $stmt->fetchAll();
−
::<syntaxhighlight lang="java" enclose="div" highlight="5" >
 
−
define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1= :valor1');
 
  
−
// Preparar a consulta
+
} catch (PDOException $e) {
−
$stmt = $conex->prepare(CONSULTA_CON_PARAM_NUMERICO);
 
−
$stmt->bindValue(':valor', 5, PDO::PARAM_INT);  // Enviamos o valor 5 de tipo int.
 
  
−
// Executar a consulta substituíndo o parámetro polo valor que queremos enviar
+
    error_log($e->getMessage());
−
$stmt->execute();
+
    die('Erro relacionado coa base de datos.');
  
−
</syntaxhighlight>
+
} catch (Throwable $e) {
  
−
: Un exemplo de tipo é o boolean.
+
    die('Produciuse un erro na aplicación.');
−
: Se a base de datos Mysql vai recoller o dato nun tipo numérico, mysql convirte true a 1 e false a 0.
 
−
: Pero noutras bases de datos pode dar un erro polo que deberíamos facelo desta forma:
 
  
 +
}
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
?>
−
$activo = true;
 
−
$stmt->bindValue(':activo', $activo, PDO::PARAM_BOOL);
 
−
$result = $stmt->execute();
 
−
</syntaxhighlight>  
 
  
 +
<!DOCTYPE html>
  
 +
<html lang="gl">
 +
<head>
 +
    <meta charset="UTF-8">
 +
    <title>Operacións BD</title>
 +
</head>
  
 +
<body>
  
−
<br />
+
<?php
  
−
==== Caso práctico: Enviando parámetros de tipo numérico ====
+
if ($libros) {
  
−
* Seguindo con exemplo, imos facer que cando o usuario seleccione un libro dentro dunha combobox, aparezan os exemplares do mesmo.
+
    echo "<table border='1'>";
−
: 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.
 
  
 +
    foreach ($libros as $libro) {
  
−
<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.
+
        echo '<tr>';
  
 +
        printf(
 +
            '<td>%s</td>',
 +
            htmlspecialchars(
 +
                $libro['titulo'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            )
 +
        );
  
 +
        echo '</tr>';
 +
    }
  
−
<br />
+
    echo '</table>';
−
=====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:
+
} else {
−
: 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" enclose="div" highlight="" >
+
    echo 'Non hai datos que amosar.';
−
<?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 {
+
?>
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
  
 +
</body>
 +
</html>
 +
</syntaxhighlight>
  
−
?>
+
* Neste caso empregamos `query()` porque a sentenza SQL non recibe ningún dato externo.
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
* `fetchAll()` recupera todas as filas do resultado.
−
<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>
+
: Esta solución é adecuada cando sabemos que o número de filas que imos recuperar é pequeno.
−
    <?php       
+
 
−
       
+
* Se o resultado puidese ter moitas filas, sería preferible procesalas unha a unha:
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
 
−
        if ($result && $result->num_rows>0){
+
::<syntaxhighlight lang="php">
−
            echo "<select name='lstLibros'>";
+
while ($libro = $stmt->fetch()) {
−
            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
+
echo htmlspecialchars(
−
}  // Pecha o try da parte superior da páxina
+
     $libro['titulo'],
−
catch (mysqli_sql_exception $e) {
+
    ENT_QUOTES | ENT_SUBSTITUTE,
−
     die("Erro mysql: " . $e->getMessage());
+
     'UTF-8'
−
}
+
);
−
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.
+
} </syntaxhighlight>
−
: 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:
+
* '''IMPORTANTE:''' Non empregaremos `rowCount()` para comprobar cantas filas devolveu unha sentenza `SELECT`.
−
[[Imagen:Php_BD_9.jpg|300px|center]]
 
  
 +
: Se realmente necesitamos coñecer o número de rexistros, empregaremos `COUNT(*)`.
  
 +
<br />
  
 +
=== Caso Práctico 2 ===
  
−
* '''Arquivo bd.php'''
+
* Imos recuperar todos os autores ordenados polos seus apelidos e nome.
−
: 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.
 
  
 +
* Cada autor aparecerá como un enlace co seguinte formato:
  
 +
::<syntaxhighlight lang="html"> <a href="detalle_autor.php?id=5">
 +
Apelidos, Nome </a> </syntaxhighlight>
  
−
* Para facer que cando se seleccione un elemento do lista se 'active' o formulario, existen múltiples formas, unha delas pode ser:
+
* O identificador será enviado mediante a URL.
−
:::            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:
+
: Que un identificador apareza na URL '''non supón por si mesmo un problema de seguridade'''.
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="42,44,47-56" >
+
: O importante é validar sempre o valor recibido na páxina de destino e comprobar que o usuario ten permiso para acceder ao recurso correspondente.
−
<?php
+
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
'''Arquivo: bd.php'''
−
ini_set('display_errors', 1);
+
 
−
ini_set('display_startup_errors', 1);
+
::<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">
 +
 
 +
<?php
 +
 
 +
ini_set('display_errors', '1');
 
error_reporting(E_ALL);
 
error_reporting(E_ALL);
  
−
require ('/var/www/confBD/bd.inc.php');
+
require '/var/www/confBD/bd.php';
  
 
try {
 
try {
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
−
?>
 
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
    $dsn = sprintf(
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
     <head>
+
        SERVIDOR,
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
        BD
−
        <title>Operacións BD</title>
+
    );
−
     </head>
+
 
 +
    $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.');
  
−
    <body>
+
}
−
    <?php       
+
 
−
       
+
?>
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
 
−
        if ($result && $result->num_rows>0){
+
<!DOCTYPE html>
−
            printf("<form action='%s' method='post'>",htmlentities($_SERVER['PHP_SELF']));
+
 
−
            echo "<select name='lstLibros' onChange='this.form.submit();'>";
+
<html lang="gl">
−
            echo "<option value='-1'></option>";
+
<head>
−
            while($row = $result->fetch_assoc()) {
+
     <meta charset="UTF-8">
−
                printf("<option value='%d'>%s</option>",htmlentities($row["id"]),htmlentities($row["titulo"]));
+
    <title>Autores</title>
−
            }
+
</head>
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
     ?>       
 
−
    <?php       
 
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
 
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
 
−
            echo "<div style='margin-top:40px'>";
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$conex->real_escape_string($_POST['lstLibros']));
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<table border='1'>";
 
−
                echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<tr>';
 
−
                    printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%.2f</td><td>%d</td>",
 
−
                            htmlentities($row["id_exemplar"]),htmlentities($row["imaxe"]),
 
−
                            CARTAFOL_IMAXES_WEB . htmlentities($row["imaxe"]),htmlentities($row["prezo"]),htmlentities($row["estado_id"]));
 
−
                    echo '</tr>';
 
−
                }
 
−
                echo '</table>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
        // $conex->close();  // Non fai falla pechar a conexión xa que esta se pecha ao final do páxina web. Noutras páxinas pode ser necesario
 
−
       
 
−
    ?>      
 
  
−
    </body>   
+
<body>
−
</html>
 
  
 
<?php
 
<?php
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    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).
+
if ($autores) {
−
::* 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.
+
    echo '<ul>';
 +
 
 +
    foreach ($autores as $autor) {
  
−
::* 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.
+
        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.';
  
−
<br />
+
}
−
* Intenta agora modificar o código anterior e fai que o select manteña seleccionado o libro cando aparecen os exemplares.
 
  
 +
?>
  
 +
</body>
 +
</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 />
 
<br />
−
: Posible solución:
 
−
:::<syntaxhighlight lang="java" 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"]));
+
=== Consulta enviando parámetros de tipo numérico ===
−
            }
+
 
−
            echo "</select>";
+
* Cando unha consulta necesita recibir datos externos empregaremos unha '''sentenza preparada''' mediante:
−
            printf("</form>");
+
 
−
            $result->free();  // Liberamos da memoria os recursos
+
::<syntaxhighlight lang="php">
−
        }
+
prepare()
−
        else {
+
execute() </syntaxhighlight>
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
  
−
</syntaxhighlight>
+
* Por exemplo:
  
 +
::<syntaxhighlight lang="sql">
 +
SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor </syntaxhighlight>
  
 +
* Definimos a consulta:
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTA_CON_PARAM_NUMERICO',
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor'
 +
); </syntaxhighlight>
  
 +
* Podemos executala desta forma:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
CONSULTA_CON_PARAM_NUMERICO
 +
);
  
−
<br />
+
$stmt->execute([
−
* O código completo sería o seguinte:
+
'valor' => 5
 +
]); </syntaxhighlight>
  
−
'''Arquivo: bd.php''':
+
* O nome do parámetro debe coincidir co indicado na sentenza SQL.
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
: Se a consulta emprega:
−
<?php
 
  
−
define ('SERVIDOR','localhost');
+
::<syntaxhighlight lang="sql">
−
define ('USUARIO','user_php');
+
:valor </syntaxhighlight>
−
define ('PASSWORD','user_php');
 
−
define ('BD','php');
 
  
 +
: debemos empregar:
  
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
+
::<syntaxhighlight lang="php">
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
+
$stmt->execute([
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id'
+
'valor' => 5
−
            . ' from exemplares '  // Necesita un espazo en branco antes do from para concatenar có anterior
+
]); </syntaxhighlight>
−
            . ' 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>
+
==== 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`.
  
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
: Na maioría das consultas MySQL realizará as conversións necesarias segundo o contexto.
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
* Se queremos indicar explicitamente o tipo empregaremos `bindValue()`:
−
<?php
+
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
::<syntaxhighlight lang="php">
−
ini_set('display_errors', 1);
+
$stmt = $conex->prepare(
−
ini_set('display_startup_errors', 1);
+
CONSULTA_CON_PARAM_NUMERICO
−
error_reporting(E_ALL);
+
);
 +
 
 +
$stmt->bindValue(
 +
':valor',
 +
5,
 +
PDO::PARAM_INT
 +
);
 +
 
 +
$stmt->execute(); </syntaxhighlight>
 +
 
 +
* Algúns dos tipos dispoñibles son:
 +
 
 +
::<syntaxhighlight lang="text">
 +
PDO::PARAM_INT
 +
PDO::PARAM_STR
 +
PDO::PARAM_BOOL
 +
PDO::PARAM_NULL </syntaxhighlight>
  
−
require ('/var/www/confBD/bd.php');
+
* É especialmente recomendable indicar explicitamente o tipo cando este teña importancia para a consulta, por exemplo con `LIMIT` ou `OFFSET`.
  
−
try {
+
<br />
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
  
−
?>
+
==== Caso práctico: Enviando parámetros de tipo numérico ====
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
* Imos facer que o usuario poida seleccionar un libro nunha lista `<select>` e que se amosen os exemplares correspondentes.
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
: 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'.
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
[[Imagen:Php_BD_9.jpg|800px|center]]
−
    <?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 "</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>   
 
−
</html>
 
  
−
<?php
 
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
* Primeiro definimos as consultas.
  
 +
'''Arquivo: bd.php'''
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_LIBROS',
 +
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo'
 +
);
  
−
<br />
+
define(
−
* '''<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.
+
'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/'
 +
); </syntaxhighlight>
  
 +
===== Solución empregando PDO =====
 +
: Fixarse que
  
  
 +
'''Arquivo: consultar_exemplares_por_libro.php'''
  
 +
::<syntaxhighlight lang="php">
  
−
<br />
+
<?php
−
* 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''':
+
ini_set('display_errors', '1');
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
error_reporting(E_ALL);
−
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>
 
  
 +
require '/var/www/confBD/bd.php';
  
 +
$libros = [];
 +
$exemplares = [];
  
−
<br />
+
$libroSeleccionado = filter_input(
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
    INPUT_POST,
−
:::<syntaxhighlight lang="java" enclose="div" highlight="14" >
+
    'lstLibros',
−
        ..................
+
    FILTER_VALIDATE_INT
 +
);
  
−
    <?php       
+
try {
−
        // 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>";
 
−
        }
 
−
       
 
−
    ?>   
 
  
−
        ..................
+
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
</syntaxhighlight>
+
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
−
* 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).
+
    /*
 +
    * Obtemos os libros que se van amosar
 +
    * no elemento select.
 +
    */
 +
    $stmt = $conex->query(CONSULTAR_LIBROS);
  
 +
    $libros = $stmt->fetchAll();
  
  
−
* Resultado:
+
    /*
 +
    * Se o usuario seleccionou un libro válido,
 +
    * consultamos os seus exemplares.
 +
    */
 +
    if (
 +
        $libroSeleccionado !== false &&
 +
        $libroSeleccionado !== null &&
 +
        $libroSeleccionado !== -1
 +
    ) {
  
−
[[Imagen:Php_BD_12.jpg|400px|center]]
+
        $stmt = $conex->prepare(
 +
            CONSULTAR_EXEMPLARES_POR_LIBRO
 +
        );
  
 +
        $stmt->bindValue(
 +
            ':libro_id',
 +
            $libroSeleccionado,
 +
            PDO::PARAM_INT
 +
        );
  
 +
        $stmt->execute();
  
 +
        $exemplares = $stmt->fetchAll();
 +
    }
  
−
<br />
+
} catch (PDOException $e) {
−
=====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:
+
    error_log($e->getMessage());
−
: 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...
+
    die('Erro relacionado coa base de datos.');
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
} catch (Throwable $e) {
−
<?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');
+
    die('Produciuse un erro na aplicación.');
  
−
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);
 
  
 
?>
 
?>
  
 +
<!DOCTYPE html>
  
−
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
<html lang="gl">
−
     <html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
<head>
 +
    <meta charset="UTF-8">
 +
     <title>Exemplares por libro</title>
 +
</head>
  
−
    <head>
+
<body>
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
<form method="post">
−
        <?php
 
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
+
<select
−
        if ($result && $result->rowCount() > 0) {
+
    name="lstLibros"
−
            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>";
 
  
−
            $result = null;
+
    <option value="-1">
−
        } else {
+
         -- Escolle un libro --
−
            echo "Non hai datos que amosar!!!!";
+
    </option>
−
         }
 
  
 +
    <?php foreach ($libros as $libro): ?>
  
−
         ?>
+
         <option
−
    </body>
+
            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>
  
−
     </html>
+
     <?php endforeach; ?>
  
−
<?php
+
</select>
−
}    // 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.
+
</form>
−
: 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:
+
<?php if (
−
[[Imagen:Php_BD_9.jpg|300px|center]]
+
    $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>
  
−
* '''Arquivo bd.php'''
+
        <?php foreach ($exemplares as $exemplar): ?>
−
: 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= :libro_id order by estado_id');
 
−
</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.
 
  
 +
            <tr>
  
 +
                <td>
 +
                    <?= (int) $exemplar['id_exemplar'] ?>
 +
                </td>
  
−
* Para facer que cando se seleccione un elemento do lista se 'active' o formulario, existen múltiples formas, unha delas pode ser:
+
                <td>
−
:::            echo "<select name='lstLibros' <b>onchange='this.form.submit()'</b>>";
+
                    <img
−
::: Será necesario 'envolver' a etiqueta <select> cun <form>, con method='post' e action apuntando á mesma páxina.
+
                        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>
  
−
:* O código que vai amosar os exemplares do libro seleccionado será o seguinte:
+
                <td>
 +
                    <?= number_format(
 +
                        (float) $exemplar['prezo'],
 +
                        2,
 +
                        ',',
 +
                        '.'
 +
                    ) ?>
 +
                </td>
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="46,54-63,49-50" >
+
                <td>
−
<?php
+
                    <?= (int) $exemplar['estado_id'] ?>
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
                </td>
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require('/var/www/confBD/bd.php');
+
            </tr>
  
−
try {
+
        <?php endforeach; ?>
−
    $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);
 
  
−
?>
+
    </table>
  
 +
<?php else: ?>
  
−
     <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
     <p>Non hai exemplares que amosar.</p>
−
    <html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
  
−
    <head>
+
<?php endif; ?>
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
<?php endif; ?>
−
        <?php
 
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
+
</body>
−
        if ($result && $result->rowCount() > 0) {
+
</html>
−
            printf("<form action='%s' method='post'>",htmlentities($_SERVER['PHP_SELF']));
+
</syntaxhighlight>
−
            echo "<select name='lstLibros' onChange='this.form.submit();'>";
+
 
−
            echo "<option value='-1'></option>";
+
* Neste exemplo empregamos `filter_input()` con `FILTER_VALIDATE_INT` para comprobar que o dato recibido é un número enteiro válido.
−
            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       
+
* Despois utilizamos unha sentenza preparada e:
−
        // 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']]);
+
PDO::PARAM_INT </syntaxhighlight>
  
−
            if ($result) {
+
: para indicar explicitamente que o identificador é numérico.
−
                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>
+
* Non necesitamos empregar `mysqli_real_escape_string()` nin concatenar o dato dentro da consulta.
  
−
    </html>
+
==== Amosar a descrición do estado ====
  
−
<?php
+
* Podemos mellorar a consulta realizando un `JOIN` coa táboa `estados` para amosar o estado do exemplar.
−
}    // 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).
+
[[Imagen:Php_BD_12.jpg|800px|center]]
−
::* 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.
 
−
::* 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.
 
  
−
::* 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.
 
  
−
:: 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.
+
'''Arquivo: bd.php'''
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_EXEMPLARES_POR_LIBRO',
 +
'SELECT exemplares.libro_id,
 +
exemplares.id_exemplar,
 +
exemplares.imaxe,
 +
exemplares.prezo,
 +
estados.descripcion AS desc_estado
 +
FROM exemplares
 +
INNER JOIN estados
 +
ON estados.id = exemplares.estado_id
 +
WHERE exemplares.libro_id = :libro_id
 +
ORDER BY desc_estado'
 +
); </syntaxhighlight>
  
 +
* Para amosar o estado:
  
 +
::<syntaxhighlight lang="php">
 +
echo htmlspecialchars(
 +
$exemplar['desc_estado'],
 +
ENT_QUOTES | ENT_SUBSTITUTE,
 +
'UTF-8'
 +
); </syntaxhighlight>
  
 
<br />
 
<br />
−
* Intenta agora modificar o código anterior e fai que o select manteña seleccionado o libro cando aparecen os exemplares.
 
  
 +
==== 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.
  
  
−
<br />
 
−
: Posible solución:
 
−
:::<syntaxhighlight lang="java" 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>
 
  
 +
[[Imagen:Php_BD_13.jpg|800px|center]]
 +
 +
:<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 />
−
* O código completo sería o seguinte:
 
  
−
'''Arquivo: bd.php''':
+
=== Caso práctico: Enviando parámetros de tipo cadea ===
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
* Imos crear unha páxina que permita '''buscar libros polo nome dun autor'''.
−
<?php
+
: O que se pretende programar é algo parecido a isto:
 +
[[Imagen:Php_BD_11.jpg|800px|center]]
  
−
define ('SERVIDOR','localhost');
 
−
define ('USUARIO','user_php');
 
−
define ('PASSWORD','user_php');
 
−
define ('BD','php');
 
  
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
 
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
 
−
define ('CONSULTAR_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>
 
  
 +
: O usuario introducirá nunha caixa de texto o nome, os apelidos ou unha parte deles.
 +
 +
: Por exemplo, se introduce:
  
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
::<syntaxhighlight lang="text">
 +
gar </syntaxhighlight>
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
: poderían atoparse autores como `García`, `Vargas`, etc., dependendo dos datos existentes na base de datos.
−
<?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');
+
* Para realizar a busca empregaremos:
  
−
try {
+
:* Un formulario HTML con método `POST`.
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
+
:* Unha consulta SQL co operador `LIKE`.
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
+
:* Unha '''sentenza preparada PDO''' para enviar o texto introducido polo usuario.
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
:* 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.
  
−
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
* Como un mesmo autor pode ter varios libros, evitaremos repetir o seu nome en todas as filas da táboa.
−
    <html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
  
−
    <head>
+
* Ademais, o título de cada libro será un enlace que permitirá abrir unha nova xanela coa páxina:
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
::<syntaxhighlight lang="text">
−
        <?php
+
detalle_libro.php </syntaxhighlight>
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
+
: á que enviaremos o identificador do libro mediante `GET`.
−
        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       
+
: Para abrir esa páxina nunha nova xanela empregaremos JavaScript e a función `window.open()`.
−
        // 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
+
: Polo tanto, o funcionamento xeral será:
−
            $result = $stmt->execute([':libro_id' => $_POST['lstLibros']]);
 
  
−
            if ($result) {
+
::<syntaxhighlight lang="text">
−
                while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
+
Usuario introduce o nome do autor
−
                    echo "<table border='1'>";
+
↓
−
                    echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
+
Validamos o texto
−
                    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+
↓
−
                        echo '<tr>';
+
prepare() + bindValue()
−
                        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"] ?? ''),
+
Consulta con LIKE
−
                                CARTAFOL_IMAXES_WEB . htmlentities($row["imaxe"] ?? ''),htmlentities($row["prezo"] ?? 0),htmlentities($row["estado_id"]));
+
↓
−
                        echo '</tr>';
+
Recuperamos os libros
−
                    }
+
↓
−
                    echo '</table>';
+
Amosamos autor e título
−
   
+
↓
−
                }
+
Preme sobre un título
−
                $stmt = null;  // Liberamos da memoria os recursos
+
↓
−
            }
+
window.open()
−
            else {
+
↓
−
                echo "Non hai datos que amosar!!!!";
+
detalle_libro.php?libro=ID </syntaxhighlight>
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
       
 
−
    ?>  
 
  
−
    </body>
+
===== Arquivo bd.php =====
  
−
    </html>
+
* Engadimos a consulta que permite buscar libros polo nome ou apelidos dun autor:
  
−
<?php
+
::<syntaxhighlight lang="php">
−
}    // Pecha o try do principio da páxina
+
define(
−
catch (PDOException $e) {
+
'CONSULTAR_LIBROS_POR_AUTOR',
−
    die("Erro PDO: " . $e->getMessage());
+
"SELECT
−
} catch (Exception $e) {
+
CONCAT(
−
    die("Erro xeral: " . $e->getMessage());
+
autores.apelidos,
−
} finally {
+
', ',
−
    $conex = null; // Pechar a conexión
+
autores.nome
−
}
+
) AS nome_autor,
−
?>
+
libros.id AS id_libro,
−
</syntaxhighlight>
+
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>
  
 +
* A consulta une as táboas:
  
 +
::<syntaxhighlight lang="text">
 +
libros
 +
libro_autor
 +
autores </syntaxhighlight>
  
 +
: para obter os libros correspondentes a cada autor.
  
−
<br />
+
* Mediante:
−
* '''<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.
+
 
 +
::<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:
−
* 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''':
+
::<syntaxhighlight lang="sql">
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
LIKE '%:autor%' </syntaxhighlight>
−
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=:libro_id order by desc_estado');  //  Necesita un espazo en branco antes do where para concatenar có anterior
 
−
</syntaxhighlight>
 
  
 +
: senón que os caracteres `%` serán engadidos ao valor desde PHP:
  
 +
::<syntaxhighlight lang="php">
 +
'%' . $textoBuscar . '%' </syntaxhighlight>
  
−
<br />
+
===== Solución completa =====
−
'''Arquivo: consultar_exemplares_por_libro.php''':
 
−
:::<syntaxhighlight lang="java" enclose="div" highlight="17,19" >
 
−
        ..................
 
  
−
    <?php       
+
'''Arquivo: buscar_libros_por_autor.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']]);
+
<?php
  
−
            if ($result) {
+
/*
−
                while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
+
* Estas directivas utilízanse durante o desenvolvemento.
−
                    echo "<table border='1'>";
+
* Nun servidor en produción non se deberían amosar
−
                    echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
+
* os erros PHP ao usuario.
−
                    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+
*/
−
                        echo '<tr>';
+
ini_set('display_errors', '1');
−
                        printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%.2f</td><td>%s</td>",
+
error_reporting(E_ALL);
−
                                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>";
 
−
        }
 
−
    ?>   
 
  
−
        ..................
+
require '/var/www/confBD/bd.php';
  
−
</syntaxhighlight>
 
  
−
* 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).
+
/*
 +
* 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'] ?? ''
 +
);
  
−
* Resultado:
 
  
−
[[Imagen:Php_BD_12.jpg|400px|center]]
+
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 === '') {
  
−
<br />
+
            $erro = 'Debes introducir un texto para realizar a busca.';
  
−
=====Exercicio proposto=====
+
            /*
 +
        * Como regra da aplicación esiximos
 +
        * polo menos 3 caracteres.
 +
        */
 +
        } elseif (mb_strlen($textoBuscar) < 3) {
  
−
: Crea unha páxina de nome '''consultar_exemplares_por_estado.php''' no que aparezan en forma de radiobutton os estados da táboa ESTADOS.
+
            $erro =
−
: 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).
+
                'A cadea a buscar debe ter polo menos 3 caracteres.';
−
: O radiobutton debe manterse seleccionado.
+
        } 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();
  
  
−
<br />
+
            /*
−
'''POSIBLE SOLUCIÓN ORIENTADA A OBXECTOS''':
+
            * Se non hai filas gardamos unha mensaxe
 +
            * para amosala posteriormente.
 +
            */
 +
            if (!$libros) {
  
−
:<u>Nota</u>: Queda para vos facer a solución empregando PDO.
+
                $erro =
 +
                    'Non se atoparon libros para ese autor.';
 +
            }
 +
        }
 +
    }
 +
} catch (PDOException $e) {
  
−
'''Arquivo bd.php'''
+
    /*
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
    * Rexistramos a información técnica no log.
−
define ('CONSULTAR_ESTADOS','select id,descripcion from estados order by descripcion');
+
    * Non debemos amosala ao usuario.
−
define ('CONSULTAR_EXEMPLARES_POR_ESTADO','select titulo,id_exemplar,prezo '
+
    */
−
         . ' from exemplares INNER JOIN libros ON (libros.id = exemplares.libro_id)'
+
    error_log(
−
        .                ' INNER JOIN estados ON (estados.id = exemplares.estado_id)'
+
         $e->getMessage()
−
        . ' where estado_id=%d order by titulo');
+
    );
  
−
</syntaxhighlight>
+
    die('Erro relacionado coa base de datos.');
 +
} catch (Throwable $e) {
  
−
: 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)
+
    die('Produciuse un erro na aplicación.');
 +
}
  
 +
?>
  
−
'''Arquivo consultar_exemplares_por_estado.php'''
+
<!DOCTYPE html>
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
−
<?php
+
<html lang="gl">
−
/* 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');
+
    <head>
  
−
try {
+
        <meta charset="UTF-8">
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
+
        <title>Buscar libros por autor</title>
−
    $conex->set_charset('utf8mb4');
+
        <script>
−
?>
+
            /*
 +
            * Recibe a URL que queremos abrir
 +
            * e solicita ao navegador unha nova xanela.
 +
            */
 +
            function abrirVentana(url) {
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
                window.open(
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
                    url,
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
                    '_blank',
−
    <head>
+
                    'popup,width=600,height=400,noopener'
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
                );
−
         <title>Operacións BD</title>
+
            }
 +
         </script>
 
     </head>
 
     </head>
  
−
     <body>
+
<body>
−
     <body>
+
 
−
         <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
+
     <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 {
  
−
        <?php       
+
                    $autorAmosar =
−
        $cont=0;    // Conta o número de estados impresos e cando leve múltiplo de dous salta de liña
+
                        $libro['nome_autor'];
−
        $result=$conex->query(CONSULTAR_ESTADOS);
 
−
        if ($result && $result->num_rows>0){
 
−
            while($row = $result->fetch_assoc()) {
 
−
                $cont++;
 
−
                if(isset($_POST['rbgrpEstado']) && $row['id']==$_POST['rbgrpEstado']){  // Se volve a cargar a páxina e ven cun estado, o seleccionamos
 
−
                    $cadea="checked='checked'";
 
 
                 }
 
                 }
−
                else{
 
−
                    $cadea='';
 
−
                }
 
−
                printf("<input onclick='javascript:this.form.submit()' type='radio' name='rbgrpEstado' value='%d' %s>%s</input>",
 
−
                        $row["id"],$cadea,$row["descripcion"]);
 
−
                if(($cont%2)==0){  // Cada par cambiamos de liña
 
−
                    echo "<br />";
 
−
                }
 
−
            }
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai estados que amosar!!!!";
 
−
        }
 
−
        ?>       
 
−
        </form>
 
−
       
 
−
       
 
−
        <?php       
 
−
        // CONSULTAMOS OS EXEMPLARES DUN ESTADO SELECCIONADO
 
−
        if (!empty($_POST['rbgrpEstado']) && filter_var($_POST['rbgrpEstado'],FILTER_VALIDATE_INT)){
 
−
           
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_ESTADO,$conex->real_escape_string($_POST['rbgrpEstado']));
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<ol>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<li>';
 
−
                    printf("T&iacute;tulo:%s - Num_exemplar:%d - Prezo:%s",$row["titulo"],$row["id_exemplar"],$row["prezo"]);
 
−
                    echo '</li>';
 
−
                }
 
−
                echo '</ol>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
           
 
−
        }
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
  </body>   
 
−
</html>
 
  
 +
            ?>
 +
                <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
  
−
<?php
+
            /*
−
}  // Pecha o try da parte superior da páxina
+
            * Gardamos o autor actual para comparalo
−
catch (mysqli_sql_exception $e) {
+
            * co autor da seguinte fila.
−
    die("Erro mysql: " . $e->getMessage());
+
            */
−
}
+
                $ultimoAutor =
−
catch (Exception $e) {
+
                    $libro['nome_autor'];
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
            endforeach;
 +
            ?>
 +
        </table>
 +
    <?php endif; ?>
 +
</body>
 +
</html>
 +
</syntaxhighlight>
  
 +
===== Explicación da solución =====
  
 +
* Recuperamos o texto enviado polo formulario mediante:
  
−
* Resultado:
+
::<syntaxhighlight lang="php">
 +
$textoBuscar = trim(
 +
$_POST['txtAutor'] ?? ''
 +
); </syntaxhighlight>
  
−
[[Imagen:Php_BD_13.jpg|400px|center]]
+
* 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.
  
  
 
<br />
 
<br />
 +
'''Validación do dato'''
  
−
=== Consulta enviando parámetros de tipo cadea ===
+
* Comprobamos primeiro que a cadea non estea baleira:
  
 +
::<syntaxhighlight lang="php">
 +
if ($textoBuscar === '') { </syntaxhighlight>
  
 +
* Como exemplo dunha regra de validación da aplicación, esiximos tamén unha lonxitude mínima:
  
−
<br />
+
::<syntaxhighlight lang="php">
−
====Orientado a obxectos====
+
elseif (mb_strlen($textoBuscar) < 3) { </syntaxhighlight>
  
−
* Cando utilicemos datos de tipo cadea, debemos lembrar poñelos entre comillas simples ('dato') na orde SQL.
+
: A esixencia de tres caracteres é unha regra da nosa aplicación.
  
 +
: Non é unha medida para evitar inxección SQL.
  
−
<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].
 
  
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
+
<br />
−
: A forma de facelo é utilizando a consulta SQL cun parámetro e substituíndo dito parámetro polo valor que queiramos enviar.
+
'''Preparación da consulta'''
  
−
* Así: SELECT col1, col2 FROM TABOA_1 WHERE col2=''''%d''''
+
* A consulta recibe datos procedentes do usuario, polo que debemos empregar unha sentenza preparada:
  
−
: No exemplo, col2 é de tipo cadea (por exemplo, varchar ou char).
+
::<syntaxhighlight lang="php">
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
$stmt = $conex->prepare(
−
define ('CONSULTA_CON_PARAM_CADEA',"SELECT col1, col2 FROM TABOA_1 WHERE col2='%s'");
+
CONSULTAR_LIBROS_POR_AUTOR
 +
); </syntaxhighlight>
  
−
$query = sprintf(CONSULTA_CON_PARAM_CADEA,'valor_cadea_a_enviar');
 
−
</syntaxhighlight>
 
−
: Fixarse como o parámetro col2 vai entrecomillado e o formato do parámetro é '%s'.
 
  
 +
<br />
 +
'''Asociación do parámetro'''
  
−
: Agora facemos o mesmo código que no exemplo anterior.
+
* Asociamos o valor ao parámetro `:autor`:
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
......
 
−
$result=$conex->query(CONSULTA_CON_PARAM_CADEA);
 
−
......
 
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="php">
 +
$stmt->bindValue(
 +
':autor',
 +
'%' . $textoBuscar . '%',
 +
PDO::PARAM_STR
 +
); </syntaxhighlight>
  
 +
* Indicamos:
  
 +
::<syntaxhighlight lang="php">
 +
PDO::PARAM_STR </syntaxhighlight>
  
−
<br />
+
: porque estamos enviando unha cadea.
  
  
−
====Empregando PDO====
 
  
−
* Non hai ningunha diferenza con respecto a enviar un dato de tipo numérico ou do tipo que sexa. Xa se encarga de poñelo entre comillas se é de cadea.
+
<br />
 +
'''Funcionamento de LIKE'''
  
 +
* Os caracteres `%` actúan como comodíns.
  
 +
: Se o usuario introduce:
  
−
<br />
+
::<syntaxhighlight lang="text">
 +
gar </syntaxhighlight>
  
−
==== Caso práctico: Enviando parámetros de tipo cadea ====
+
: PHP enviará:
  
−
* Imos a aplicalo ao noso exemplo e imos crear unha páxina no que buscaremos libros en base a un nome do autor.
+
::<syntaxhighlight lang="text">
−
:: 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.
+
%gar% </syntaxhighlight>
−
:: Se queremos distinguir maiúsculas de minúsculas utilizaremos o operador [http://stackoverflow.com/questions/14007450/how-do-you-force-mysql-like-to-be-case-sensitive like binary].
 
−
:: O operador like o imos aplicar ao nome completo do autor, polo que teremos que concatenar o nome e apelido. Usaremos a función [http://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_concat-ws concat_ws] ou [http://dev.mysql.com/doc/refman/8.0/en/string-functions.html#function_concat concat].
 
  
 +
* Desta forma poderán coincidir cadeas nas que `gar` apareza ao principio, no medio ou ao final.
  
−
* O usuario introducirá nunha caixa de texto o nome (ou parte del) do autor.
 
  
 +
<br />
 +
'''Execución e recuperación dos resultados'''
  
 +
* Executamos:
  
−
<br />
+
::<syntaxhighlight lang="php">
−
=====Orientado a obxectos=====
+
$stmt->execute(); </syntaxhighlight>
−
: <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].
+
 
 +
* E recuperamos os resultados:
  
−
:: 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'.
+
::<syntaxhighlight lang="php">
−
:: Por exemplo:
+
$libros = $stmt->fetchAll(); </syntaxhighlight>
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
$cidade = "'s Hertogenbosch";
 
  
−
/* esta consulta fallará debido a que no escapa $ciudad */
+
* Como na conexión configuramos:
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
 
−
    printf("Erro: %s\n", $mysqli->sqlstate);
 
−
}
 
  
−
$cidade = $mysqli->real_escape_string($cidade);
+
::<syntaxhighlight lang="php">
 +
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC </syntaxhighlight>
  
−
/* esta consulta con $ciudad escapada funcionará */
+
: cada fila recuperada será un array asociativo.
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
 
−
    printf("Fila inxerida.\n");
 
−
}
 
  
−
</syntaxhighlight>
 
−
::Exemplo obtido de php.net
 
  
 +
<br />
 +
'''Evitar repetir o nome do autor'''
  
−
* 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:
+
* Pode suceder que un mesmo autor teña varios libros.
  
−
[[Imagen:Php_BD_10.jpg|500px|center]]
+
: Por exemplo:
  
−
:: 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.
+
::<syntaxhighlight lang="text">
 +
García, Ana | Libro A
 +
García, Ana | Libro B
 +
García, Ana | Libro C
 +
Pérez, Luís | Libro D </syntaxhighlight>
  
−
:: O código sería algo parecido a isto:
+
* Queremos amosalo desta forma:
−
::<syntaxhighlight lang="java" 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);
+
::<syntaxhighlight lang="text">
 +
García, Ana | Libro A
 +
            | Libro B
 +
            | Libro C
 +
Pérez, Luís | Libro D </syntaxhighlight>
  
−
                        $last_autor=$row["nome_autor"];  // Gardamos o nome do autor para comparar có autor que veña na seguinte fila de datos.
+
* Para iso gardamos o último autor procesado:
−
                    }
 
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php">
 +
$ultimoAutor = ''; </syntaxhighlight>
  
 +
* En cada iteración comparamos:
  
 +
::<syntaxhighlight lang="php">
 +
if ($ultimoAutor ===$libro['nome_autor']) {
  
−
* 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).
+
  $autorAmosar = '';
  
−
:: Para facelo imos facer uso de javascript e da [http://www.w3schools.com/jsref/met_win_open.asp función window.open].
+
} else {
−
:: 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,...
+
     $autorAmosar = $libro['nome_autor'];
−
:: O código será o seguinte:
+
}  
−
::<syntaxhighlight lang="java" enclose="div" highlight="4-8" >
+
</syntaxhighlight>
−
     <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>  
 
  
 +
* E ao rematar gardamos o autor actual:
  
−
:: Para chamar a esta función usaremos a etiqueta <a> e o evento onclick, coma o seguinte exemplo:
+
::<syntaxhighlight lang="php">
−
::<syntaxhighlight lang="java" enclose="div" highlight="4-8" >
+
$ultimoAutor =
−
<a href='#' onclick="javascript:abrirVentana('detalle_libro.php?libro=1');">Título Libro</a>
+
$libro['nome_autor']; </syntaxhighlight>
−
</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 />
 +
'''Abrir o detalle dun libro'''
  
 +
* Cada título será un enlace á páxina:
  
 +
::<syntaxhighlight lang="text">detalle_libro.php </syntaxhighlight>
  
 +
* O identificador enviarase mediante `GET`:
  
−
<br />
+
::<syntaxhighlight lang="text">detalle_libro.php?libro=5 </syntaxhighlight>
−
* '''SOLUCIÓN'''
 
  
−
'''Arquivo: bd.inc.php'''
 
  
−
<u>Nota:</u> Definimos a consulta a realizar á base de datos ('''lembrar que é mellor facer dita consulta directamente no Mysql e despois copiala ao código PHP pasando os parámetros correspondentes''')
 
−
::<syntaxhighlight lang="java" enclose="div">
 
−
<?php
 
  
−
define ('CONSULTAR_LIBROS_POR_AUTOR',"select CONCAT(apelidos,',',nome) as nome_autor,libros.id ,titulo from libros " .
+
* O enlace xerado será:
−
        ' INNER JOIN libro_autor ON (libro_autor.libro_id=libros.id) ' .
+
::<syntaxhighlight lang="php">
−
        ' INNER JOIN autores ON (libro_autor.autor_id=autores.id)' .
+
<a href="detalle_libro.php?libro=<?= (int) $libro['id_libro'] ?>" onclick="abrirVentana(this.href); return false;">
−
        " where CONCAT(autores.nome,' ',autores.apelidos) like '%%%s%%' " .
 
−
        'order by nome_autor,titulo');
 
 
</syntaxhighlight>
 
</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.
+
* Mediante:
−
: É 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="javascript">this.href </syntaxhighlight>
 +
 +
: obtemos a URL real almacenada no atributo `href`.
 +
 +
* Chamamos despois á función:
 +
 +
::<syntaxhighlight lang="javascript">abrirVentana(this.href); </syntaxhighlight>
 +
 +
* Finalmente:
 +
 +
::<syntaxhighlight lang="javascript">return false; </syntaxhighlight>
 +
 +
: evita que o navegador abra tamén o enlace na xanela actual.
  
  
  
 
<br />
 
<br />
−
'''Arquivo buscar_libros_por_autor.php'''
+
'''Función JavaScript'''
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
−
<?php
+
* A función empregada é:
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
 
−
ini_set('display_errors', 1);
+
::<syntaxhighlight lang="javascript">
−
ini_set('display_startup_errors', 1);
+
function abrirVentana(url) {
−
error_reporting(E_ALL);
+
 
 +
window.open(
 +
    url,
 +
    '_blank',
 +
    'popup,width=600,height=400,noopener'
 +
);
  
−
require ('/var/www/confBD/bd.inc.php');
+
} </syntaxhighlight>
  
−
try {
+
* `window.open()` solicita ao navegador a apertura dunha nova xanela.
−
    $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.
+
* 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`.
 +
 
 +
'''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.
  
−
<!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>
 
−
        <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']) ? htmlentities($_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 {
 
−
                $query=sprintf(CONSULTAR_LIBROS_POR_AUTOR,$conex->real_escape_string($textoBuscar));
 
−
                $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
+
<br />
−
}  // Pecha o try da parte superior da páxina
+
'''Vantaxe de empregar un href real'''
−
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>
 
  
−
* 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.
+
* Non empregamos:
−
:: Polo tanto a conexión e o peche da base de datos se podería poñer dentro do else da liña 43.
+
::<syntaxhighlight lang="html"> <a href="#"> </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.
 
  
 +
* Empregamos:
 +
::<syntaxhighlight lang="html"> <a href="detalle_libro.php?libro=5"> </syntaxhighlight>
  
−
* '''Resultado:'''
+
: 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.
−
[[Imagen:Php_BD_11.jpg|500px|center]]
 
  
  
  
 
<br />
 
<br />
 +
'''Páxina detalle_libro.php'''
  
−
<br />
+
* A páxina de detalle recibirá o identificador do libro mediante:
−
=====Empregando PDO=====
 
  
−
* Imos crear unha páxina no que buscaremos libros en base a un nome do autor.
+
::<syntaxhighlight lang="text">$_GET['libro'] </syntaxhighlight>
−
:: A busca farase utilizando a [http://www.w3schools.com/sql/sql_like.asp orde SQL '''like'''], que busca patróns de cadea. Este operador non distingue maiúsculas de minúsculas.
 
−
:: Se queremos distinguir maiúsculas de minúsculas utilizaremos o operador [http://stackoverflow.com/questions/14007450/how-do-you-force-mysql-like-to-be-case-sensitive like binary].
 
−
:: O operador like o imos aplicar ao nome completo do autor, polo que teremos que concatenar o nome e apelido. Usaremos a función [http://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_concat-ws concat_ws] ou [http://dev.mysql.com/doc/refman/8.0/en/string-functions.html#function_concat concat].
 
  
 +
* Como é un dato externo, debemos validalo:
  
−
* O usuario introducirá nunha caixa de texto o nome (ou parte del) do autor.
+
::<syntaxhighlight lang="php">
 +
$libroId = filter_input(
 +
INPUT_GET,
 +
'libro',
 +
FILTER_VALIDATE_INT
 +
);
  
−
: <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].
+
if (
−
: Empregando PDO <u>non necesitamos escapar os parámetros</u> xa que o fai el automaticamente.
+
  $libroId === false ||
 +
  $libroId === null ||
 +
  $libroId < 1
 +
) {
  
 +
  die('Identificador de libro non válido.');
  
 +
}
 +
</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:
 
  
−
[[Imagen:Php_BD_10.jpg|500px|center]]
 
  
−
:: Para evitalo, teremos que gardar en cada iteración unha vez 'impresa' a liña do autor, o nome do autor e comparalo cando carguemos a seguinte fila de datos. Se o nome do autor da nova liña de datos é igual ao anterior, entón non o debemos de amosar.
+
* A consulta posterior tamén deberá ser preparada:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
  'SELECT id, titulo
 +
  FROM libros
 +
  WHERE id = :libro_id'
 +
);
  
−
:: O código sería algo parecido a isto:
+
$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
$stmt->execute();
−
                    $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(PDO::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);
+
$libro = $stmt->fetch();  
 +
</syntaxhighlight>
  
−
                        $last_autor=$row["nome_autor"];  // Gardamos o nome do autor para comparar có autor que veña na seguinte fila de datos.
+
* Se queremos recuperar tamén os exemplares dese libro faremos outra consulta preparada:
−
                    }
 
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php">
 +
$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();
 +
</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).
+
===== Resumo do proceso =====
  
−
:: Para facelo imos facer uso de javascript e da [http://www.w3schools.com/jsref/met_win_open.asp función window.open].
+
::<syntaxhighlight lang="text">
−
:: 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,...
+
$_POST['txtAutor']
−
:: O código será o seguinte:
+
↓
−
::<syntaxhighlight lang="java" enclose="div" highlight="4-8" >
+
trim()
−
    <head>
+
↓
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
validación
−
        <title>Operacións BD</title>
+
↓
−
        <script type="text/javascript">
+
prepare()
−
            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');
+
bindValue(PDO::PARAM_STR)
−
            }
+
↓
−
        </script>
+
execute()
−
       
+
↓
−
    </head>
+
fetchAll()
−
</syntaxhighlight>  
+
↓
 +
htmlspecialchars()
 +
↓
 +
enlace ao libro
 +
↓
 +
window.open()
 +
↓
 +
detalle_libro.php?libro=ID
 +
↓
 +
FILTER_VALIDATE_INT
 +
↓
 +
prepare() + PDO::PARAM_INT </syntaxhighlight>
 +
 
 +
</syntaxhighlight>
  
  
−
:: Para chamar a esta función usaremos a etiqueta <a> e o evento onclick, coma o seguinte exemplo:
+
* Debemos diferenciar claramente:
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
<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>.
+
::<syntaxhighlight lang="text">
  
 +
! 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>
  
  
  
 
<br />
 
<br />
−
* '''SOLUCIÓN'''
 
  
−
'''Arquivo: bd.php'''
+
=== Consulta chamando a procedementos almacenados ===
  
−
<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''')
+
* Un procedemento almacenado permite gardar no propio xestor de base de datos un conxunto de sentenzas SQL que poden ser executadas posteriormente.
−
::<syntaxhighlight lang="java" enclose="div">
 
−
<?php
 
  
−
define ('CONSULTAR_LIBROS_POR_AUTOR',"select CONCAT(apelidos,',',nome) as nome_autor,libros.id ,titulo from libros " .
+
* Desde PDO chamarémolo mediante a sentenza:
−
        ' 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,titulo');
 
−
</syntaxhighlight>
 
  
−
: <u>Nota:</u> Fixarse como o parámetro que recibe 'like' é de tipo cadea e polo tanto vai entre comillas. Ao empregar parámetros con PDO el xa se encarga de poñer comillas.
+
::<syntaxhighlight lang="sql">
−
: '''A complicación é que nos queremos enviar %valor% para que busque por patrón. Iso o facemos ao darlle o valor ao parámetro.'''
+
CALL NOME_PROCEDEMENTO() </syntaxhighlight>
  
 +
* Por exemplo, en MySQL:
  
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
  
 +
CREATE PROCEDURE CONSULTAR_LIBROS()
 +
BEGIN
  
−
<br />
+
SELECT id, titulo
−
'''Arquivo buscar_libros_por_autor.php'''
+
FROM libros
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
ORDER BY titulo;
−
<?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');
+
END$$
  
−
try {
+
DELIMITER ; </syntaxhighlight>
−
    $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);
 
  
−
    $erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
==== Empregando PDO ====
−
?>
 
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_LIBROS()'
 +
);
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
$stmt->execute();
−
    "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>
+
$libros = $stmt->fetchAll();
−
        <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']) ? htmlentities($_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 {
 
−
                $stmt=$conex->prepare(CONSULTAR_LIBROS_POR_AUTOR);    // Temos que poñer prepare
 
−
                $autor = $_POST['txtAutor'];
 
−
                $result = $stmt->execute(['autor' => "%$autor%"]);
 
  
−
                if ($result){
+
$stmt->closeCursor(); </syntaxhighlight>
−
                    $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 = $stmt->fetch(PDO::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 = null;
 
−
                    echo "</table>";
 
−
                }
 
−
                else {
 
−
                    $erro = "<br />Non hai resultados coa cadea $textoBuscar";
 
−
                }
 
−
               
 
−
               
 
−
            }
 
−
        }
 
−
       
 
−
        ?>       
 
−
        </form>
 
−
        <?php       
 
−
        if ($erro!='') {
 
−
            printf("<h3>%s</h3>",$erro);
 
−
        }
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
* `closeCursor()` libera os resultados pendentes asociados á chamada.
−
}    // 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>
 
  
−
* 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.
+
: É especialmente recomendable ao traballar con procedementos almacenados en MySQL antes de executar outra consulta na mesma conexión.
−
:: Polo tanto a conexión e o peche da base de datos se podería poñer dentro do else da liña 44.
 
−
* 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.
 
  
 +
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
  
−
* '''Resultado:'''
+
* Definimos o nome do procedemento:
−
[[Imagen:Php_BD_11.jpg|500px|center]]
 
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'PROCEDEMENTO_CONSULTAR_LIBROS',
 +
'CONSULTAR_LIBROS'
 +
); </syntaxhighlight>
  
−
<br />
+
* Realizamos a chamada:
  
−
=== Consulta chamando a procedementos almacenados ===
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL ' .
 +
PROCEDEMENTO_CONSULTAR_LIBROS .
 +
'()'
 +
);
  
−
* 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.
+
$stmt->execute();
  
−
* 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.
+
$libros = $stmt->fetchAll();
  
−
: 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.
+
$stmt->closeCursor(); </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.
 
  
 +
* Non necesitamos comprobar o resultado de `execute()` para saber se o `SELECT` devolveu filas.
  
−
* Máis información [http://php.net/manual/es/mysqli.quickstart.stored-procedures.php neste enlace].
+
: `execute()` indica se a sentenza puido executarse.
  
−
* O primeiro que temos que facer é crear o procedemento almacenado.
+
: Para saber se temos resultados comprobamos os datos recuperados:
−
* A orde SQL é [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html '''CREATE PROCEDURE'''].
 
  
−
* Creamos o procedemento almacenado:
+
::<syntaxhighlight lang="php">
−
:* 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].
+
if ($libros) {
  
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `NOME_PROCEDEMENTO` ()
+
// Hai resultados
−
BEGIN
 
  
−
  select col1,col2
+
} else {
−
  from TABOA_1
 
−
  order by col1;
 
  
 +
// Non hai resultados
  
−
END
+
} </syntaxhighlight>
−
$$
 
−
</syntaxhighlight>  
 
  
−
: Como vemos o procedemento está formado por unha consulta SELECT (pode poñerse calquera)
+
<br />
  
 +
=== Consulta chamando a procedementos almacenados con parámetros de entrada ===
  
−
* Agora dende PHP temos que facer uso do comando CALL 'NOME_PROCEDEMENTO'.
+
* 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.
  
−
<br />
+
* Por exemplo:
−
====Orientado a obxectos====
 
−
: Isto é o que teriamos que poñer se dende Mysql queredes chamar ao procedemento....
 
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="12" >
+
::<syntaxhighlight lang="sql">
−
<?php
+
DELIMITER $$
−
/* 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)
+
CREATE PROCEDURE PROCEDEMENTO_CON_PARAMETROS(
 +
IN p_num INT,
 +
IN p_text VARCHAR(100)
 +
)
 +
BEGIN
  
−
try {
+
SELECT col1, col2
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);    
+
FROM TABOA_1
−
    $conex->set_charset('utf8mb4');
+
WHERE col1 = p_num
−
    $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
+
   AND col2 = p_text;
  
−
} 
+
END$$
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
  
 +
DELIMITER ; </syntaxhighlight>
  
−
</syntaxhighlight>
+
==== 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
 +
)'
 +
);
  
−
* A diferenza do caso anterior, cando chamemos a un procedemento almacenado teremos que facer uso dunha das seguintes funcións:
+
$stmt->bindValue(
−
:: [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]
+
':param_num',
 +
$valorNum,
 +
PDO::PARAM_INT
 +
);
  
−
: 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).
+
$stmt->bindValue(
−
::<syntaxhighlight lang="java" enclose="div" highlight="3,4" >
+
':param_text',
−
        ........................
+
$valorTexto,
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
+
PDO::PARAM_STR
−
        if ($conex->multi_query($proc)){
+
);
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
  
−
        }
+
$stmt->execute();
 +
 
 +
$filas = $stmt->fetchAll();
 +
 
 +
$stmt->closeCursor(); </syntaxhighlight>
  
−
</syntaxhighlight>
+
* Desta forma non necesitamos empregar `sprintf()` nin concatenar os valores dentro da chamada SQL.
  
 +
==== Caso Práctico: buscar libros por autor ====
  
−
: Unha vez temos o conxunto de resultados ($result) xa operamos como nos casos anteriores.
+
* 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>
  
  
−
*<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.
+
* Procedemento almacenado (isto tedes que executalo en Mysql empregando a ferramenta Mysql WorkBench ou PhpMyAdmin):
−
: A resposta a ese erro a tedes [https://dev.mysql.com/doc/refman/8.0/en/commands-out-of-sync.html neste enlace].
 
  
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
  
 +
CREATE PROCEDURE CONSULTAR_LIBROS_POR_AUTOR(
 +
IN p_autor_buscar VARCHAR(100)
 +
)
 +
BEGIN
  
−
* 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.
+
SELECT CONCAT(
−
: Para facelo, podedes engadir o seguinte código:
+
          autores.apelidos,
−
::<syntaxhighlight lang="java" enclose="div" highlight="9" >
+
          ', ',
−
        ........................
+
          autores.nome
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
+
      ) AS nome_autor,
−
        if ($conex->multi_query($proc)){
+
      libros.id AS id_libro,
−
            $result = $conex->store_result();
+
      libros.titulo
−
            if ($result && ($result->num_rows>0)){
+
FROM libros
−
                ................
+
INNER JOIN libro_autor
−
                // Procesamos cada fila
+
    ON libro_autor.libro_id = libros.id
−
            }
+
INNER JOIN autores
−
            while($conex->next_result() && $conex->store_result());
+
    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>
  
−
</syntaxhighlight>
+
* Chamada desde PDO cambiando no exercicio anterior a chamada ao SELECT pola chamada a este procedemento almacenado:
  
 +
::<syntaxhighlight lang="php" highlight="23-25,58">
 +
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  
 +
        /*
 +
        * Comprobamos que se introduciu algún texto.
 +
        */
 +
        if ($textoBuscar === '') {
  
 +
            $erro = 'Debes introducir un texto para realizar a busca.';
  
−
* 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:
+
        * Como regra da aplicación esiximos
 +
        * polo menos 3 caracteres.
 +
        */
 +
        } elseif (mb_strlen($textoBuscar) < 3) {
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="8-10" >
+
            $erro =
−
        ........................
+
                'A cadea a buscar debe ter polo menos 3 caracteres.';
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
+
         } else {
−
         if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
            if ($conex->next_result()){
 
−
              $result=$conex->store_result();
 
−
            }
 
  
−
        }
+
            /*
 +
            * Preparamos a consulta.
 +
            */
 +
            $stmt = $conex->prepare(
 +
                'call CONSULTAR_LIBROS_POR_AUTOR(:autor)'
 +
            );
  
−
</syntaxhighlight>
 
  
−
: Se imos procesar todos os conxuntos de resultados da mesma forma poderíamos utilizar un while{}...
+
            /*
 +
            * 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();
  
  
−
<br />
+
            /*
−
====Empregando PDO====
+
            * Recuperamos todos os resultados.
−
::<syntaxhighlight lang="java" enclose="div" highlight="27" >
+
            */
−
<?php
+
            $libros = $stmt->fetchAll();
−
/* 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');
+
            $stmt->closeCursor();
  
−
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);
 
  
−
?>
+
            /*
 +
            * Se non hai filas gardamos unha mensaxe
 +
            * para amosala posteriormente.
 +
            */
 +
            if (!$libros) {
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
                $erro =
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
                    'Non se atoparon libros para ese autor.';
−
<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>
+
</syntaxhighlight>
−
     </head>
 
  
−
    <body>
+
<br />
−
    <?php       
 
−
    // Preparar a chamada ao procedemento almacenado
 
−
    $proc = $conex->prepare("CALL " . NOME_PROCEDEMENTO);
 
  
−
    // Se o procedemento necesita parámetros:
+
=== Consulta chamando a procedementos almacenados con parámetros de saída ===
−
    // $proc->execute(['param1' => $valor1, 'param2' => $valor2]);
 
−
    // Sen parámetros:
 
−
    $proc->execute();
 
−
       
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
* Un parámetro `OUT` permite que un procedemento devolva un valor.
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
  die("Erro PDO: " . $e->getMessage());
 
−
} finally {
 
−
  $conex = null; // Pechar a conexión
 
−
}
 
  
−
?>
+
* Por exemplo:
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
  
 +
CREATE PROCEDURE CONSULTAR_NUM_LIBROS(
 +
OUT p_num_libros INT
 +
)
 +
BEGIN
  
−
<br />
+
SELECT COUNT(*)
−
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
+
INTO p_num_libros
 +
FROM libros;
  
−
* 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.
+
END$$
  
 +
DELIMITER ; </syntaxhighlight>
  
−
* Creamos o procedemento almacenado:
+
* Con PDO e MySQL podemos empregar unha variable de usuario de MySQL:
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS` ()
+
::<syntaxhighlight lang="php">
−
BEGIN
+
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_NUM_LIBROS(
 +
@numero_libros
 +
)'
 +
);
  
−
  select ìd,titulo
+
$stmt->execute();
−
  from libros
+
 
−
  order by titulo;
+
/*
 +
 
 +
* É importante liberar os resultados
 +
* da chamada antes de executar outro SELECT.
 +
  */
 +
  $stmt->closeCursor();
 +
 
 +
$stmt = $conex->query(
 +
'SELECT @numero_libros'
 +
);
 +
 
 +
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
 +
 
 +
* Podemos amosar o resultado:
 +
 
 +
::<syntaxhighlight lang="php">
 +
printf(
 +
'<h1>Número de libros á venda: %d</h1>',
 +
$numLibros
 +
); </syntaxhighlight>
  
 +
<br />
  
−
END
+
=== Consulta chamando a funcións ===
−
$$
 
−
</syntaxhighlight>
 
  
 +
* 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.
  
−
* Definimos nunha constante o nome do procedemento almacenado dentro do arquivo bd.php.
+
* Por exemplo:
  
−
:'''Arquivo: bd.php'''
+
::<syntaxhighlight lang="sql">
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
DELIMITER $$
−
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>
 
  
 +
CREATE FUNCTION CONSULTAR_NUM_LIBROS()
 +
RETURNS INT
 +
READS SQL DATA
 +
BEGIN
  
 +
DECLARE num_libros INT;
  
−
<br />
+
SELECT COUNT(*)
−
=====Empregando procedementos almacenados=====
+
INTO num_libros
 +
FROM libros;
  
−
* 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]
+
RETURN num_libros;
−
:: 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'''
+
END$$
−
::<syntaxhighlight lang="java" enclose="div" highlight="24-26" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
DELIMITER ; </syntaxhighlight>
  
−
try {
+
==== Empregando PDO ====
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
?>
+
* Podemos chamar á función mediante un `SELECT`:
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
 
−
    "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">
+
$stmt = $conex->query(
−
    <head>
+
'SELECT CONSULTAR_NUM_LIBROS()'
−
        <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()
+
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
−
:* 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.
 
  
 +
* `fetchColumn()` resulta especialmente útil cando unha consulta devolve un único valor.
  
 +
==== Funcións con parámetros ====
  
−
<br />
+
* Supoñamos unha función:
−
=====Empregando PDO=====
 
−
:'''Arquivo: consultar_libros_proc.php'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="24-26" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
::<syntaxhighlight lang="sql">
 +
SELECT Suma(5, 7) </syntaxhighlight>
  
−
try {
+
* Desde 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);
 
  
−
?>
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT Suma(:a, :b)'
 +
);
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
$stmt->bindValue(
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
':a',
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
5,
−
    <head>
+
PDO::PARAM_INT
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
);
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
$stmt->bindValue(
−
    <?php       
+
':b',
−
       
+
7,
−
        $proc=$conex->prepare("CALL " . PROCEDEMENTO_CONSULTAR_LIBROS);
+
PDO::PARAM_INT
−
        $result = $proc->execute();
+
);
−
        // Comprobar se hai filas
 
−
        if ($result) {
 
−
            echo "<table border='1'>";
 
−
            while ($fila = $proc->fetch(PDO::FETCH_ASSOC)) {
 
−
                echo '<tr>';
 
−
                printf("<td>%s</td>",htmlentities($fila["titulo"]));
 
−
                echo '</tr>';
 
−
            }
 
−
            echo '</table>';
 
−
            $result = null; 
 
−
        } else {
 
−
          echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
     
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
$stmt->execute();
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
  die("Erro PDO: " . $e->getMessage());
 
−
} finally {
 
−
  $conex = null; // Pechar a conexión
 
−
}
 
  
−
?>
+
$resultado = $stmt->fetchColumn(); </syntaxhighlight>
  
−
</syntaxhighlight>
+
<br />
  
−
=== Consulta chamando a procedementos almacenados con parámetros de entrada ===
+
=== Paxinación de resultados ===
  
−
* Podemos chamar a un procedemento almacenado enviando datos ao mesmo en forma de parámetros.
+
* En moitas ocasións unha consulta pode devolver unha cantidade elevada de rexistros.
−
: 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].
+
: Para evitar amosalos todos simultaneamente podemos dividir o resultado en páxinas.
  
−
* Por defecto, se non especificamos nada, todos os parámetros dun procedemento almacenado son considerados 'de entrada'
+
* En MySQL podemos empregar `LIMIT` e `OFFSET`.
  
−
* Primeiro teremos que definir o procedemento en Mysql:
+
* Por exemplo:
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100))
+
::<syntaxhighlight lang="sql">
−
BEGIN
+
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT 2 OFFSET 0; </syntaxhighlight>
 +
 
 +
: Devolverá os dous primeiros rexistros.
  
−
    SELECT col1,col2
+
::<syntaxhighlight lang="sql">
−
    FROM TABOA_1
+
SELECT id, titulo
−
    WHERE col2=_param1;
+
FROM libros
 +
ORDER BY titulo
 +
LIMIT 2 OFFSET 2; </syntaxhighlight>
  
−
END
+
: Devolverá os dous seguintes.
−
</syntaxhighlight>
 
  
−
: Como vemos, xunto ao nome do procedemento hai que especificar o tipo (dentro dos tipos de mysql).
+
==== Pasos necesarios para realizar a paxinación ====
−
: 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.
 
  
 +
* Necesitamos:
  
−
: Podemos definir máis parámetros, separando por comas cada un deles, por exemplo:
+
:# Coñecer o número total de rexistros.
−
::<syntaxhighlight lang="java" enclose="div" highlight="3,8,9" >
+
:# Establecer cantos rexistros se amosarán por páxina.
−
DELIMITER $$
+
:# Coñecer a páxina actual.
 +
:# Calcular o número total de páxinas.
 +
:# Calcular o desprazamento (`OFFSET`).
 +
:# Realizar a consulta correspondente.
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100),_param2 int)
 
−
BEGIN
 
  
−
    SELECT col1,col2
+
<br />
−
    FROM TABOA_1
+
'''Número total de rexistros'''
−
    WHERE col2=_param1 and
 
−
          col1=_param2
 
  
−
END
+
::<syntaxhighlight lang="php">
−
</syntaxhighlight>  
+
$stmt = $conex->query(
 +
'SELECT COUNT(*)
 +
FROM libros'
 +
);
  
 +
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
  
−
* Unha  vez definido teremos que enviar os datos que queiramos dende PHP.
 
  
  
 
<br />
 
<br />
−
====Orientado a obxectos====
+
'''Número de rexistros por páxina'''
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
+
::<syntaxhighlight lang="php">
−
</syntaxhighlight>  
+
define(
−
: Fixarse como no caso de datos de tipo numérico non enviamos o dato con comiñas.
+
'CONST_NUM_LIBROS_PAXINA',
−
: Fixarse como 'xogamos' coas comiñas simples de dobres para pechar correctamente a cadea.
+
10
 +
); </syntaxhighlight>
 +
 
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="5,6" >
+
<br />
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
+
'''Obter e validar a páxina actual'''
  
−
        ........................
+
* O número de páxina chegará normalmente mediante `GET`.
  
−
        $proc = sprintf(CHAMAR_PROCEDEMENTO_CON_PARAMETROS,valor_numérico,'valor_cadea');
+
::<syntaxhighlight lang="php">
−
        $proc = sprintf("CALL %s",$proc);
+
$paxinaActual = filter_input(
−
        if ($conex->multi_query($proc)){
+
INPUT_GET,
−
            $result = $conex->store_result();
+
'paxina',
−
            if ($result && ($result->num_rows>0)){
+
FILTER_VALIDATE_INT
−
                ................
+
);
−
            }
 
−
        }
 
  
−
</syntaxhighlight>
+
if (
 +
$paxinaActual === false ||
 +
$paxinaActual === null ||
 +
$paxinaActual < 1
 +
) {
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
+
$paxinaActual = 1;
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
  
 +
} </syntaxhighlight>
  
 +
* `htmlspecialchars()` non serve para validar que un dato sexa numérico.
  
−
<br />
+
: Neste caso empregamos `FILTER_VALIDATE_INT`.
−
====Empregando PDO====
 
  
−
: En PDO, a preparación da chamada se fai coma sempre:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
$proc = $conex->prepare("CALL NOME_PROCEDEMENTO(:param_num, :param_text)");
 
−
</syntaxhighlight>
 
  
−
: Neste exemplo estou a supoñer que o procedemento leva dous parámetros de entrada, un de tipo numérico e outro de tipo cadea.
+
<br />
 +
'''Número total de páxinas'''
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="6,7" >
+
::<syntaxhighlight lang="php">
−
// Valores dos parámetros
+
$totalPaxinas = (int) ceil(
−
    $valor_num = 42;
+
$numLibros /
−
    $valor_text = "exemplo";
+
CONST_NUM_LIBROS_PAXINA
 +
); </syntaxhighlight>
  
−
    // Asignamos valores e tipos
+
* Podemos evitar que o usuario solicite unha páxina superior á existente:
−
    $proc->bindValue(':param_num', $valor_num, PDO::PARAM_INT);
 
−
    $proc->bindValue(':param_text', $valor_text, PDO::PARAM_STR);
 
  
−
    // Executamos o procedemento
+
::<syntaxhighlight lang="php">
−
    $proc->execute();
+
if (
−
    while ($fila = $proc->fetch(PDO::FETCH_ASSOC)) {
+
$totalPaxinas > 0 &&
−
      .............
+
$paxinaActual > $totalPaxinas
−
    }
+
) {
−
</syntaxhighlight>  
 
−
: Emprego bindValue para pasarelle o valor xunto có tipo de dato.
 
−
: Se queremos unha forma máis sinxela podemos facelo como ata o de agora e enviar os valores no execute pero teríamos que <u>estar seguros de que estamos a enviar o dato có tipo correcto</u>.
 
  
 +
$paxinaActual = $totalPaxinas;
  
 +
} </syntaxhighlight>
  
  
 
<br />
 
<br />
−
==== Caso Práctico: Consulta chamando a procedementos almacenados con parámetros de entrada ====
+
'''Calcular o OFFSET'''
  
−
* 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.
+
::<syntaxhighlight lang="php">
 +
$inicio = (
 +
$paxinaActual - 1
 +
) * CONST_NUM_LIBROS_PAXINA; </syntaxhighlight>
  
−
'''Solución:'''
 
−
* '''Procedemento almacenado CONSULTAR_LIBROS_POR_AUTOR en Mysql'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS_POR_AUTOR`(IN p_autor_buscar varchar(100))
 
−
BEGIN
 
  
−
select CONCAT(apelidos,',',nome) as nome_autor,libros.id as id_libro,titulo from libros
+
<br />
−
            INNER JOIN libro_autor ON (libro_autor.libro_id=libros.id)
+
'''Consulta paxinada'''
−
            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,titulo;
 
  
−
END$$
+
* Como `LIMIT` e `OFFSET` necesitan valores numéricos, indicaremos explicitamente o tipo mediante `PDO::PARAM_INT`.
−
</syntaxhighlight>
 
  
−
: <u>Nota:</u> Para comprobar se funciona facede primeiro unha chamada dende o propio Mysql coa orde: call CONSULTAR_LIBROS_POR_AUTOR('dru');
+
::<syntaxhighlight lang="php">
−
: Fixarse como o parámetro é de tipo IN (valor por defecto, podería non poñerse nada).
+
$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
 +
);
  
−
<br />
+
$stmt->execute();
−
=====Orientado a obxectos=====
 
−
* Definimos a constante en PHP no que está definido o nome do procedemento e o parámetro que vai levar.
 
−
:'''Arquivo: bd.php'''
 
−
::<syntaxhighlight lang="java" 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.
+
$libros = $stmt->fetchAll(); </syntaxhighlight>
  
  
 +
<br />
 +
'''Amosar os resultados'''
 +
 +
::<syntaxhighlight lang="php">
 +
if ($libros) {
 +
 +
echo "<table border='1'>";
 +
 +
foreach ($libros as $libro) {
 +
 +
    echo '<tr>';
  
−
* '''Arquivo buscar_libros_por_autor_proc.php'''
+
    printf(
−
::<syntaxhighlight lang="java" enclose="div" highlight="44-47" >
+
        '<td>%s</td>',
−
<?php
+
        htmlspecialchars(
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
            $libro['titulo'],
−
ini_set('display_errors', 1);
+
            ENT_QUOTES | ENT_SUBSTITUTE,
−
ini_set('display_startup_errors', 1);
+
            'UTF-8'
−
error_reporting(E_ALL);
+
        )
 +
    );
  
−
require ('/var/www/confBD/bd.php');
+
    echo '</tr>';
 +
}
  
−
try {
+
echo '</table>';
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
    $erro="";  // Variable que a usaremos para amosar os posibles erros que poidan darse
+
} else {
  
−
?>
+
echo 'Non hai datos que amosar.';
  
−
<!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">
 
−
    <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>
 
−
        <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
+
 
−
}    // Pecha o try do principio da páxina
+
<br />
−
catch (mysqli_sql_exception $e) {
+
'''Amosar os enlaces ás páxinas'''
−
     die("Erro mysql: " . $e->getMessage());
+
 
 +
::<syntaxhighlight lang="php">
 +
echo '<div>';
 +
 
 +
for (
 +
$cont = 1;
 +
$cont <= $totalPaxinas;
 +
$cont++
 +
) {
 +
 
 +
if ($cont === $paxinaActual) {
 +
 
 +
    printf(
 +
        '<strong>%d</strong> ',
 +
        $cont
 +
    );
 +
 
 +
} else {
 +
 
 +
     printf(
 +
        "<a href='?paxina=%d'>%d</a> ",
 +
        $cont,
 +
        $cont
 +
    );
 
}
 
}
−
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)
+
echo '</div>'; </syntaxhighlight>
 +
 
  
  
 +
==== Exemplo completo de paxinación ====
  
  
  
−
<br />
+
'''Aquivo bd.php'''
−
=====Empregando PDO=====
+
::<syntaxhighlight lang="php">
−
* Definimos a constante en PHP no que está definido o nome do procedemento e o parámetro que vai levar.
+
define(
−
:'''Arquivo: bd.php'''
+
    'CONST_NUM_LIBROS_PAXINA',
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
    10
−
define('PROCEDEMENTO_CONSULTAR_LIBROS_POR_AUTOR','CONSULTAR_LIBROS_POR_AUTOR(:nome)');
+
);
−
</syntaxhighlight>  
+
::</syntaxhighlight>
  
  
 +
'''Páxina php a cargar'''
 +
::<syntaxhighlight lang="php">
  
−
* '''Arquivo buscar_libros_por_autor_proc.php'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="44-47" >
 
 
<?php
 
<?php
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
 
−
ini_set('display_errors', 1);
+
ini_set('display_errors', '1');
−
ini_set('display_startup_errors', 1);
 
 
error_reporting(E_ALL);
 
error_reporting(E_ALL);
  
−
require ('/var/www/confBD/bd.php');
+
require '/var/www/confBD/bd.php';
 +
 
  
 
try {
 
try {
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",SERVIDOR,BD);
 
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
 
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
  
−
     $erro="";   // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
     $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
 +
        ]
 +
    );
 +
 
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
    /*
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
    * Número total de libros.
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
    */
−
     <head>
+
     $stmt = $conex->query(
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
         'SELECT COUNT(*)
−
         <title>Operacións BD</title>
+
        FROM libros'
−
        <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>
+
     $numLibros = (int) $stmt->fetchColumn();
−
        <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 = $conex->prepare("CALL " . PROCEDEMENTO_CONSULTAR_LIBROS_POR_AUTOR);
 
−
                $proc->bindValue(':autor',$_POST['txtAutor'],  PDO::PARAM_STR);
 
−
                $result = $proc->execute();
 
  
−
                if ($result) {
 
−
                        $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 = $proc->fetch(PDO::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 = null;
 
−
                        echo "</table>";
 
−
                    }
 
−
                    else {
 
−
                        $erro = "Non hai resultados,";
 
−
                    }
 
−
                }
 
−
            }
 
−
       
 
−
        ?>       
 
−
        </form>
 
−
        <?php       
 
−
        if ($erro!='') {
 
−
            printf("<h3>%s</h3>",$erro);
 
−
        }
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
    /*
−
}    // Pecha o try do principio da páxina
+
    * Número total de páxinas.
−
catch (PDOException $e) {
+
    */
−
     die("Erro PDO: " . $e->getMessage());
+
     $totalPaxinas = (int) ceil(
−
} catch (Exception $e) {
+
        $numLibros /
−
    die("Erro xeral: " . $e->getMessage());
+
        CONST_NUM_LIBROS_PAXINA
−
} finally {
+
     );
−
     $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
−
</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)
 
  
 +
    /*
 +
    * Páxina solicitada polo usuario.
 +
    */
 +
    $paxinaActual = filter_input(
 +
        INPUT_GET,
 +
        'paxina',
 +
        FILTER_VALIDATE_INT
 +
    );
  
 +
    if (
 +
        $paxinaActual === false ||
 +
        $paxinaActual === null ||
 +
        $paxinaActual < 1
 +
    ) {
  
 +
        $paxinaActual = 1;
 +
    }
  
−
<br />
 
  
−
=== Consulta chamando a procedementos almacenados con parámetros de saída ===
+
    if (
 +
        $totalPaxinas > 0 &&
 +
        $paxinaActual > $totalPaxinas
 +
    ) {
  
−
* 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.
+
        $paxinaActual = $totalPaxinas;
 +
    }
  
−
* Primeiro teremos que definir o procedemento en Mysql:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="3,7" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(OUT _param1 varchar(100))
+
    /*
−
BEGIN
+
    * Primeiro rexistro da páxina.
 +
    */
 +
    $inicio = (
 +
        $paxinaActual - 1
 +
    ) * CONST_NUM_LIBROS_PAXINA;
  
−
    SELECT col2
 
−
    INTO _param1
 
−
    FROM TABOA_1
 
−
    WHERE col1=1;
 
  
−
END
+
    /*
−
</syntaxhighlight>  
+
    * Consulta dos libros da páxina actual.
 +
    */
 +
    $stmt = $conex->prepare(
 +
        'SELECT id, titulo
 +
        FROM libros
 +
        ORDER BY titulo
 +
        LIMIT :limite
 +
        OFFSET :inicio'
 +
    );
  
−
: Agora especificamos que o parámetro é de saída antepoñendo a palabra OUT ao seu nomo.
+
    $stmt->bindValue(
−
: 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.
+
        ':limite',
−
: Podemos modificar o seu valor dentro do procedemento coa orde: SET _param1=valor;
+
        CONST_NUM_LIBROS_PAXINA,
 +
        PDO::PARAM_INT
 +
    );
  
 +
    $stmt->bindValue(
 +
        ':inicio',
 +
        $inicio,
 +
        PDO::PARAM_INT
 +
    );
  
 +
    $stmt->execute();
  
−
<br />
+
    $libros = $stmt->fetchAll();
−
====Orientado a obxectos====
 
  
 +
} catch (PDOException $e) {
  
−
* Unha  vez definido teremos que construír a chamada ao procedemento enviando o nome dunha variable (para recoller o valor modificado).
+
    error_log($e->getMessage());
−
: O nome da variable terá este formato: '''@'''nomeVariable:
+
    die('Erro relacionado coa base de datos.');
−
::<syntaxhighlight lang="java" 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.
 
  
 +
} catch (Throwable $e) {
  
−
* 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.
+
    die('Produciuse un erro na aplicación.');
−
: Para recoller o dato da variable teremos que facer unha consulta SELECT da forma: SELECT @nomeVariable as valorDevolto
 
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="10,14" >
+
}
−
<?php
 
−
        define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS("@nomeVariable")');
 
  
−
        ........................
+
?>
  
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
+
<!DOCTYPE html>
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
  
−
            if (!($resultado = $conex->query("SELECT @nomeVariable as valorDevolto"))) {
+
<html lang="gl">
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
−
            else {
 
−
                $fila = $resultado->fetch_assoc();
 
−
                echo $fila['valorDevolto']);
 
−
            }
 
  
−
        }
+
<head>
−
        ........................
+
    <meta charset="UTF-8">
 +
    <title>Paxinación de libros</title>
 +
</head>
  
 +
<body>
  
−
?>
+
<?php if ($libros): ?>
  
−
</syntaxhighlight>  
+
<table border="1">
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor') // 5 e valor son datos de exemplo
+
    <?php foreach ($libros as $libro): ?>
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
  
 +
        <tr>
 +
            <td>
 +
                <?= htmlspecialchars(
 +
                    $libro['titulo'],
 +
                    ENT_QUOTES | ENT_SUBSTITUTE,
 +
                    'UTF-8'
 +
                ) ?>
 +
            </td>
 +
        </tr>
  
 +
    <?php endforeach; ?>
  
−
: <u>Nota:</u> Non fai falla facer un alias do nome da variable. Podemos empregar o propio nome no fetch_assoc().
+
</table>
−
:: Podemos poñer:
 
  
−
:::<syntaxhighlight lang="java" enclose="div" highlight="5,10" >
+
<?php else: ?>
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
  
−
            if (!($resultado = $conex->query("SELECT @nomeVariable"))) {
+
<p>Non hai libros que amosar.</p>
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
−
            else {
 
−
                $fila = $resultado->fetch_assoc();
 
−
                echo $fila['@nomVariable']);
 
−
            }
 
  
−
        }
+
<?php endif; ?>
−
</syntaxhighlight>  
 
  
 +
<?php if ($totalPaxinas > 1): ?>
  
 +
<div>
  
−
<br />
+
    <?php
−
====Empregando PDO====
+
    for (
 +
        $cont = 1;
 +
        $cont <= $totalPaxinas;
 +
        $cont++
 +
    ):
 +
    ?>
  
 +
        <?php if ($cont === $paxinaActual): ?>
  
−
* Unha  vez definido teremos que construír a chamada ao procedemento enviando o nome dunha variable (para recoller o valor modificado).
+
            <strong>
−
: O nome da variable terá este formato: '''@'''nomeVariable:
+
                <?= $cont ?>
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
            </strong>
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(@nomeVariable)');
 
−
</syntaxhighlight>  
 
  
 +
        <?php else: ?>
  
−
* 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.
+
            <a href="?paxina=<?= $cont ?>">
−
: Para recoller o dato da variable teremos que facer unha consulta SELECT da forma: SELECT @nomeVariable as valorDevolto
+
                <?= $cont ?>
 +
            </a>
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="10,14" >
+
        <?php endif; ?>
−
<?php
 
−
        define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(@nomeVariable)');
 
  
−
        ........................
+
    <?php endfor; ?>
  
−
        $stmt = $conex->prepare("CALL " . CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
+
</div>
−
        $stmt->execute();
 
−
 
 
−
        $res = $conex->query("SELECT @nomeVariable AS total")->fetch(PDO::FETCH_ASSOC);
 
−
        if ($res){
 
−
          echo $res['valorDevolto'];
 
−
        }
 
−
        ........................
 
  
 +
<?php endif; ?>
  
−
?>
+
</body>
 +
</html>
 +
</syntaxhighlight>
  
−
</syntaxhighlight>
+
==== Sobre a optimización da paxinación ====
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
+
* En xeral non debemos enviar pola URL o número total de rexistros e confiar posteriormente nese dato:
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
−
 
 
−
 
 
−
</syntaxhighlight>
 
  
 +
::<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.
  
−
<br />
+
* Como primeira solución realizaremos en cada petición:
  
−
==== Caso práctico: Consulta chamando a procedementos almacenados con parámetros de saída ====
+
::<syntaxhighlight lang="sql">
−
 
+
SELECT COUNT(*)
−
* 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.
+
FROM libros </syntaxhighlight>
−
 
 
−
 
 
−
* '''Procedemento almacenado CONSULTAR_NUM_LIBROS en Mysql'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
DELIMITER $$
 
−
 
 
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_NUM_LIBROS`(OUT p_num_libros int)
 
−
BEGIN
 
−
SELECT COUNT(*)  
 
−
INTO p_num_libros
 
−
FROM libros;
 
−
 
 
−
END
 
−
$$
 
−
</syntaxhighlight>  
 
−
 
 
−
 
 
−
: Como se pode ver leva un parámetro de saída (OUT). Indicar que podemos poñer calquera combinación de parámetros na definición dun procedemento almacenado.
 
  
 +
* Se 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 />
 
<br />
−
=====Orientado a obxectos=====
 
−
* '''Arquivo bd.php''':
 
−
* 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" enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_NUM_LIBROS','CONSULTAR_NUM_LIBROS(@numero_libros)');
 
−
</syntaxhighlight>
 
  
 +
=== Exercicio proposto ===
  
 +
* Crea unha páxina chamada `buscar_libros.php` que permita buscar libros empregando os seguintes criterios:
  
−
* '''Arquivo consultar_num_libros.php'''
+
:* '''Título''': unha caixa de texto que realizará unha busca mediante `LIKE`.
−
: 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.
+
:* '''Prezo''': un campo no que se validará que o valor introducido sexa numérico.
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
:* '''Estado''': unha lista `<select>` cos estados dispoñibles.
−
<?php
+
:* '''Autor''': unha lista `<select>` cos autores dispoñibles.
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
:* '''Criterio de ordenación''': título, prezo ou autor.
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
−
 
 
−
require ('/var/www/confBD/bd.php');
 
−
 
 
−
try {
 
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
−
 
 
−
$erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
 
−
?>
 
−
<!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
 
−
            $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"))) {
 
−
                    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>
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
 
 
−
=====Empregando PDO=====
 
−
* '''Arquivo bd.php''':
 
−
* 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" enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_NUM_LIBROS','CONSULTAR_NUM_LIBROS(@numero_libros)');
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
* '''Arquivo consultar_num_libros.php'''
 
−
: 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" enclose="div" highlight="" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
−
 
 
−
require ('/var/www/confBD/bd.php');
 
−
 
 
−
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);
 
−
 
 
−
    $erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
 
−
?>
 
−
 
 
−
 
 
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
       
 
−
    </head>
 
−
 
 
−
    <body>
 
−
 
 
−
        <?php
 
−
            $stmt = $conex->prepare("CALL " . PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
 
−
            $stmt->execute();
 
−
       
 
−
            $proc= sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
 
−
            $res = $conex->query("SELECT @numero_libros AS valorDevolto")->fetch(PDO::FETCH_ASSOC);
 
−
            if ($res){
 
−
                printf("<h1>NUMERO DE LIBROS A VENDA:%d",$res['valorDevolto']);
 
−
            }
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
−
<?php
 
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
    die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
<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.
 
−
 
 
−
: [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html Definicións de funcións dende Mysql].
 
−
 
 
−
 
 
−
* Vexamos un exemplo:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
DELIMITER $$
 
−
 
 
−
CREATE FUNCTION `EXEMPLO_FUNCION`() RETURNS int
 
−
BEGIN
 
−
    declare variable int;
 
−
 
 
−
SELECT col2
 
−
INTO variable
 
−
FROM TABLA_1
 
−
        WHERE col1=1;
 
−
 
 
−
return variable;
 
−
 
 
−
 
 
−
END
 
−
$$
 
−
</syntaxhighlight>
 
−
 
 
−
: Como vemos, esta función vai devolver un número de tipo integer.
 
−
 
 
−
 
 
−
* Para chamar á función dende Mysql teremos que 'engadila' dentro dunha cláusula SELECT.
 
−
::<syntaxhighlight lang="java" 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 />
 
−
====Orientado a obxectos====
 
−
* 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.
 
−
 
 
−
 
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="19" >
 
−
 
 
−
<?php
 
−
 
 
−
define('FUNCION_EXEMPLO_MYSQL','select EXEMPLO_FUNCION() as valor_devolto');
 
−
 
 
−
$conex = new mysqli('SERVIDOR','USUARIO','PASSWORD','BD');
 
−
$conex->set_charset('utf8');
 
−
 
 
−
if ($conex->connect_error){
 
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
−
 
 
−
?>
 
−
 
 
−
 
 
−
        <?php
 
−
            $result=$conex->query(FUNCION_EXEMPLO_MYSQL);
 
−
            if ($result || ($result->num_rows>0)){
 
−
                $row = $result->fetch_assoc();
 
−
                printf("<h1>Dato devolto:%d</h1>",$row['valor_devolto']);
 
−
                $result->free();
 
−
            }
 
−
            else {
 
−
                echo "Fallou a chamada á función: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
−
 
 
−
       
 
−
            $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.
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
====Empregando PDO====
 
−
 
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="19,26" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
−
 
 
−
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);
 
−
 
 
−
?>
 
−
 
 
−
 
 
−
<?php
 
−
    define('FUNCION_EXEMPLO_MYSQL','select EXEMPLO_FUNCION() as valor_devolto');
 
−
 
 
−
    $stmt = $conex->prepare(FUNCION_EXEMPLO_MYSQL);
 
−
 
 
−
    // Executar
 
−
    $stmt->execute();
 
−
    $res = $stmt->fetch(PDO::FETCH_ASSOC);
 
−
    echo "O resultado da función é: " . $res['valor_devolto'];
 
−
    $stmt = null;
 
−
 
 
−
} catch (PDOException $e) {
 
−
    die("Erro PDO: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null;
 
−
}
 
−
 
 
−
?>       
 
−
</syntaxhighlight>
 
−
 
 
−
:<u>Nota:</u> Fixarse como na liña 26 a referencia que utilizamos é o alias posto no DEFINE na liña 19.
 
−
 
 
−
 
 
−
* Se a función levara parámetros teríamos que pasalos como antes nos procedementos almacenados:
 
−
 
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
// Chamamos á función usando SELECT
 
−
    $stmt = $conex->prepare("SELECT Suma(:a, :b) AS resultado");
 
−
 
 
−
    // Parametros
 
−
    $stmt->bindValue(':a', 5, PDO::PARAM_INT);
 
−
    $stmt->bindValue(':b', 7, PDO::PARAM_INT);
 
−
 
 
−
    // Executar
 
−
    $stmt->execute();
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
==== Caso Práctico: Consulta chamando a funcións ====
 
−
 
 
−
* Imos facer un exemplo aplicado ao noso proxecto.
 
−
: 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="java" enclose="div" highlight="" >
 
−
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$$
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
=====Orientado a obxectos=====
 
−
* '''Aqruivo bd.php'''
 
−
: Definimos a constante que vai chamar á función.
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define('FUNCION_CONSULTAR_NUM_LIBROS','select CONSULTAR_NUM_LIBROS() as num_libros');
 
−
</syntaxhighlight>
 
−
 
 
−
: Fixarse no alias da chamada á función.
 
−
 
 
−
 
 
−
* '''Creamos unha páxina consultar_num_libros_con_funcionMYSQL.php'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
−
 
 
−
require ('/var/www/confBD/bd.php');
 
−
 
 
−
try {
 
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
−
 
 
−
$erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
 
−
?>
 
−
<!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(FUNCION_CONSULTAR_NUM_LIBROS);
 
−
        if ($result && ($result->num_rows>0)){
 
−
            $row = $result->fetch_assoc();
 
−
            printf("<h1>N&uacute;mero de libros:%d</h1>",$row['num_libros']);
 
−
            $result->free();
 
−
        }
 
−
        else {
 
−
            echo "Fallou a chamada á función.: (" . $conex->errno . ") " . $conex->error;
 
−
        }
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
−
<?php
 
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
=====Empregando PDO=====
 
−
* '''Aqruivo bd.php'''
 
−
: Definimos a constante que vai chamar á función.
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define('FUNCION_CONSULTAR_NUM_LIBROS','select CONSULTAR_NUM_LIBROS() as num_libros');
 
−
</syntaxhighlight>
 
−
 
 
−
: Fixarse no alias da chamada á función.
 
−
 
 
−
 
 
−
* '''Creamos unha páxina consultar_num_libros_con_funcionMYSQL.php'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
−
 
 
−
require('/var/www/confBD/bd.php');
 
−
 
 
−
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);
 
−
 
 
−
?>
 
−
 
 
−
 
 
−
    <!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
 
−
 
 
−
        $stmt = $conex->prepare(FUNCION_CONSULTAR_NUM_LIBROS);
 
−
        $stmt->execute();
 
−
 
 
−
        $res = $stmt->fetch(PDO::FETCH_ASSOC);
 
−
        if ($res) {
 
−
            printf("<h1>N&uacute;mero de libros:%d</h1>", $res['num_libros']);
 
−
        }
 
−
        ?>
 
−
    </body>
 
−
 
 
−
    </html>
 
−
 
 
−
<?php
 
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
    die("Erro PDO: " . $e->getMessage());
 
−
} catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
 
 
−
<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.
 
−
 
 
−
 
 
−
* A idea é moi simple. Utilizando a cláusula [https://dev.mysql.com/doc/refman/5.5/en/limit-optimization.html '''LIMIT''' da orde SQL Select].
 
−
: Dita cláusula pode levar dous parámetros separados por coma: LIMIT numero_fila,tamaño e vai ao final da orde SELECT.
 
−
 
 
−
: Por exemplo:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="4" >
 
−
SELECT id,titulo
 
−
FROM libros
 
−
ORDER by titulo
 
−
LIMIT 0,2
 
−
</syntaxhighlight>
 
−
 
 
−
:: Este exemplo amosaría os dous primeiros rexistros empezando polo primeiro do conxunto de resultados (rexistros 0 e 1)
 
−
 
 
−
: Se modifico a orde e poño:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="4" >
 
−
SELECT id,titulo
 
−
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 !!!!
 
−
 
 
−
 
 
−
* 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.
 
−
 
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
==== 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).
 
−
 
 
−
 
 
−
* Aplicado ao noso exemplo, imos engadir máis datos para poder facer unha paxinación.
 
−
 
 
−
: '''Táboa autores'''
 
−
 
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('ABBOTT', 'RACHEL');
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('HJORTH', 'MICHAEL');
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('LAURENS', 'STEPHANIE');
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('ESTRINGANA', 'MORUENA');
 
−
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('NIXON', 'RITA');
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
: '''Táboa libros'''
 
−
 
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('COMO UNA EXTRAÑA');
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('CRÍMENES DUPLICADOS');
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('A SALVO CON SU CAPTOR');
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('MI ERROR FUE NO SER YO MISMA. PARTE II');
 
−
INSERT INTO `php`.`libros` (`titulo`) VALUES ('EL AMOR NO ES UN INVENTO DE LOS POETAS');
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
: '''Táboa libro_autor'''
 
−
:: Teredes que ter en conta os identificadores autonuméricos xerados nos insert´s anteriores.
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('3', '2');
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('4', '3');
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('5', '4');
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('6', '5');
 
−
INSERT INTO `php`.`libro_autor` (`libro_id`, `autor_id`) VALUES ('7', '6');
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
: '''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>
 
−
 
 
−
 
 
−
* Creamos o procedemento almacenado en Mysql para que devolva os libros con paxinación.
 
−
: Poderíamos modificar o que xa tiñamos, comprobando se enviamos valores aos parámetros ou non (valor null) e facendo un select ou outro en función dos valores...
 
−
 
 
−
'''Procedemento almacenado CONSULTAR_LIBROS_PAXINACION en Mysql'''
 
−
::<syntaxhighlight lang="java" 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$$
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
* Creamos a constante que vai facer uso deste procedemento.
 
−
'''Arquivo: bd.inc.php'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_LIBROS_PAXINACION','CONSULTAR_LIBROS_PAXINACION(%d,%d)');
 
−
</syntaxhighlight>
 
−
 
 
−
:: Como vemos imos enviar dous parámetros.
 
−
 
 
−
 
 
−
 
 
−
 
 
−
* 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'''
 
−
: 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
 
−
 
 
−
 
 
−
: '''Arquivo: consultar_libros_con_paxinacion.php'''
 
−
 
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="8,31-42,48-68,71-72" >
 
−
<?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');
 
−
define('CONST_NUM_LIBROS_PAXINA',2);    // Podería ir en bd.inc.php ou noutro arquivo inc.php se vai ser empregada por outras páxinas
 
−
 
 
−
 
 
−
try {
 
−
    $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"
 
−
    "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
 
−
                // 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'];
 
−
                    }
 
−
                }
 
−
            ?>
 
−
 
 
−
           
 
−
            <?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.
 
−
 
 
  
 +
* 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.
 
  
 +
== Resumo das funcións PDO empregadas ==
  
 +
::<syntaxhighlight lang="php">
 +
! Método / constante
  
−
* Posible solución enviando o número total de libros na URL:
+
| ! 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>
  
−
'''Arquivo consultar_libros_con_paxinacion.php'''
+
== Regras que empregaremos nos exames ==
−
::<syntaxhighlight lang="java" enclose="div" highlight="9-11,47" >
 
−
.......................
 
  
 +
# As operacións contra a base de datos realizaranse mediante '''PDO'''.
  
 +
# Para consultas sen parámetros externos poderá empregarse `query()`.
  
−
            <?php
+
# Se hai datos procedentes do usuario ou outros valores externos empregarase `prepare()` + `execute()`.
−
                // 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>
+
# Non se concatenarán directamente datos externos nas sentenzas SQL.
  
 +
# Os datos recibidos mediante formularios ou URL deberán validarse.
  
 +
# Cando o tipo sexa relevante empregarase `bindValue()` indicando `PDO::PARAM_INT`, `PDO::PARAM_STR`, etc.
  
−
<br />
+
# Non se empregará `rowCount()` para coñecer o número de filas devoltas por un `SELECT`.
−
* 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'''
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="1-4,9,21,29" >
 
−
<?php
 
−
  session_start();  // Iría ao principio de todo
 
−
 
 
−
?>
 
−
.......................
 
−
 
 
−
 
 
−
            <?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].
 
−
 
  
  

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