Diferencia entre revisiones de «PHP UD3 OP CONSULTA»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 41 ediciones intermedias del mismo usuario)
Línea 20: Línea 20:
  
  
 +
* '''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 ===
 
=== Realización dunha consulta SELECT ===
Línea 166: Línea 176:
  
  
−
* Se realmente necesitamos coñecer o número de rexistros, realizaremos unha consulta específica empregando COUNT(*):
+
* [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">
 
::<syntaxhighlight lang="php">
 
$stmt = $conex->query(
 
$stmt = $conex->query(
Línea 245: Línea 257:
  
 
<br />
 
<br />
 +
 
=== Recuperación de filas ===
 
=== Recuperación de filas ===
 
'''Paso 4: Agora necesitaremos percorrer dende o primeiro rexistro ata o último amosando o resultado.'''  
 
'''Paso 4: Agora necesitaremos percorrer dende o primeiro rexistro ata o último amosando o resultado.'''  
Línea 523: Línea 536:
 
'''Arquivo bd.php'''
 
'''Arquivo bd.php'''
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
 +
 +
define('CARTAFOL_IMAXES_WEB', 'http://localhost/imaxes/');
 +
 
define(
 
define(
 
'CONSULTAR_EXEMPLARES_POR_LIBRO',
 
'CONSULTAR_EXEMPLARES_POR_LIBRO',
Línea 535: Línea 551:
 
);
 
);
 
</syntaxhighlight>
 
</syntaxhighlight>
−
'''Código PHP'''
 
−
::<syntaxhighlight lang="php">
 
−
$libroId = filter_input(
 
−
INPUT_POST,
 
−
'lstLibros',
 
−
FILTER_VALIDATE_INT
 
−
);
 
−
if (
 
−
$libroId !== false &&
 
−
$libroId !== null &&
 
−
$libroId !== -1
 
−
) {
 
−
$stmt = $conex->prepare(
 
−
    CONSULTAR_EXEMPLARES_POR_LIBRO
 
−
);
 
  
−
$stmt->bindValue(
 
−
    ':libro_id',
 
−
    $libroId,
 
−
    PDO::PARAM_INT
 
−
);
 
  
−
$stmt->execute();
 
  
−
$fila = $stmt->fetch();
+
<br />
 +
* '''Código PHP'''
 +
::<syntaxhighlight lang="php">
 +
<html>
  
−
if ($fila !== false) {
+
<head>Páxina de exemplo
 +
</head>
  
−
    echo "<table border='1'>";
+
<body>
−
    echo '<tr>';
 
−
    echo '<th>Num. exemplar</th>';
 
−
    echo '<th>Imaxe</th>';
 
−
    echo '<th>Prezo</th>';
 
−
    echo '<th>Estado</th>';
 
−
    echo '</tr>';
 
  
−
     do {
+
     <?php
 +
    ini_set('display_errors', 1);
 +
    ini_set('display_startup_errors', 1);
 +
    error_reporting(E_ALL);
  
−
        echo '<tr>';
+
    require('/var/www/confBD/bd.php');
 +
    try {
  
−
         printf(
+
         $dsn = sprintf(
−
             '<td>%d</td>',
+
             'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
             (int) $fila['id_exemplar']
+
             SERVIDOR,
 +
            BD
 
         );
 
         );
  
−
         printf(
+
         $opciones = [
−
             "<td><img alt='%s' src='%s' width='42' height='42'></td>",
+
             PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
             htmlspecialchars(
+
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
                $fila['imaxe'] ?? '',
+
             PDO::ATTR_EMULATE_PREPARES  => false
−
                ENT_QUOTES | ENT_SUBSTITUTE,
+
        ];
−
                'UTF-8'
+
 
−
             ),
+
        $conex = new PDO(
−
             htmlspecialchars(
+
             $dsn,
−
                CARTAFOL_IMAXES_WEB . ($fila['imaxe'] ?? ''),
+
             USUARIO,
−
                ENT_QUOTES | ENT_SUBSTITUTE,
+
            PASSWORD,
−
                'UTF-8'
+
             $opciones
−
             )
 
 
         );
 
         );
  
−
         printf(
+
         $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...
−
            '<td>%.2f</td>',
+
        $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)
−
             (float) ($fila['prezo'] ?? 0)
+
 
−
        );
+
        if (
 +
            $libroId !== false &&
 +
            $libroId !== null &&
 +
            $libroId !== -1
 +
        ) {
 +
            $stmt = $conex->prepare(
 +
                CONSULTAR_EXEMPLARES_POR_LIBRO
 +
             );
 +
 
 +
            $stmt->bindValue(
 +
                ':libro_id',
 +
                $libroId,
 +
                PDO::PARAM_INT
 +
            );
 +
 
 +
            $stmt->execute();
  
−
        printf(
+
             $fila = $stmt->fetch();
−
             '<td>%d</td>',
 
−
            (int) $fila['estado_id']
 
−
        );
 
  
−
        echo '</tr>';
+
            if ($fila !== false) {
  
−
    } while ($fila = $stmt->fetch());
+
                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>';
  
−
    echo '</table>';
+
                do {
  
−
} else {
+
                    echo '<tr>';
  
−
    echo 'Non hai datos que amosar';
+
                    printf(
 +
                        '<td>%d</td>',
 +
                        (int) $fila['id_exemplar']
 +
                    );
  
−
}
+
                    printf(
−
}
+
                        "<td><img alt='%s' src='%s' width='42' height='42'></td>",
−
</syntaxhighlight>
+
                        htmlspecialchars(
 +
                            $fila['imaxe'] ?? '',
 +
                            ENT_QUOTES | ENT_SUBSTITUTE,
 +
                            'UTF-8'
 +
                        ),
 +
                        htmlspecialchars(
 +
                            CARTAFOL_IMAXES_WEB . ($fila['imaxe'] ?? ''),
 +
                            ENT_QUOTES | ENT_SUBSTITUTE,
 +
                            'UTF-8'
 +
                        )
 +
                    );
  
−
== Procedementos almacenados empregando PDO ==
+
                    printf(
−
Os procedementos almacenados execútanse mediante a sentenza SQL CALL.
+
                        '<td>%.2f</td>',
−
: Por exemplo:
+
                        (float) ($fila['prezo'] ?? 0)
−
::<syntaxhighlight lang="php">
+
                    );
 +
 
 +
                    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>
 +
</syntaxhighlight>
 +
 
 +
== Procedementos almacenados empregando PDO ==
 +
Os procedementos almacenados execútanse mediante a sentenza SQL CALL.
 +
: Por exemplo:
 +
::<syntaxhighlight lang="php">
 
$stmt = $conex->prepare(
 
$stmt = $conex->prepare(
 
'CALL CONSULTAR_LIBROS()'
 
'CALL CONSULTAR_LIBROS()'
Línea 694: Línea 754:
 
$stmt = $conex->prepare(
 
$stmt = $conex->prepare(
 
'CALL PROCEDEMENTO_CON_PARAMETROS(
 
'CALL PROCEDEMENTO_CON_PARAMETROS(
−
,
+
:dato1,:dato2
  
 
)'
 
)'
 
);
 
);
 
$stmt->bindValue(
 
$stmt->bindValue(
−
'',
+
':dato1',
 
$valorNum,
 
$valorNum,
 
PDO::PARAM_INT
 
PDO::PARAM_INT
 
);
 
);
 
$stmt->bindValue(
 
$stmt->bindValue(
−
'',
+
':dato2',
 
$valorTexto,
 
$valorTexto,
 
PDO::PARAM_STR
 
PDO::PARAM_STR
Línea 711: Línea 771:
 
</syntaxhighlight>
 
</syntaxhighlight>
 
: Desta forma non necesitamos construír a chamada mediante sprintf() nin concatenar os valores dentro da sentenza SQL.
 
: 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 ===
 
=== 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'''.
 
PDO define mecanismos xenéricos para parámetros de saída, pero o controlador '''PDO_MYSQL non xestiona correctamente PDO::PARAM_INPUT_OUTPUT'''.
Línea 762: Línea 823:
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
 
$stmt = $conex->prepare(
 
$stmt = $conex->prepare(
−
'SELECT Suma(, )'
+
'SELECT Suma(:dato1,:dato2 )'
 
);
 
);
 
$stmt->bindValue(
 
$stmt->bindValue(
−
'',
+
':dato1',
 
5,
 
5,
 
PDO::PARAM_INT
 
PDO::PARAM_INT
 
);
 
);
 
$stmt->bindValue(
 
$stmt->bindValue(
−
'',
+
'':dato2,
 
7,
 
7,
 
PDO::PARAM_INT
 
PDO::PARAM_INT
Línea 778: Línea 839:
 
</syntaxhighlight>
 
</syntaxhighlight>
 
: fetchColumn() resulta especialmente cómodo cando a consulta devolve un único valor.
 
: fetchColumn() resulta especialmente cómodo cando a consulta devolve un único valor.
 +
 
== Paxinación de resultados empregando PDO ==
 
== Paxinación de resultados empregando PDO ==
 
Para paxinar resultados empregaremos:
 
Para paxinar resultados empregaremos:
Línea 791: Línea 853:
 
</syntaxhighlight>
 
</syntaxhighlight>
 
: Esta consulta devolvería ata 10 rexistros empezando desde o rexistro situado na posición 20.
 
: Esta consulta devolvería ata 10 rexistros empezando desde o rexistro situado na posición 20.
−
=== Número total de rexistros ===
+
 
 +
 
 +
 
 +
<br />
 +
'''Número total de rexistros'''
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
 
$stmt = $conex->query(
 
$stmt = $conex->query(
Línea 799: Línea 865:
 
$numLibros = (int) $stmt->fetchColumn();
 
$numLibros = (int) $stmt->fetchColumn();
 
</syntaxhighlight>
 
</syntaxhighlight>
−
=== Páxina actual ===
+
 
 +
 
 +
 
 +
<br />
 +
'''Páxina actual'''
 
O número de páxina recibido mediante GET é un dato externo e debe validarse.
 
O número de páxina recibido mediante GET é un dato externo e debe validarse.
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
Línea 816: Línea 886:
 
</syntaxhighlight>
 
</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.
 
: 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 ===
+
 
 +
 
 +
<br />
 +
'''Cálculo do número de páxinas'''
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
 
define(
 
define(
Línea 835: Línea 908:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
−
=== Calcular o desprazamento ===
+
 
 +
 
 +
<br />
 +
'''Calcular o desprazamento'''
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
 
$inicio = (
 
$inicio = (
Línea 841: Línea 917:
 
) * CONST_NUM_LIBROS_PAXINA;
 
) * CONST_NUM_LIBROS_PAXINA;
 
</syntaxhighlight>
 
</syntaxhighlight>
−
=== Consulta paxinada ===
+
 
 +
: Por exemplo, se estamos:
 +
:* Na páxina 1 => inicio = 0
 +
:* Na páxina 2 => inicio = 10
 +
:* Na páxina 3 => inicio = 20
 +
 
 +
 
 +
<br />
 +
'''Consulta paxinada'''
 
Neste caso indicaremos explicitamente que os parámetros son enteiros:
 
Neste caso indicaremos explicitamente que os parámetros son enteiros:
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
Línea 848: Línea 932:
 
FROM libros
 
FROM libros
 
ORDER BY titulo
 
ORDER BY titulo
−
LIMIT
+
LIMIT :limite
−
OFFSET '
+
OFFSET :desprazamento'
 
);
 
);
 
$stmt->bindValue(
 
$stmt->bindValue(
−
'',
+
':limite',
 
CONST_NUM_LIBROS_PAXINA,
 
CONST_NUM_LIBROS_PAXINA,
 
PDO::PARAM_INT
 
PDO::PARAM_INT
 
);
 
);
 
$stmt->bindValue(
 
$stmt->bindValue(
−
'',
+
':desprazamento',
 
$inicio,
 
$inicio,
 
PDO::PARAM_INT
 
PDO::PARAM_INT
Línea 864: Línea 948:
 
$libros = $stmt->fetchAll();
 
$libros = $stmt->fetchAll();
 
</syntaxhighlight>
 
</syntaxhighlight>
−
Despois podemos amosar os libros:
+
 
 +
 
 +
* Despois podemos amosar os libros:
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
 
if ($libros) {
 
if ($libros) {
Línea 890: Línea 976:
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
−
=== Enlaces da paxinación ===
+
 
 +
 
 +
 
 +
 
 +
<br />
 +
'''Enlaces da paxinación'''
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
 
echo '<div>';
 
echo '<div>';
Línea 916: Línea 1007:
 
echo '</div>';
 
echo '</div>';
 
</syntaxhighlight>
 
</syntaxhighlight>
−
=== Sobre a optimización do número total de rexistros ===
+
 
 +
 
 +
 
 +
<br />
 +
'''Sobre a optimización do número total de rexistros'''
 
En xeral non deberiamos enviar o número total de rexistros na URL confiando despois nese valor:
 
En xeral non deberiamos enviar o número total de rexistros na URL confiando despois nese valor:
 
::<syntaxhighlight lang="text">
 
::<syntaxhighlight lang="text">
Línea 929: Línea 1024:
 
</syntaxhighlight>
 
</syntaxhighlight>
 
: Se nunha aplicación real esta operación chegase a ser un problema de rendemento, poderían estudarse posteriormente técnicas de caché ou outras estratexias de optimización.
 
: 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 ==
 
== Xestión de excepcións ==
−
Nos exemplos completos empregaremos o seguinte esquema:
+
 
 +
 
 +
* Nos exemplos completos empregaremos o seguinte esquema:
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
 
try {
 
try {
−
// Conexión e operacións coa base de datos
+
    $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) {
 
} catch (PDOException $e) {
−
/*
+
    // Rexistrar o erro nun log (monolog, etc.)
−
* Durante o desenvolvemento podemos consultar
+
    error_log($e->getMessage());
−
* $e->getMessage().
+
   
−
*
+
    die('Erro relacionado coa base de datos.');
−
* Nunha aplicación en produción non debemos
 
−
* amosar esta información ao usuario.
 
−
*/
 
−
die(
 
−
    'Erro relacionado coa base de datos.'
 
−
);
 
 
} catch (Throwable $e) {
 
} catch (Throwable $e) {
−
die(
+
    die('Produciuse un erro na aplicación.');
−
    'Produciuse un erro na aplicación.'
 
−
);
 
−
} finally {
 
−
$conex = null;
 
 
}
 
}
 +
 +
// 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>
 
</syntaxhighlight>
−
PDOException permite tratar especificamente os erros de PDO.
+
: PDOException permite tratar especificamente os erros de PDO.
−
Throwable permite capturar outros erros ou excepcións producidos durante a execución.
+
: 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.
+
: Os bloques catch deben colocarse desde o máis específico ao máis xeral.
−
== Outros cambios que deben aplicarse nos exemplos ==
+
 
−
Cambiar:
+
 
−
::<syntaxhighlight lang="text">
+
 
−
<syntaxhighlight lang="java">
+
<br />
−
</syntaxhighlight>
+
== Exercicios prácticos ==
−
: por:
+
 
−
::<syntaxhighlight lang="text">
+
* 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`.
−
<syntaxhighlight lang="php">
+
 
−
</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.
−
: nos bloques PHP, e por:
+
 
−
::<syntaxhighlight lang="text">
+
* Os exemplos con `mysqli` poden consultarse como información adicional, pero os exemplos de referencia dos apuntes serán os realizados mediante PDO.
−
<syntaxhighlight lang="sql">
+
 
−
</syntaxhighlight>
+
* Nos exemplos completos empregaremos o seguinte esquema:
−
: nas sentenzas SQL.
+
 
−
Empregar HTML5:
+
: No arquivo bd.php que se atopa fora da ruta /var/www/html gardaremos os datos da conexión e as consultar a empregar:
−
::<syntaxhighlight lang="html">
 
−
<!DOCTYPE html>
 
−
<html lang="gl"> <head> <meta charset="UTF-8"> <title>Operacións BD</title> </head> </syntaxhighlight>
 
−
: en lugar do antigo XHTML 1.0 Transitional.
 
−
Substituír nos exemplos PDO:
 
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
−
catch (Exception $e)
+
<?php
 +
 
 +
define('SERVIDOR', 'localhost');
 +
define('USUARIO', 'user_php');
 +
define('PASSWORD', 'user_php');
 +
define('BD', 'php');
 +
 
 +
define(
 +
    'NOME_DA_CONSULTA',
 +
    'SELECT col1,col2,...
 +
    FROM taboa
 +
    ORDER BY col1'
 +
);
 
</syntaxhighlight>
 
</syntaxhighlight>
−
: por:
+
 
 +
 
 +
Nas páxinas PHP que van acceder á base de datos terán este esquema:
 
::<syntaxhighlight lang="php">
 
::<syntaxhighlight lang="php">
−
catch (Throwable $e)
+
try {
−
</syntaxhighlight>
 
−
Corrixir todas as aparicións incorrectas de:
 
−
::<syntaxhighlight lang="php">
 
−
Expcetion
 
−
</syntaxhighlight>
 
−
: por:
 
−
::<syntaxhighlight lang="php">
 
−
Exception
 
−
</syntaxhighlight>
 
−
: ou, nos exemplos novos:
 
−
::<syntaxhighlight lang="php">
 
−
Throwable
 
−
</syntaxhighlight>
 
−
Non empregar htmlentities() para validar valores recibidos mediante GET ou POST.
 
−
: A validación e o escape de saída son operacións diferentes:
 
−
::<syntaxhighlight lang="text">
 
−
Datos de entrada
 
−
↓
 
−
validación
 
−
↓
 
−
operación coa base de datos mediante PDO
 
−
↓
 
−
htmlspecialchars() ao producir HTML
 
−
</syntaxhighlight>
 
−
<br />
 
  
 +
require '/var/www/confBD/bd.php';
  
−
<br />
+
try {
−
:* '''Paso 5: Ao rematar de percorrer o resultado, liberaremos a memoria chamando á [http://php.net/manual/es/mysqli-result.free.php función free()]'''
 
  
−
::* Versión procedimental:
+
    $dsn = sprintf(
−
:::<syntaxhighlight lang="php" enclose="div" highlight="6" >
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
if ($result && (mysqli_num_rows($result)>0)){
+
        SERVIDOR,
−
  // Veñen datos
+
        BD
−
  while($row = mysqli_fetch_assoc($result)) {
+
    );
−
    printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
+
 
−
  }
+
    $conex = new PDO(
−
  mysqli_free_result($result);  // Liberamos da memoria os recursos
+
        $dsn,
−
}
+
        USUARIO,
−
else {
+
        PASSWORD,
−
  echo 'Non hai datos ou a consulta está mal feita';
+
        [
−
}
+
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
</syntaxhighlight>
+
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
 +
 
 +
    // Operacións coa base de datos...
  
  
−
::* Versión orientada a obxectos con fetch_assoc:
+
} catch (PDOException $e) {
−
:::<syntaxhighlight lang="php" enclose="div" highlight="6" >
 
−
if ($result && ($result->num_rows>0)){
 
−
  // Veñen datos
 
−
  while($row = $result->fetch_assoc()) {
 
−
    printf("%s-%s",htmlentities($row["col1"]),htmlentities($row["col2"]));
 
−
  }
 
−
  $result->free();  // Liberamos da memoria os recursos
 
−
}
 
−
else {
 
−
  echo 'Non hai datos ou a consulta está mal feita';
 
−
}
 
−
</syntaxhighlight>
 
  
  
−
::* Versión PDO:
+
// Rexistrar o erro nun log.
−
:::<syntaxhighlight lang="php" enclose="div" highlight="7" >
+
// Nunha aplicación real tamén poderían empregarse
−
// Comprobar se hai filas
+
// ferramentas como Monolog.
−
if ($result && $result->rowCount() > 0) {
+
error_log($e->getMessage());
−
  // Procesamos os datos
 
−
  while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) { // tamén pode ser PDO::FETCH_OBJ
 
−
        echo htmlentities($fila['col1']) . ' - ' . htmlentities($fila['col2']) . "<br>";
 
−
  }
 
−
  $result = null;
 
−
} else {
 
−
  echo "Non hai datos que amosar!!!!";
 
−
}
 
  
 +
die('Erro relacionado coa base de datos.');
  
−
</syntaxhighlight>
+
} catch (Throwable $e) {
  
 +
die('Produciuse un erro na aplicación.');
  
 +
} </syntaxhighlight>
  
 +
:* `PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC` fai que, por defecto, as filas recuperadas mediante `fetch()` ou `fetchAll()` sexan arrays asociativos.
 +
:* `PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION` fai que os erros producidos por PDO xeren unha excepción de tipo `PDOException`.
 +
:* `PDOException` permítenos tratar especificamente os erros relacionados con PDO.
 +
:* `Throwable` permite capturar outros erros ou excepcións producidos durante a execución da aplicación.
 +
:* Os bloques `catch` deben ordenarse desde o máis específico ao máis xeral.
 +
:* Nunha aplicación en produción non debemos amosar ao usuario a mensaxe devolta por `$e->getMessage()`. Podemos rexistrala mediante `error_log()` ou outro sistema de logs.
  
 
<br />
 
<br />
−
:* '''Paso 6: Pechamos a conexión.'''.
 
−
 
−
:: [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.]
 
  
 
=== Caso Práctico 1 ===
 
=== Caso Práctico 1 ===
−
: Imos crear unha consulta que nos devolva todos os libros da táboa LIBROS dentro dunha táboa:
 
−
: Para non ter as consultas no medio do código PHP das diferentes páxinas, imos definila no '''arquivo bd.inc.php''' creado no punto anterior.
 
−
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
define ('CONSULTAR_LIBROS','select id,titulo from libros order by titulo');
 
−
</syntaxhighlight>
 
  
 +
* Imos crear unha consulta que devolva todos os libros da táboa `LIBROS` e os amose dentro dunha táboa HTML.
  
−
* '''Normas nos nomes das constantes''':
+
* Para non ter as consultas SQL no medio do código das diferentes páxinas, definirémolas no arquivo `bd.php`.
−
:* Comenzarán cun verbo que vai poder ser: CONSULTAR, INSERIR, MODIFICAR e ELIMINAR
 
−
:* Seguirá cun guión baixo.
 
−
:* En caso de consultar os datos dunha única táboa se indicará o nome da táboa. En caso de consultar datos de varias táboas se indicará con varias palabras separadas con guión baixo que indiquen o obxectivo da consulta.
 
  
 +
'''Arquivo: bd.php'''
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_LIBROS',
 +
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo'
 +
); </syntaxhighlight>
  
 +
* Como norma para os nomes das constantes que conteñan sentenzas SQL:
  
−
* Aplicado ao exemplo que estamos a seguir...
+
:* Comezarán cun verbo que poderá ser `CONSULTAR`, `INSERIR`, `MODIFICAR` ou `ELIMINAR`.
−
:<u>Nota</u> Lembrar activar as directivas para amosar erros e quitalas cando rematedes a páxina.
+
:* A continuación empregarase un guión baixo.
 +
:* Despois indicarase o obxectivo da operación.
  
−
==== Solución Caso Práctico 1 Orientado a Obxectos====
+
==== Solución Caso Práctico 1 empregando PDO ====
  
 
'''Arquivo: consultar_libros.php'''
 
'''Arquivo: consultar_libros.php'''
−
::<syntaxhighlight lang="php" enclose="div" highlight="27-40" >
+
 
 +
::<syntaxhighlight lang="php">
 +
 
 
<?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 {
−
     $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD);   // Non indicamos o nome da bd
+
 
−
     $conex->set_charset('utf8mb4');
+
    $dsn = sprintf(
−
     $conex->select_db(BD); // O parámetro indica o nome da base de datos
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
 +
 
 +
     $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
 +
 
 +
     $stmt = $conex->query(CONSULTAR_LIBROS);
 +
 
 +
    $libros = $stmt->fetchAll();
 +
 
 +
} catch (PDOException $e) {
 +
 
 +
     error_log($e->getMessage());
 +
    die('Erro relacionado coa base de datos.');
 +
 
 +
} catch (Throwable $e) {
 +
 
 +
    die('Produciuse un erro na aplicación.');
 +
 
 +
}
  
 
?>
 
?>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
<!DOCTYPE html>
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
<html lang="gl">
−
    <head>
+
<head>
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
    <meta charset="UTF-8">
−
        <title>Operacións BD</title>
+
    <title>Operacións BD</title>
−
    </head>
+
</head>
  
−
    <body>
+
<body>
−
    <?php       
 
−
       
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->num_rows>0){
 
−
            echo "<table border='1'>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                echo '<tr>';
 
−
                printf("<td>%s</td>",htmlentities($row["titulo"]));
 
−
                echo '</tr>';
 
−
            }
 
−
            echo '</table>';
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
       
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
 
<?php
 
<?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>
+
if ($libros) {
  
 +
    echo "<table border='1'>";
  
−
* Resultado:
+
    foreach ($libros as $libro) {
−
[[Imagen:Php_BD_8.jpg|300px|center]]
 
  
 +
        echo '<tr>';
  
−
* 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.
+
        printf(
 +
            '<td>%s</td>',
 +
            htmlspecialchars(
 +
                $libro['titulo'],
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            )
 +
        );
  
 +
        echo '</tr>';
 +
    }
  
 +
    echo '</table>';
  
−
* O método fetch_assoc() move un punteiro interno á seguinte fila de resultados.
+
} else {
−
: 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]·
 
  
 +
    echo 'Non hai datos que amosar.';
  
 +
}
  
 +
?>
  
 +
</body>
 +
</html>
 +
</syntaxhighlight>
 +
 +
* Neste caso empregamos `query()` porque a sentenza SQL non recibe ningún dato externo.
  
−
<br />
+
* `fetchAll()` recupera todas as filas do resultado.
−
==== Solución Caso Práctico 1 empregando PDO====
+
 
 +
: Esta solución é adecuada cando sabemos que o número de filas que imos recuperar é pequeno.
 +
 
 +
* Se o resultado puidese ter moitas filas, sería preferible procesalas unha a unha:
 +
 
 +
::<syntaxhighlight lang="php">
 +
while ($libro = $stmt->fetch()) {
  
−
'''Arquivo: consultar_libros.php'''
+
echo htmlspecialchars(
−
::<syntaxhighlight lang="php" enclose="div" highlight="27-40" >
+
    $libro['titulo'],
−
<?php
+
    ENT_QUOTES | ENT_SUBSTITUTE,
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
    'UTF-8'
−
ini_set('display_errors', 1);
+
);
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
} </syntaxhighlight>
  
−
try {
+
* '''IMPORTANTE:''' Non empregaremos `rowCount()` para comprobar cantas filas devolveu unha sentenza `SELECT`.
−
    $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 realmente necesitamos coñecer o número de rexistros, empregaremos `COUNT(*)`.
  
−
<!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>
+
=== Caso Práctico 2 ===
−
    <?php       
+
 
−
       
+
* Imos recuperar todos os autores ordenados polos seus apelidos e nome.
−
        $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
+
* Cada autor aparecerá como un enlace co seguinte formato:
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
  die("Erro PDO: " . $e->getMessage());
 
−
} finally {
 
−
  $conex = null; // Pechar a conexión
 
−
}
 
  
−
?>
+
::<syntaxhighlight lang="html"> <a href="detalle_autor.php?id=5">
 +
Apelidos, Nome </a> </syntaxhighlight>
  
−
</syntaxhighlight>
+
* O identificador será enviado mediante a URL.
  
 +
: Que un identificador apareza na URL '''non supón por si mesmo un problema de seguridade'''.
  
−
* Resultado:
+
: O importante é validar sempre o valor recibido na páxina de destino e comprobar que o usuario ten permiso para acceder ao recurso correspondente.
−
[[Imagen:Php_BD_8.jpg|300px|center]]
 
  
 +
'''Arquivo: bd.php'''
  
−
* 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.
+
::<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 ====
  
−
<br />
+
'''Arquivo: consultar_autores.php'''
  
−
=== Caso Práctico 2 ===
+
::<syntaxhighlight lang="php">
  
−
* 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>
+
<?php
−
: 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>
+
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
 +
    );
  
−
<br />
+
    $conex = new PDO(
−
==== Solución Caso Práctico 2 orientado a obxectos====
+
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
−
: No '''arquivo bd.php''' creado no punto anterior.
+
    $stmt = $conex->query(CONSULTAR_AUTORES);
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
 
−
</syntaxhighlight>
 
  
 +
    $autores = $stmt->fetchAll();
  
 +
} catch (PDOException $e) {
  
−
<br />
+
    error_log($e->getMessage());
−
'''Páxina consultar_autores.php'''
+
    die('Erro relacionado coa base de datos.');
−
:<syntaxhighlight lang="php" enclose="div" highlight="26-40" >
+
 
−
<?php
+
} catch (Throwable $e) {
−
/* 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 {
+
}
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);    // Indico nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
  
 
?>
 
?>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
<!DOCTYPE html>
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
<html lang="gl">
−
    <head>
+
<head>
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
    <meta charset="UTF-8">
−
        <title>Operacións BD</title>
+
    <title>Autores</title>
−
    </head>
+
</head>
  
−
    <body>
+
<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
 
<?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.';
 +
 
}
 
}
−
catch (mysqli_sql_exception $e) {
+
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
 
?>
 
?>
 +
 +
</body>
 +
</html>
 
</syntaxhighlight>
 
</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 />
−
==== Solución Caso Práctico 2 empregando PDO====
 
  
−
: No '''arquivo bd.php''' creado no punto anterior.
+
=== Consulta enviando parámetros de tipo numérico ===
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
 
−
</syntaxhighlight>
 
  
 +
* Cando unha consulta necesita recibir datos externos empregaremos unha '''sentenza preparada''' mediante:
  
 +
::<syntaxhighlight lang="php">
 +
prepare()
 +
execute() </syntaxhighlight>
  
−
<br />
+
* Por exemplo:
−
'''Páxina consultar_autores.php'''
 
−
:<syntaxhighlight lang="php" enclose="div" highlight="27-41" >
 
−
<?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 col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor </syntaxhighlight>
  
−
try {
+
* Definimos a consulta:
−
    $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">
 +
define(
 +
'CONSULTA_CON_PARAM_NUMERICO',
 +
'SELECT col1, col2
 +
FROM TABOA_1
 +
WHERE col1 = :valor'
 +
); </syntaxhighlight>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
* Podemos executala desta forma:
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
::<syntaxhighlight lang="php">
−
    <?php      
+
$stmt = $conex->prepare(
−
       
+
CONSULTA_CON_PARAM_NUMERICO
−
        $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; 
+
$stmt->execute([
−
        } else {
+
'valor' => 5
−
          echo "Non hai datos que amosar!!!!";
+
]); </syntaxhighlight>
−
        }
 
−
     
 
−
    ?>      
 
−
    </body>  
 
−
</html>
 
  
−
<?php
+
* O nome do parámetro debe coincidir co indicado na sentenza SQL.
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
  die("Erro PDO: " . $e->getMessage());
 
−
} finally {
 
−
  $conex = null; // Pechar a conexión
 
−
}
 
  
−
?>
+
: Se a consulta emprega:
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="sql">
 +
:valor </syntaxhighlight>
  
 +
: debemos empregar:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt->execute([
 +
'valor' => 5
 +
]); </syntaxhighlight>
  
−
<br />
+
==== Indicar explicitamente o tipo de dato ====
  
−
=== Consulta enviando parámetros de tipo numérico ===
+
* 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.
  
−
====Orientado a obxectos====
+
* Se queremos indicar explicitamente o tipo empregaremos `bindValue()`:
−
<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.
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
CONSULTA_CON_PARAM_NUMERICO
 +
);
  
 +
$stmt->bindValue(
 +
':valor',
 +
5,
 +
PDO::PARAM_INT
 +
);
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
+
$stmt->execute(); </syntaxhighlight>
−
: 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'''
+
* Algúns dos tipos dispoñibles son:
  
−
: No exemplo, col1 é de tipo numérico.
+
::<syntaxhighlight lang="text">
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
PDO::PARAM_INT
−
define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1=%d');
+
PDO::PARAM_STR
 +
PDO::PARAM_BOOL
 +
PDO::PARAM_NULL </syntaxhighlight>
  
−
$query = sprintf(CONSULTA_CON_PARAM_NUMERICO,valor_numérico_a_enviar);
+
* É especialmente recomendable indicar explicitamente o tipo cando este teña importancia para a consulta, por exemplo con `LIMIT` ou `OFFSET`.
−
</syntaxhighlight>
 
  
−
: Agora facemos o mesmo código que no exemplo anterior.
+
<br />
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
......
 
−
$result=$conex->query(CONSULTA_CON_PARAM_NUMERICO);
 
−
......
 
−
</syntaxhighlight>  
 
  
 +
==== 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'.
  
−
<br />
+
[[Imagen:Php_BD_9.jpg|800px|center]]
−
====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.
 
−
: 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.
 
−
: A forma de facelo é utilizando a consulta SQL cun parámetro e substituíndo dito parámetro polo valor que queiramos enviar.
 
  
−
* Así: SELECT col1, col2 FROM TABOA_1 WHERE col1= ''':valor'''
 
  
−
: No exemplo, col1 é de tipo numérico.
+
* Primeiro definimos as consultas.
−
::<syntaxhighlight lang="php" 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
+
'''Arquivo: bd.php'''
−
$stmt = $conex->prepare(CONSULTA_CON_PARAM_NUMERICO);
 
  
−
// Executar a consulta substituíndo o parámetro polo valor que queremos enviar
+
::<syntaxhighlight lang="php">
−
$stmt->execute(['valor' => 5]);
+
define(
−
// 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.
+
'CONSULTAR_LIBROS',
−
// $stmt->execute([5]);
+
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo'
 +
);
  
−
</syntaxhighlight>
+
define(
 +
'CONSULTAR_EXEMPLARES_POR_LIBRO',
 +
'SELECT libro_id,
 +
id_exemplar,
 +
imaxe,
 +
prezo,
 +
estado_id
 +
FROM exemplares
 +
WHERE libro_id = :libro_id
 +
ORDER BY estado_id'
 +
);
  
−
: 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:
+
define(
−
::<syntaxhighlight lang="php" enclose="div" highlight="5" >
+
'CARTAFOL_IMAXES_WEB',
−
define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1= :valor1');
+
'http://localhost/IMAXES/'
 +
); </syntaxhighlight>
  
−
// Preparar a consulta
+
===== Solución empregando PDO =====
−
$stmt = $conex->prepare(CONSULTA_CON_PARAM_NUMERICO);
+
: Fixarse que
−
$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
 
−
$stmt->execute();
 
  
−
</syntaxhighlight>
+
'''Arquivo: consultar_exemplares_por_libro.php'''
  
−
: Un exemplo de tipo é o boolean.
+
::<syntaxhighlight lang="php">
−
: 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:
 
  
 +
<?php
  
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
ini_set('display_errors', '1');
−
$activo = true;
+
error_reporting(E_ALL);
−
$stmt->bindValue(':activo', $activo, PDO::PARAM_BOOL);
 
−
$result = $stmt->execute();
 
−
</syntaxhighlight>
 
  
 +
require '/var/www/confBD/bd.php';
  
 +
$libros = [];
 +
$exemplares = [];
  
 +
$libroSeleccionado = filter_input(
 +
    INPUT_POST,
 +
    'lstLibros',
 +
    FILTER_VALIDATE_INT
 +
);
  
−
<br />
+
try {
  
−
==== Caso práctico: Enviando parámetros de tipo numérico ====
+
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
* Seguindo con exemplo, imos facer que cando o usuario seleccione un libro dentro dunha combobox, aparezan os exemplares do mesmo.
+
    $conex = new PDO(
−
: Para iso teremos que:
+
        $dsn,
−
:* Crear a sentencia SQL correspondente enviando un parámetro que vai ser o código do libro seleccionado.
+
        USUARIO,
−
:* Obter dito dato. Isto o faremos utilizando un formulario cun método POST.
+
        PASSWORD,
−
:* 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.
+
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
:* Manter seleccionado o libro na combo.
+
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
 +
    /*
 +
    * Obtemos os libros que se van amosar
 +
    * no elemento select.
 +
    */
 +
    $stmt = $conex->query(CONSULTAR_LIBROS);
  
−
<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.
+
    $libros = $stmt->fetchAll();
  
  
 +
    /*
 +
    * Se o usuario seleccionou un libro válido,
 +
    * consultamos os seus exemplares.
 +
    */
 +
    if (
 +
        $libroSeleccionado !== false &&
 +
        $libroSeleccionado !== null &&
 +
        $libroSeleccionado !== -1
 +
    ) {
  
−
<br />
+
        $stmt = $conex->prepare(
−
=====Orientado a obxectos=====
+
            CONSULTAR_EXEMPLARES_POR_LIBRO
−
* 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:
+
        $stmt->bindValue(
−
: 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...
+
            ':libro_id',
 +
            $libroSeleccionado,
 +
            PDO::PARAM_INT
 +
        );
  
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
        $stmt->execute();
−
<?php
+
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
        $exemplares = $stmt->fetchAll();
−
ini_set('display_errors', 1);
+
    }
−
ini_set('display_startup_errors', 1);
+
 
−
error_reporting(E_ALL);
+
} catch (PDOException $e) {
 +
 
 +
    error_log($e->getMessage());
 +
    die('Erro relacionado coa base de datos.');
  
−
require ('/var/www/confBD/bd.php');
+
} catch (Throwable $e) {
  
−
try {
+
     die('Produciuse un erro na aplicación.');
−
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
  
 +
}
  
 
?>
 
?>
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
<!DOCTYPE html>
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
 
−
     <head>
+
<html lang="gl">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
<head>
−
         <title>Operacións BD</title>
+
    <meta charset="UTF-8">
−
     </head>
+
     <title>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): ?>
  
−
    <body>
+
        <option
−
    <?php       
+
            value="<?= (int) $libro['id'] ?>"
−
       
+
             <?php
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
             if (
−
        if ($result && $result->num_rows>0){
+
                $libroSeleccionado !== false &&
−
            echo "<select name='lstLibros'>";
+
                $libroSeleccionado !== null &&
−
             echo "<option value='-1'></option>";
+
                 (int) $libro['id'] === $libroSeleccionado
−
             while($row = $result->fetch_assoc()) {
+
            ) {
−
                 printf("<option value='%d'>%s</option>",htmlentities($row["id"]),htmlentities($row["titulo"]));
+
                echo 'selected';
 
             }
 
             }
−
             echo "</select>";
+
             ?>
−
             $result->free();  // Liberamos da memoria os recursos
+
        >
−
        }
+
             <?= htmlspecialchars(
−
        else {
+
                $libro['titulo'],
−
             echo "Non hai datos que amosar!!!!";
+
                ENT_QUOTES | ENT_SUBSTITUTE,
−
        }
+
                'UTF-8'
−
       
+
             ) ?>
−
    ?>      
+
        </option>
−
    </body>  
+
 
−
</html>
+
    <?php endforeach; ?>
  
−
<?php
+
</select>
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Exception $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
−
:* Liña 32: Engadimos unha entrada en branco un valor -1 no seu value, para 'obrigar' a escoller un valor e que por defecto non apareza cun libro seleccionado.
+
</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="php" 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="php" 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="php" enclose="div" highlight="42,44,47-56" >
+
                <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.inc.php');
+
            </tr>
  
−
try {
+
        <?php endforeach; ?>
−
    $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">
+
     </table>
−
<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 else: ?>
−
    <?php      
+
 
−
       
+
    <p>Non hai exemplares que amosar.</p>
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
 
−
        if ($result && $result->num_rows>0){
+
<?php endif; ?>
−
            printf("<form action='%s' method='post'>",htmlentities($_SERVER['PHP_SELF']));
+
 
−
            echo "<select name='lstLibros' onChange='this.form.submit();'>";
+
<?php endif; ?>
−
            echo "<option value='-1'></option>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                printf("<option value='%d'>%s</option>",htmlentities($row["id"]),htmlentities($row["titulo"]));
 
−
            }
 
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
    ?>       
 
−
    <?php       
 
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
 
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
 
−
            echo "<div style='margin-top:40px'>";
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$conex->real_escape_string($_POST['lstLibros']));
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<table border='1'>";
 
−
                echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<tr>';
 
−
                    printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%.2f</td><td>%d</td>",
 
−
                            htmlentities($row["id_exemplar"]),htmlentities($row["imaxe"]),
 
−
                            CARTAFOL_IMAXES_WEB . htmlentities($row["imaxe"]),htmlentities($row["prezo"]),htmlentities($row["estado_id"]));
 
−
                    echo '</tr>';
 
−
                }
 
−
                echo '</table>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
        // $conex->close();  // Non fai falla pechar a conexión xa que esta se pecha ao final do páxina web. Noutras páxinas pode ser necesario
 
−
       
 
−
    ?>      
 
  
−
    </body>  
+
</body>
 
</html>
 
</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>
 
</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).
+
* Neste exemplo empregamos `filter_input()` con `FILTER_VALIDATE_INT` para comprobar que o dato recibido é un número enteiro válido.
−
::* Liña 44: Definimos a consulta a realizar contra a base de datos, utilizando a constante definida e substituíndo o parámetro polo código do libro seleccionado.
 
−
::* Liñas 47-52: Construímos a táboa para que amose os datos. Fixarse que na imaxe temos que definir o atributo 'alt' para que cumpra o estándar XHTML, polo que enviamos dúas veces o nome do arquivo. No dato que vai no atributo 'src' enviamos o nome do arquivo xunto coa URL onde se atopan as imaxes.
 
  
−
::* Liña 62: Non pechamos a conexión xa que esta se pecha ao final de todo. Sempre que se abra unha conexión temos que ter o código que a pecha. Pode suceder que na mesma páxina abramos e pechemos varias veces a conexión ao introducir código con include.
+
* Despois utilizamos unha sentenza preparada e:
  
 +
::<syntaxhighlight lang="php">
 +
PDO::PARAM_INT </syntaxhighlight>
  
 +
: para indicar explicitamente que o identificador é numérico.
  
 +
* Non necesitamos empregar `mysqli_real_escape_string()` nin concatenar o dato dentro da consulta.
  
−
<br />
+
==== Amosar a descrición do estado ====
−
* Intenta agora modificar o código anterior e fai que o select manteña seleccionado o libro cando aparecen os exemplares.
 
  
 +
* Podemos mellorar a consulta realizando un `JOIN` coa táboa `estados` para amosar o estado do exemplar.
  
 +
[[Imagen:Php_BD_12.jpg|800px|center]]
  
  
 +
'''Arquivo: bd.php'''
 +
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_EXEMPLARES_POR_LIBRO',
 +
'SELECT exemplares.libro_id,
 +
exemplares.id_exemplar,
 +
exemplares.imaxe,
 +
exemplares.prezo,
 +
estados.descripcion AS desc_estado
 +
FROM exemplares
 +
INNER JOIN estados
 +
ON estados.id = exemplares.estado_id
 +
WHERE exemplares.libro_id = :libro_id
 +
ORDER BY desc_estado'
 +
); </syntaxhighlight>
 +
 +
* Para amosar o estado:
 +
 +
::<syntaxhighlight lang="php">
 +
echo htmlspecialchars(
 +
$exemplar['desc_estado'],
 +
ENT_QUOTES | ENT_SUBSTITUTE,
 +
'UTF-8'
 +
); </syntaxhighlight>
  
 
<br />
 
<br />
−
: Posible solución:
 
−
:::<syntaxhighlight lang="php" 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"]));
+
==== Exercicio proposto ====
−
            }
+
 
−
            echo "</select>";
+
* Crea unha páxina chamada `consultar_exemplares_por_estado.php`.
−
            printf("</form>");
+
 
−
            $result->free();  // Liberamos da memoria os recursos
+
* Debe:
−
        }
+
 
−
        else {
+
:* Amosar os estados da táboa `ESTADOS` mediante botóns de opción (`radio`).
−
            echo "Non hai datos que amosar!!!!";
+
:* 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.
 +
 
 +
 
  
−
</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="php" 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');
+
: O usuario introducirá nunha caixa de texto o nome, os apelidos ou unha parte deles.
−
define ('CONSULTAR_AUTORES','SELECT id,apelidos,nome FROM autores order by apelidos,nome');
 
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id'
 
−
            . ' from exemplares '  // Necesita un espazo en branco antes do from para concatenar có anterior
 
−
            . ' where libro_id=%d order by titulo');  //  Necesita un espazo en branco antes do where para concatenar có anterior
 
−
define ('CARTAFOL_IMAXES_WEB','http://localhost/IMAXES/');
 
  
−
</syntaxhighlight>
+
: Por exemplo, se introduce:
  
 +
::<syntaxhighlight lang="text">
 +
gar </syntaxhighlight>
  
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
: poderían atoparse autores como `García`, `Vargas`, etc., dependendo dos datos existentes na base de datos.
  
−
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
* Para realizar a busca empregaremos:
−
<?php
+
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
:* Un formulario HTML con método `POST`.
−
ini_set('display_errors', 1);
+
:* Unha consulta SQL co operador `LIKE`.
−
ini_set('display_startup_errors', 1);
+
:* Unha '''sentenza preparada PDO''' para enviar o texto introducido polo usuario.
−
error_reporting(E_ALL);
+
:* 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.
  
−
require ('/var/www/confBD/bd.php');
+
* Ademais, o título de cada libro será un enlace que permitirá abrir unha nova xanela coa páxina:
  
−
try {
+
::<syntaxhighlight lang="text">
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD);  // Indico o nome da bd
+
detalle_libro.php </syntaxhighlight>
−
    $conex->set_charset('utf8mb4');
 
  
−
?>
+
: á que enviaremos o identificador do libro mediante `GET`.
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
: Para abrir esa páxina nunha nova xanela empregaremos JavaScript e a función `window.open()`.
−
<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>
+
: Polo tanto, o funcionamento xeral será:
−
    <?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"]));
+
::<syntaxhighlight lang="text">
−
            }
+
Usuario introduce o nome do autor
−
            echo "</select>";
+
↓
−
            printf("</form>");
+
Validamos o texto
−
            $result->free();  // Liberamos da memoria os recursos
+
↓
−
        }
+
prepare() + bindValue()
−
        else {
+
↓
−
            echo "Non hai datos que amosar!!!!";
+
Consulta con LIKE
−
        }
+
↓
−
    ?>       
+
Recuperamos os libros
−
    <?php       
+
↓
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
+
Amosamos autor e título
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
+
↓
−
            echo "<div style='margin-top:40px'>";
+
Preme sobre un título
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$conex->real_escape_string($_POST['lstLibros']));
+
↓
−
            $result=$conex->query($query);
+
window.open()
−
            if ($result && ($result->num_rows>0)){
+
↓
−
                echo "<table border='1'>";
+
detalle_libro.php?libro=ID </syntaxhighlight>
−
                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>   
+
===== Arquivo bd.php =====
−
</html>
 
  
−
<?php
+
* Engadimos a consulta que permite buscar libros polo nome ou apelidos dun autor:
−
}  // 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>
 
  
 +
::<syntaxhighlight lang="php">
 +
define(
 +
'CONSULTAR_LIBROS_POR_AUTOR',
 +
"SELECT
 +
CONCAT(
 +
autores.apelidos,
 +
', ',
 +
autores.nome
 +
) AS nome_autor,
 +
libros.id AS id_libro,
 +
libros.titulo
 +
FROM libros
 +
INNER JOIN libro_autor
 +
ON libro_autor.libro_id = libros.id
 +
INNER JOIN autores
 +
ON libro_autor.autor_id = autores.id
 +
WHERE CONCAT(
 +
autores.nome,
 +
' ',
 +
autores.apelidos
 +
) LIKE :autor
 +
ORDER BY nome_autor, libros.titulo"
 +
); </syntaxhighlight>
  
 +
* A consulta une as táboas:
  
 +
::<syntaxhighlight lang="text">
 +
libros
 +
libro_autor
 +
autores </syntaxhighlight>
  
−
<br />
+
: para obter os libros correspondentes a cada autor.
−
* '''<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.
 
  
 +
* Mediante:
  
 +
::<syntaxhighlight lang="sql">
 +
CONCAT(
 +
autores.nome,
 +
' ',
 +
autores.apelidos
 +
) </syntaxhighlight>
  
 +
: construímos unha cadea formada polo nome e os apelidos do autor.
  
 +
* Sobre esa cadea aplicamos:
  
 +
::<syntaxhighlight lang="sql">
 +
LIKE :autor </syntaxhighlight>
  
−
<br />
+
: sendo `:autor` o parámetro que enviaremos posteriormente desde PDO.
−
* 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''':
+
* O parámetro debe representar o valor completo.
−
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id,estados.descripcion as desc_estado'
 
−
            . ' from exemplares INNER JOIN estados ON (estados.id=exemplares.estado_id)'  // Atención aos espazos en branco no from. Se non os pos quedaría mal a sentenza. Lembra facer un echo da sentenza para ver que todo estea ben
 
−
            . ' where libro_id=%d order by desc_estado');  //  Necesita un espazo en branco antes do where para concatenar có anterior
 
−
</syntaxhighlight>
 
  
 +
: Polo tanto, non escribiremos:
  
 +
::<syntaxhighlight lang="sql">
 +
LIKE '%:autor%' </syntaxhighlight>
  
−
<br />
+
: senón que os caracteres `%` serán engadidos ao valor desde PHP:
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
 
−
:::<syntaxhighlight lang="php" enclose="div" highlight="14" >
+
::<syntaxhighlight lang="php">
−
        ..................
+
'%' . $textoBuscar . '%' </syntaxhighlight>
  
−
    <?php       
+
===== Solución completa =====
−
        // 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>";
 
−
        }
 
−
       
 
−
    ?>   
 
  
−
        ..................
+
'''Arquivo: buscar_libros_por_autor.php'''
  
−
</syntaxhighlight>
+
::<syntaxhighlight lang="php">
 +
<?php
  
−
* 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).
+
/*
 +
* 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';
  
  
−
* Resultado:
+
/*
 +
* Variables que utilizaremos posteriormente no HTML.
 +
*/
 +
$erro = '';
 +
$libros = [];
  
−
[[Imagen:Php_BD_12.jpg|400px|center]]
 
  
 +
/*
 +
* 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 {
  
−
<br />
+
    /*
−
=====Empregando PDO=====
+
    * Creamos o DSN.
−
* Crearemos unha nova páxina de nome '''consultar_exemplares_por_libro.php'''.
+
    */
 +
    $dsn = sprintf(
 +
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
: Modificamos o código para que aparezan os libros dentro da etiqueta <select> de html:
 
−
: 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="php" enclose="div" highlight="" >
+
    /*
−
<?php
+
    * Abrimos a conexión.
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
    *
−
ini_set('display_errors', 1);
+
    * Os datos SERVIDOR, USUARIO, PASSWORD e BD
−
ini_set('display_startup_errors', 1);
+
    * están definidos no arquivo bd.php.
−
error_reporting(E_ALL);
+
    */
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        [
 +
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
 +
        ]
 +
    );
  
−
require('/var/www/confBD/bd.php');
 
  
−
try {
+
    /*
−
     $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4", SERVIDOR, BD);
+
    * Comprobamos se o formulario foi enviado.
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
+
    */
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
     if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  
−
?>
+
        /*
 +
        * Comprobamos que se introduciu algún texto.
 +
        */
 +
        if ($textoBuscar === '') {
  
 +
            $erro = 'Debes introducir un texto para realizar a busca.';
  
−
    <!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">
+
        * Como regra da aplicación esiximos
 +
        * polo menos 3 caracteres.
 +
        */
 +
        } elseif (mb_strlen($textoBuscar) < 3) {
  
−
    <head>
+
            $erro =
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
                'A cadea a buscar debe ter polo menos 3 caracteres.';
−
         <title>Operacións BD</title>
+
         } else {
−
    </head>
 
  
−
    <body>
+
            /*
−
        <?php
+
            * Preparamos a consulta.
 +
            */
 +
            $stmt = $conex->prepare(
 +
                CONSULTAR_LIBROS_POR_AUTOR
 +
            );
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->rowCount() > 0) {
 
−
            echo "<select name='lstLibros'>";
 
−
            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;
+
             /*
−
        } else {
+
            * Engadimos os comodíns % de LIKE ao valor
−
             echo "Non hai datos que amosar!!!!";
+
            * que enviamos ao parámetro.
−
        }
+
            *
 +
            * Por exemplo:
 +
            *
 +
            * gar
 +
            *
 +
            * convértese en:
 +
            *
 +
            * %gar%
 +
            */
 +
            $stmt->bindValue(
 +
                ':autor',
 +
                '%' . $textoBuscar . '%',
 +
                PDO::PARAM_STR
 +
             );
  
  
−
        ?>
+
            /*
−
    </body>
+
            * Executamos a consulta.
 +
            */
 +
            $stmt->execute();
  
−
    </html>
 
  
−
<?php
+
            /*
−
}    // Pecha o try do principio da páxina
+
            * Recuperamos todos os resultados.
−
catch (PDOException $e) {
+
            */
−
    die("Erro PDO: " . $e->getMessage());
+
            $libros = $stmt->fetchAll();
−
} 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.
 
−
: 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:
+
            /*
−
[[Imagen:Php_BD_9.jpg|300px|center]]
+
            * 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) {
  
−
* '''Arquivo bd.php'''
+
    die('Produciuse un erro na aplicación.');
−
: Definimos nunha constate a consulta que vai obter os exemplares do libro seleccionado:
+
}
−
::<syntaxhighlight lang="php" 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="php" 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.
 
  
 +
?>
  
 +
<!DOCTYPE html>
  
−
* Para facer que cando se seleccione un elemento do lista se 'active' o formulario, existen múltiples formas, unha delas pode ser:
+
<html lang="gl">
−
:::            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:
+
    <head>
  
−
:::<syntaxhighlight lang="php" enclose="div" highlight="46,54-63,49-50" >
+
        <meta charset="UTF-8">
−
<?php
+
        <title>Buscar libros por autor</title>
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
        <script>
−
ini_set('display_errors', 1);
+
            /*
−
ini_set('display_startup_errors', 1);
+
            * Recibe a URL que queremos abrir
−
error_reporting(E_ALL);
+
            * e solicita ao navegador unha nova xanela.
 +
            */
 +
            function abrirVentana(url) {
  
−
require('/var/www/confBD/bd.php');
+
                window.open(
 +
                    url,
 +
                    '_blank',
 +
                    'popup,width=600,height=400,noopener'
 +
                );
 +
            }
 +
        </script>
 +
    </head>
  
−
try {
+
<body>
−
    $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);
 
  
−
?>
+
    <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">
  
−
     <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
     </form>
−
    <html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
  
−
     <head>
+
     <?php if ($erro !== ''): ?>
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
         <p>
−
         <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
            <?= htmlspecialchars(
−
         <?php
+
                $erro,
 +
                ENT_QUOTES | ENT_SUBSTITUTE,
 +
                'UTF-8'
 +
            ) ?>
 +
         </p>
 +
    <?php endif; ?>
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
+
    <?php if ($libros): ?>
−
        if ($result && $result->rowCount() > 0) {
+
        <table border="1">
−
            printf("<form action='%s' method='post'>",htmlentities($_SERVER['PHP_SELF']));
+
             <tr>
−
             echo "<select name='lstLibros' onChange='this.form.submit();'>";
+
                <th>Autor</th>
−
            echo "<option value='-1'></option>";
+
                 <th>Libro</th>
−
            while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
+
             </tr>
−
                 printf("<option value='%d'>%s</option>", htmlentities($row["id"]), htmlentities($row["titulo"]));
+
             <?php
−
             }
 
−
            echo "</select>";
 
−
             printf("</form>");
 
−
            $result = null;
 
−
        } else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
        ?>
 
  
−
        <?php       
+
        /*
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
+
        * Esta variable permite saber cal foi
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
+
        * o último autor que amosamos.
−
            echo "<div style='margin-top:40px'>";
+
        */
 +
            $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']
 +
                ) {
  
−
            $stmt=$conex->prepare(CONSULTAR_EXEMPLARES_POR_LIBRO);    // Temos que poñer prepare
+
                    $autorAmosar = '';
−
            $result = $stmt->execute([':libro_id' => $_POST['lstLibros']]);
+
                } else {
  
−
            if ($result) {
+
                     $autorAmosar =
−
                while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
+
                         $libro['nome_autor'];
−
                    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>
+
            ?>
 +
                <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
  
−
    </html>
+
            /*
 +
            * Gardamos o autor actual para comparalo
 +
            * co autor da seguinte fila.
 +
            */
 +
                $ultimoAutor =
 +
                    $libro['nome_autor'];
  
−
<?php
+
            endforeach;
−
}    // Pecha o try do principio da páxina
+
            ?>
−
catch (PDOException $e) {
+
        </table>
−
     die("Erro PDO: " . $e->getMessage());
+
     <?php endif; ?>
−
} catch (Exception $e) {
+
</body>
−
    die("Erro xeral: " . $e->getMessage());
+
</html>
−
} finally {
 
−
    $conex = null; // Pechar a conexión
 
−
}
 
−
?>
 
 
</syntaxhighlight>
 
</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).
+
===== Explicación da solución =====
−
::* 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.
+
* Recuperamos o texto enviado polo formulario mediante:
  
−
:: 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.
+
::<syntaxhighlight lang="php">
 +
$textoBuscar = trim(
 +
$_POST['txtAutor'] ?? ''
 +
); </syntaxhighlight>
  
 +
* 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 />
−
* Intenta agora modificar o código anterior e fai que o select manteña seleccionado o libro cando aparecen os exemplares.
+
'''Validación do dato'''
  
 +
* 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:
  
 +
::<syntaxhighlight lang="php">
 +
elseif (mb_strlen($textoBuscar) < 3) { </syntaxhighlight>
  
−
<br />
+
: A esixencia de tres caracteres é unha regra da nosa aplicación.
−
: Posible solución:
 
−
:::<syntaxhighlight lang="php" 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>
 
  
 +
: Non é unha medida para evitar inxección SQL.
  
  
  
 +
<br />
 +
'''Preparación da consulta'''
  
 +
* A consulta recibe datos procedentes do usuario, polo que debemos empregar unha sentenza preparada:
  
−
<br />
+
::<syntaxhighlight lang="php">
−
* O código completo sería o seguinte:
+
$stmt = $conex->prepare(
 +
CONSULTAR_LIBROS_POR_AUTOR
 +
); </syntaxhighlight>
  
−
'''Arquivo: bd.php''':
 
  
−
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
<br />  
−
<?php
+
'''Asociación do parámetro'''
  
−
define ('SERVIDOR','localhost');
+
* Asociamos o valor ao parámetro `:autor`:
−
define ('USUARIO','user_php');
 
−
define ('PASSWORD','user_php');
 
−
define ('BD','php');
 
  
−
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->bindValue(
−
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');
+
':autor',
−
define ('CARTAFOL_IMAXES_WEB','http://www.exames.es/IMAXES/');  
+
'%' . $textoBuscar . '%',
−
</syntaxhighlight>
+
PDO::PARAM_STR
 +
); </syntaxhighlight>
  
 +
* Indicamos:
  
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
::<syntaxhighlight lang="php">
 +
PDO::PARAM_STR </syntaxhighlight>
  
−
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
: porque estamos enviando unha cadea.
−
<?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);
 
  
−
?>
+
<br />
 +
'''Funcionamento de LIKE'''
  
 +
* Os caracteres `%` actúan como comodíns.
  
−
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
: Se o usuario introduce:
−
    <html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
  
−
    <head>
+
::<syntaxhighlight lang="text">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
gar </syntaxhighlight>
−
        <title>Operacións BD</title>
+
 
−
    </head>
+
: PHP enviará:
 +
 
 +
::<syntaxhighlight lang="text">
 +
%gar% </syntaxhighlight>
  
−
    <body>
+
* Desta forma poderán coincidir cadeas nas que `gar` apareza ao principio, no medio ou ao final.
−
        <?php
 
  
−
        $result = $conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->rowCount() > 0) {
 
−
            printf("<form action='%s' method='post'>",htmlentities($_SERVER['PHP_SELF']));
 
−
            echo "<select name='lstLibros' onChange='this.form.submit();'>";
 
−
            echo "<option value='-1'></option>";
 
−
            while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
 
−
                $cadea = '';
 
−
                if(isset($_POST['lstLibros']) && $row['id']==$_POST['lstLibros']){  // Se o libro seleccionado é igual ao id da fila, o seleccionamos en html
 
−
                    $cadea="selected='selected'";
 
−
                }
 
−
                printf("<option value='%d' %s>%s</option>",htmlentities($row["id"]),$cadea,htmlentities($row["titulo"]));
 
−
            }
 
−
            echo "</select>";
 
−
            printf("</form>");
 
−
            $result = null;
 
−
        } else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
        ?>
 
  
−
        <?php       
+
<br />
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO NA COMBO
+
'''Execución e recuperación dos resultados'''
−
        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
+
* Executamos:
−
            $result = $stmt->execute([':libro_id' => $_POST['lstLibros']]);
 
  
−
            if ($result) {
+
::<syntaxhighlight lang="php">
−
                while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
+
$stmt->execute(); </syntaxhighlight>
−
                    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>
+
* E recuperamos os resultados:
  
−
    </html>
+
::<syntaxhighlight lang="php">
 +
$libros = $stmt->fetchAll(); </syntaxhighlight>
  
−
<?php
+
* Como na conexión configuramos:
−
}    // 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>
 
  
 +
::<syntaxhighlight lang="php">
 +
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC </syntaxhighlight>
  
 +
: cada fila recuperada será un array asociativo.
  
  
 
<br />
 
<br />
−
* '''<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.
+
'''Evitar repetir o nome do autor'''
  
 +
* Pode suceder que un mesmo autor teña varios libros.
  
 +
: Por exemplo:
  
 +
::<syntaxhighlight lang="text">
 +
García, Ana | Libro A
 +
García, Ana | Libro B
 +
García, Ana | Libro C
 +
Pérez, Luís | Libro D </syntaxhighlight>
  
 +
* Queremos amosalo desta forma:
  
 +
::<syntaxhighlight lang="text">
 +
García, Ana | Libro A
 +
            | Libro B
 +
            | Libro C
 +
Pérez, Luís | Libro D </syntaxhighlight>
  
−
<br />
+
* Para iso gardamos o último autor procesado:
−
* 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="php">
−
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
$ultimoAutor = ''; </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
+
* En cada iteración comparamos:
−
            . ' where libro_id=:libro_id order by desc_estado'); //  Necesita un espazo en branco antes do where para concatenar có anterior
+
 
 +
::<syntaxhighlight lang="php">
 +
if ($ultimoAutor ===$libro['nome_autor']) {
 +
 
 +
  $autorAmosar = '';
 +
 
 +
} else {
 +
    $autorAmosar = $libro['nome_autor'];
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
* E ao rematar gardamos o autor actual:
 +
 +
::<syntaxhighlight lang="php">
 +
$ultimoAutor =
 +
$libro['nome_autor']; </syntaxhighlight>
  
  
 
<br />
 
<br />
−
'''Arquivo: consultar_exemplares_por_libro.php''':
+
'''Abrir o detalle dun libro'''
−
:::<syntaxhighlight lang="php" enclose="div" highlight="17,19" >
+
 
−
        ..................
+
* Cada título será un enlace á páxina:
 +
 
 +
::<syntaxhighlight lang="text">detalle_libro.php </syntaxhighlight>
  
−
    <?php       
+
* O identificador enviarase mediante `GET`:
−
        // 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="text">detalle_libro.php?libro=5 </syntaxhighlight>
−
            $result = $stmt->execute([':libro_id' => $_POST['lstLibros']]);
 
  
−
            if ($result) {
 
−
                while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) {
 
−
                    echo "<table border='1'>";
 
−
                    echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
 
−
                        echo '<tr>';
 
−
                        printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%.2f</td><td>%s</td>",
 
−
                                htmlentities($row["id_exemplar"]),htmlentities($row["imaxe"] ?? ''),
 
−
                                CARTAFOL_IMAXES_WEB . htmlentities($row["imaxe"] ?? ''),htmlentities($row["prezo"] ?? 0),htmlentities($row["desc_estado"]));
 
−
                        echo '</tr>';
 
−
                    }
 
−
                    echo '</table>';
 
−
   
 
−
                }
 
−
                $stmt = null;  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
            echo "</div>";
 
−
        }
 
−
    ?>   
 
  
−
        ..................
 
  
 +
* O enlace xerado será:
 +
::<syntaxhighlight lang="php">
 +
<a href="detalle_libro.php?libro=<?= (int) $libro['id_libro'] ?>" onclick="abrirVentana(this.href); return false;">
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* 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).
+
* Mediante:
  
 +
::<syntaxhighlight lang="javascript">this.href </syntaxhighlight>
  
 +
: obtemos a URL real almacenada no atributo `href`.
  
−
* Resultado:
+
* Chamamos despois á función:
−
 
 
−
[[Imagen:Php_BD_12.jpg|400px|center]]
 
  
 +
::<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 />
 +
'''Función JavaScript'''
  
−
=====Exercicio proposto=====
+
* A función empregada é:
  
−
: Crea unha páxina de nome '''consultar_exemplares_por_estado.php''' no que aparezan en forma de radiobutton os estados da táboa ESTADOS.
+
::<syntaxhighlight lang="javascript">
−
: 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).
+
function abrirVentana(url) {
−
: O radiobutton debe manterse seleccionado.
 
  
 +
window.open(
 +
    url,
 +
    '_blank',
 +
    'popup,width=600,height=400,noopener'
 +
);
  
 +
} </syntaxhighlight>
  
 +
* `window.open()` solicita ao navegador a apertura dunha nova xanela.
  
 +
* Indicamos aproximadamente:
 +
::<syntaxhighlight lang="text">
 +
width=600
 +
height=400 </syntaxhighlight>
  
 +
* A opción:
 +
::<syntaxhighlight lang="text">
 +
noopener </syntaxhighlight>
 +
: evita que a nova xanela poida acceder á xanela orixinal mediante `window.opener`.
  
 +
'''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.
  
  
  
 
<br />
 
<br />
−
'''POSIBLE SOLUCIÓN ORIENTADA A OBXECTOS''':
+
'''Vantaxe de empregar un href real'''
  
−
:<u>Nota</u>: Queda para vos facer a solución empregando PDO.
+
* Non empregamos:
 +
::<syntaxhighlight lang="html"> <a href="#"> </syntaxhighlight>
  
−
'''Arquivo bd.php'''
+
* Empregamos:
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
::<syntaxhighlight lang="html"> <a href="detalle_libro.php?libro=5"> </syntaxhighlight>
−
define ('CONSULTAR_ESTADOS','select id,descripcion from estados order by descripcion');
+
 
−
define ('CONSULTAR_EXEMPLARES_POR_ESTADO','select titulo,id_exemplar,prezo '
+
: 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.
−
        . ' from exemplares INNER JOIN libros ON (libros.id = exemplares.libro_id)'
 
−
        .                ' INNER JOIN estados ON (estados.id = exemplares.estado_id)'
 
−
        . ' where estado_id=%d order by titulo');
 
  
−
</syntaxhighlight>
 
  
−
: 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)
 
  
 +
<br />
 +
'''Páxina detalle_libro.php'''
  
−
'''Arquivo consultar_exemplares_por_estado.php'''
+
* A páxina de detalle recibirá o identificador do libro mediante:
−
::<syntaxhighlight lang="php" 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');
+
::<syntaxhighlight lang="text">$_GET['libro'] </syntaxhighlight>
  
−
try {
+
* Como é un dato externo, debemos validalo:
−
    $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"
+
::<syntaxhighlight lang="php">
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
$libroId = filter_input(
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
INPUT_GET,
−
    <head>
+
'libro',
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
FILTER_VALIDATE_INT
−
        <title>Operacións BD</title>
+
);
−
    </head>
 
  
−
    <body>
+
if (
−
    <body>
+
  $libroId === false ||
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
+
  $libroId === null ||
 +
  $libroId < 1
 +
) {
  
−
        <?php       
+
   die('Identificador de libro non válido.');
−
        $cont=0;    // Conta o número de estados impresos e cando leve múltiplo de dous salta de liña
 
−
        $result=$conex->query(CONSULTAR_ESTADOS);
 
−
        if ($result && $result->num_rows>0){
 
−
            while($row = $result->fetch_assoc()) {
 
−
                $cont++;
 
−
                if(isset($_POST['rbgrpEstado']) && $row['id']==$_POST['rbgrpEstado']){   // Se volve a cargar a páxina e ven cun estado, o seleccionamos
 
−
                    $cadea="checked='checked'";
 
−
                }
 
−
                else{
 
−
                    $cadea='';
 
−
                }
 
−
                printf("<input onclick='javascript:this.form.submit()' type='radio' name='rbgrpEstado' value='%d' %s>%s</input>",
 
−
                        $row["id"],$cadea,$row["descripcion"]);
 
−
                if(($cont%2)==0){  // Cada par cambiamos de liña
 
−
                    echo "<br />";
 
−
                }
 
−
            }
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai estados que amosar!!!!";
 
−
        }
 
−
        ?>       
 
−
        </form>
 
−
       
 
−
       
 
−
        <?php       
 
−
        // CONSULTAMOS OS EXEMPLARES DUN ESTADO SELECCIONADO
 
−
        if (!empty($_POST['rbgrpEstado']) && filter_var($_POST['rbgrpEstado'],FILTER_VALIDATE_INT)){
 
−
           
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_ESTADO,$conex->real_escape_string($_POST['rbgrpEstado']));
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<ol>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<li>';
 
−
                    printf("T&iacute;tulo:%s - Num_exemplar:%d - Prezo:%s",$row["titulo"],$row["id_exemplar"],$row["prezo"]);
 
−
                    echo '</li>';
 
−
                }
 
−
                echo '</ol>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
           
 
−
        }
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
  </body>   
 
−
</html>
 
  
 +
}
 +
</syntaxhighlight>
  
−
<?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>
 
  
  
 +
* A consulta posterior tamén deberá ser preparada:
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
  'SELECT id, titulo
 +
  FROM libros
 +
  WHERE id = :libro_id'
 +
);
  
 +
$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
 +
$stmt->execute();
  
−
* Resultado:
+
$libro = $stmt->fetch();
 +
</syntaxhighlight>
  
−
[[Imagen:Php_BD_13.jpg|400px|center]]
+
* Se queremos recuperar tamén os exemplares dese libro faremos outra consulta preparada:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT id_exemplar,imaxe,prezo,estado_id
 +
  FROM exemplares
 +
  WHERE libro_id = :libro_id
 +
  ORDER BY id_exemplar'
 +
);
  
 +
$stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT);
 +
$stmt->execute();
  
 +
$exemplares = $stmt->fetchAll();
 +
</syntaxhighlight>
  
−
<br />
+
===== Resumo do proceso =====
  
−
=== Consulta enviando parámetros de tipo cadea ===
+
::<syntaxhighlight lang="text">
−
 
+
$_POST['txtAutor']
−
 
+
↓
−
 
+
trim()
−
<br />
+
↓
−
====Orientado a obxectos====
+
validación
−
 
+
↓
−
* Cando utilicemos datos de tipo cadea, debemos lembrar poñelos entre comillas simples ('dato') na orde SQL.
+
prepare()
−
 
+
↓
−
 
+
bindValue(PDO::PARAM_STR)
−
<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].
+
↓
 +
execute()
 +
↓
 +
fetchAll()
 +
↓
 +
htmlspecialchars()
 +
↓
 +
enlace ao libro
 +
↓
 +
window.open()
 +
↓
 +
detalle_libro.php?libro=ID
 +
↓
 +
FILTER_VALIDATE_INT
 +
↓
 +
prepare() + PDO::PARAM_INT </syntaxhighlight>
  
 +
</syntaxhighlight>
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
 
−
: A forma de facelo é utilizando a consulta SQL cun parámetro e substituíndo dito parámetro polo valor que queiramos enviar.
 
  
−
* Así: SELECT col1, col2 FROM TABOA_1 WHERE col2=''''%d''''
+
* Debemos diferenciar claramente:
  
−
: No exemplo, col2 é de tipo cadea (por exemplo, varchar ou char).
+
::<syntaxhighlight lang="text">
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
define ('CONSULTA_CON_PARAM_CADEA',"SELECT col1, col2 FROM TABOA_1 WHERE col2='%s'");
 
  
−
$query = sprintf(CONSULTA_CON_PARAM_CADEA,'valor_cadea_a_enviar');
+
! Operación
−
</syntaxhighlight>
 
−
: Fixarse como o parámetro col2 vai entrecomillado e o formato do parámetro é '%s'.
 
  
 +
| ! 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>
  
−
: Agora facemos o mesmo código que no exemplo anterior.
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
......
 
−
$result=$conex->query(CONSULTA_CON_PARAM_CADEA);
 
−
......
 
−
</syntaxhighlight>
 
  
  
 +
<br />
  
 +
=== Consulta chamando a procedementos almacenados ===
  
−
<br />
+
* 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:
  
−
====Empregando PDO====
+
::<syntaxhighlight lang="sql">
 +
CALL NOME_PROCEDEMENTO() </syntaxhighlight>
  
−
* 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.
+
* Por exemplo, en MySQL:
  
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
  
 +
CREATE PROCEDURE CONSULTAR_LIBROS()
 +
BEGIN
  
−
<br />
+
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo;
  
−
==== Caso práctico: Enviando parámetros de tipo cadea ====
+
END$$
  
−
* Imos a aplicalo ao noso exemplo e imos crear unha páxina no que buscaremos libros en base a un nome do autor.
+
DELIMITER ; </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].
 
  
 +
==== Empregando PDO ====
  
−
* O usuario introducirá nunha caixa de texto o nome (ou parte del) do autor.
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL CONSULTAR_LIBROS()'
 +
);
  
 +
$stmt->execute();
  
 +
$libros = $stmt->fetchAll();
  
−
<br />
+
$stmt->closeCursor(); </syntaxhighlight>
−
=====Orientado a obxectos=====
 
−
: <u>Nota:</u> Lembra que no paso de datos temos que ter en conta os posibles 'ataques' que poidan intentar facerse. Repasar o dado [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios na wiki anteriormente].
 
  
−
:: 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'.
+
* `closeCursor()` libera os resultados pendentes asociados á chamada.
−
:: Por exemplo:
 
−
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
$cidade = "'s Hertogenbosch";
 
  
−
/* esta consulta fallará debido a que no escapa $ciudad */
+
: É especialmente recomendable ao traballar con procedementos almacenados en MySQL antes de executar outra consulta na mesma conexión.
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
 
−
    printf("Erro: %s\n", $mysqli->sqlstate);
 
−
}
 
  
−
$cidade = $mysqli->real_escape_string($cidade);
+
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
  
−
/* esta consulta con $ciudad escapada funcionará */
+
* Definimos o nome do procedemento:
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
 
−
    printf("Fila inxerida.\n");
 
−
}
 
  
−
</syntaxhighlight>
+
::<syntaxhighlight lang="php">
−
::Exemplo obtido de php.net
+
define(
 +
'PROCEDEMENTO_CONSULTAR_LIBROS',
 +
'CONSULTAR_LIBROS'
 +
); </syntaxhighlight>
  
 +
* Realizamos a chamada:
  
−
* 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:
+
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL ' .
 +
PROCEDEMENTO_CONSULTAR_LIBROS .
 +
'()'
 +
);
  
−
[[Imagen:Php_BD_10.jpg|500px|center]]
+
$stmt->execute();
  
−
:: 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.
+
$libros = $stmt->fetchAll();
  
−
:: O código sería algo parecido a isto:
+
$stmt->closeCursor(); </syntaxhighlight>
−
::<syntaxhighlight lang="php" 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);
+
* Non necesitamos comprobar o resultado de `execute()` para saber se o `SELECT` devolveu filas.
  
−
                        $last_autor=$row["nome_autor"];  // Gardamos o nome do autor para comparar có autor que veña na seguinte fila de datos.
+
: `execute()` indica se a sentenza puido executarse.
−
                    }
 
  
−
</syntaxhighlight>
+
: Para saber se temos resultados comprobamos os datos recuperados:
  
 +
::<syntaxhighlight lang="php">
 +
if ($libros) {
  
  
−
* 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).
+
// Hai resultados
  
−
:: 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,...
 
−
:: O código será o seguinte:
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="4-8" >
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
        <script type="text/javascript">
 
−
            function abrirVentana($url){
 
−
                window.open($url,'_blank','channelmode=0,status=0;directories=0,location=0,menubar=0,titlebar=0,toolbar=0,width=600,height=400');
 
−
            }
 
−
        </script>
 
−
       
 
−
    </head>
 
−
</syntaxhighlight>
 
  
 +
// Non hai resultados
  
−
:: Para chamar a esta función usaremos a etiqueta <a> e o evento onclick, coma o seguinte exemplo:
+
} </syntaxhighlight>
−
::<syntaxhighlight lang="php" enclose="div" highlight="4-8" >
 
−
<a href='#' onclick="javascript:abrirVentana('detalle_libro.php?libro=1');">Título Libro</a>
 
−
</syntaxhighlight>  
 
  
−
:: <u>Hai que ter moito coidado cando estamos a traballar con PHP-Javascript polo tema das comillas xa que teremos que 'xogar' coas simples e dobres para que se pechen correctamente</u>.
+
<br />
  
 +
=== 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:
  
−
<br />
+
::<syntaxhighlight lang="sql">
−
* '''SOLUCIÓN'''
+
DELIMITER $$
  
−
'''Arquivo: bd.inc.php'''
+
CREATE PROCEDURE PROCEDEMENTO_CON_PARAMETROS(
 +
IN p_num INT,
 +
IN p_text VARCHAR(100)
 +
)
 +
BEGIN
  
−
<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''')
+
SELECT col1, col2
−
::<syntaxhighlight lang="php" enclose="div">
+
FROM TABOA_1
−
<?php
+
WHERE col1 = p_num
 +
  AND col2 = p_text;
  
−
define ('CONSULTAR_LIBROS_POR_AUTOR',"select CONCAT(apelidos,',',nome) as nome_autor,libros.id ,titulo from libros " .
+
END$$
−
        ' INNER JOIN libro_autor ON (libro_autor.libro_id=libros.id) ' .
+
 
−
        ' INNER JOIN autores ON (libro_autor.autor_id=autores.id)' .
+
DELIMITER ; </syntaxhighlight>
−
        " where CONCAT(autores.nome,' ',autores.apelidos) like '%%%s%%' " .
 
−
        'order by nome_autor,titulo');
 
−
</syntaxhighlight>
 
  
−
: <u>Nota:</u> Fixarse como o parámetro que recibe 'like' é de tipo cadea e polo tanto vai entre comillas e leva dobre % para que escriba o % non o interprete como un parámetro do printf.
+
==== Empregando PDO ====
−
: É 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 %%.
 
  
 +
* Preparamos a chamada igual que calquera outra sentenza parametrizada:
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'CALL PROCEDEMENTO_CON_PARAMETROS(
 +
:param_num,
 +
:param_text
 +
)'
 +
);
 +
 +
$stmt->bindValue(
 +
':param_num',
 +
$valorNum,
 +
PDO::PARAM_INT
 +
);
 +
 +
$stmt->bindValue(
 +
':param_text',
 +
$valorTexto,
 +
PDO::PARAM_STR
 +
);
  
 +
$stmt->execute();
  
−
<br />
+
$filas = $stmt->fetchAll();
−
'''Arquivo buscar_libros_por_autor.php'''
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
$stmt->closeCursor(); </syntaxhighlight>
  
−
try {
+
* Desta forma non necesitamos empregar `sprintf()` nin concatenar os valores dentro da chamada SQL.
−
    $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.
+
==== Caso Práctico: buscar libros por autor ====
−
?>
 
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
* Lembrar que o usuario que se conecta á base de datos dende php ten que ter permiso execute sobre a base de datos.
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
::<syntaxhighlight lang="sql">
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
grant execute on php.* to 'user_php'@'localhost';
−
    <head>
+
flush privileges;
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
</syntaxhighlight>
−
        <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
+
* Procedemento almacenado (isto tedes que executalo en Mysql empregando a ferramenta Mysql WorkBench ou PhpMyAdmin):
−
}  // 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>
 
  
−
* Fixarse que nesta páxina non necesitamos conectar ao principio, xa que soamente se carga a caixa de texto para que o usuario introduza o nome do autor a buscar.
+
::<syntaxhighlight lang="sql">
−
:: Polo tanto a conexión e o peche da base de datos se podería poñer dentro do else da liña 43.
+
DELIMITER $$
−
* 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.
 
  
 +
CREATE PROCEDURE CONSULTAR_LIBROS_POR_AUTOR(
 +
IN p_autor_buscar VARCHAR(100)
 +
)
 +
BEGIN
  
−
* '''Resultado:'''
+
SELECT CONCAT(
−
[[Imagen:Php_BD_11.jpg|500px|center]]
+
          autores.apelidos,
 +
          ', ',
 +
          autores.nome
 +
      ) AS nome_autor,
 +
      libros.id AS id_libro,
 +
      libros.titulo
 +
FROM libros
 +
INNER JOIN libro_autor
 +
    ON libro_autor.libro_id = libros.id
 +
INNER JOIN autores
 +
    ON libro_autor.autor_id = autores.id
 +
WHERE CONCAT(
 +
          autores.nome,
 +
          ' ',
 +
          autores.apelidos
 +
      ) LIKE CONCAT(
 +
          '%',
 +
          p_autor_buscar,
 +
          '%'
 +
      )
 +
ORDER BY nome_autor, libros.titulo;
  
 +
END$$
  
 +
DELIMITER ; </syntaxhighlight>
  
−
<br />
+
* Chamada desde PDO cambiando no exercicio anterior a chamada ao SELECT pola chamada a este procedemento almacenado:
  
−
<br />
+
::<syntaxhighlight lang="php" highlight="23-25,58">
−
=====Empregando PDO=====
+
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  
−
* Imos crear unha páxina no que buscaremos libros en base a un nome do autor.  
+
        /*
−
:: A busca farase utilizando a [http://www.w3schools.com/sql/sql_like.asp orde SQL '''like'''], que busca patróns de cadea. Este operador non distingue maiúsculas de minúsculas.
+
        * Comprobamos que se introduciu algún texto.
−
:: 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].
+
        if ($textoBuscar === '') {
  
 +
            $erro = 'Debes introducir un texto para realizar a busca.';
  
−
* O usuario introducirá nunha caixa de texto o nome (ou parte del) do autor.
+
            /*
 +
        * Como regra da aplicación esiximos
 +
        * polo menos 3 caracteres.
 +
        */
 +
        } elseif (mb_strlen($textoBuscar) < 3) {
  
−
: <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].
+
            $erro =
−
: Empregando PDO <u>non necesitamos escapar os parámetros</u> xa que o fai el automaticamente.
+
                'A cadea a buscar debe ter polo menos 3 caracteres.';
 +
        } else {
  
 +
            /*
 +
            * Preparamos a consulta.
 +
            */
 +
            $stmt = $conex->prepare(
 +
                'call CONSULTAR_LIBROS_POR_AUTOR(: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:
+
            /*
 +
            * 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
 +
            );
  
−
[[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.
+
            /*
 +
            * Executamos a consulta.
 +
            */
 +
            $stmt->execute();
  
−
:: O código sería algo parecido a isto:
 
−
::<syntaxhighlight lang="php" 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(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);
+
            /*
 +
            * Recuperamos todos os resultados.
 +
            */
 +
            $libros = $stmt->fetchAll();
  
−
                        $last_autor=$row["nome_autor"];   // Gardamos o nome do autor para comparar có autor que veña na seguinte fila de datos.
+
            $stmt->closeCursor();
−
                    }
 
  
−
</syntaxhighlight>
 
  
 +
            /*
 +
            * Se non hai filas gardamos unha mensaxe
 +
            * para amosala posteriormente.
 +
            */
 +
            if (!$libros) {
  
−
 
+
                $erro =
−
* 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).
+
                    'Non se atoparon libros para ese autor.';
−
 
 
−
:: Para facelo imos facer uso de javascript e da [http://www.w3schools.com/jsref/met_win_open.asp función window.open].
 
−
:: Crearei unha función en javascript que espere recibir un parámetro, que sexa a url a abrir e chamarei a window.open cunha serie de modificacións, como o tamaño da xanela, que non poida modificar a url,...
 
−
:: O código será o seguinte:
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="4-8" >
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
        <script type="text/javascript">
 
−
            function abrirVentana($url){
 
−
                window.open($url,'_blank','channelmode=0,status=0;directories=0,location=0,menubar=0,titlebar=0,toolbar=0,width=600,height=400');
 
 
             }
 
             }
−
         </script>
+
         }
−
       
+
     }
−
     </head>
+
</syntaxhighlight>
−
</syntaxhighlight>  
 
  
 +
<br />
  
−
:: Para chamar a esta función usaremos a etiqueta <a> e o evento onclick, coma o seguinte exemplo:
+
=== Consulta chamando a procedementos almacenados con parámetros de saída ===
−
::<syntaxhighlight lang="php" 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>.
+
* Un parámetro `OUT` permite que un procedemento devolva un valor.
  
 +
* Por exemplo:
  
 +
::<syntaxhighlight lang="sql">
 +
DELIMITER $$
  
 +
CREATE PROCEDURE CONSULTAR_NUM_LIBROS(
 +
OUT p_num_libros INT
 +
)
 +
BEGIN
  
 +
SELECT COUNT(*)
 +
INTO p_num_libros
 +
FROM libros;
  
−
<br />
+
END$$
−
* '''SOLUCIÓN'''
 
  
−
'''Arquivo: bd.php'''
+
DELIMITER ; </syntaxhighlight>
  
−
<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''')
+
* Con PDO e MySQL podemos empregar unha variable de usuario de MySQL:
−
::<syntaxhighlight lang="php" enclose="div">
 
−
<?php
 
  
−
define ('CONSULTAR_LIBROS_POR_AUTOR',"select CONCAT(apelidos,',',nome) as nome_autor,libros.id ,titulo from libros " .
+
::<syntaxhighlight lang="php">
−
        ' INNER JOIN libro_autor ON (libro_autor.libro_id=libros.id) ' .
+
$stmt = $conex->prepare(
−
        ' INNER JOIN autores ON (libro_autor.autor_id=autores.id)' .
+
'CALL CONSULTAR_NUM_LIBROS(
−
        " where CONCAT(autores.nome,' ',autores.apelidos) like :autor" .
+
@numero_libros
−
        ' 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.
+
$stmt->execute();
−
: '''A complicación é que nos queremos enviar %valor% para que busque por patrón. Iso o facemos ao darlle o valor ao parámetro.'''
 
  
 +
/*
  
 +
* É importante liberar os resultados
 +
* da chamada antes de executar outro SELECT.
 +
  */
 +
  $stmt->closeCursor();
  
 +
$stmt = $conex->query(
 +
'SELECT @numero_libros'
 +
);
  
−
<br />
+
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
−
'''Arquivo buscar_libros_por_autor.php'''
 
−
::<syntaxhighlight lang="php" 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');
+
* Podemos amosar o resultado:
  
−
try {
+
::<syntaxhighlight lang="php">
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",SERVIDOR,BD);
+
printf(
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
+
'<h1>Número de libros á venda: %d</h1>',
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
$numLibros
 +
); </syntaxhighlight>
  
−
    $erro="";  // Variable que a usaremos para amosar os posibles erros controlados por nos que poidan darse.
+
<br />
−
?>
 
  
 +
=== Consulta chamando a funcións ===
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
* MySQL tamén permite definir funcións almacenadas.
−
    "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>
+
* Unha función, a diferenza dun procedemento, devolve directamente un valor e pode empregarse dentro dunha expresión SQL.
−
        <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){
+
* Por exemplo:
−
                    $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
+
::<syntaxhighlight lang="sql">
−
}    // Pecha o try do principio da páxina
+
DELIMITER $$
−
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.
+
CREATE FUNCTION CONSULTAR_NUM_LIBROS()
−
:: Polo tanto a conexión e o peche da base de datos se podería poñer dentro do else da liña 44.
+
RETURNS INT
−
* 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.
+
READS SQL DATA
 +
BEGIN
  
 +
DECLARE num_libros INT;
  
−
* '''Resultado:'''
+
SELECT COUNT(*)
−
[[Imagen:Php_BD_11.jpg|500px|center]]
+
INTO num_libros
 +
FROM libros;
  
 +
RETURN num_libros;
  
−
<br />
+
END$$
  
−
=== Consulta chamando a procedementos almacenados ===
+
DELIMITER ; </syntaxhighlight>
  
−
* Un procedemento almacenado ven a ser o equivalente a unha función en PHP, coa diferenza que as ordes que conforman o procedemento van permitir realizar accións (inserción, borrado, modificación e selección) sobre a base de datos.
+
==== Empregando PDO ====
  
−
* 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.
+
* Podemos chamar á función mediante un `SELECT`:
  
−
: Tamén é unha forma de aumentar a seguridade de acceso á base de datos Mysql, xa que teremos que dar permiso de execución sobre o procedemento almacenado e o usuario non vai poder acceder ás táboas da base de datos nin examinar a estrutura das mesmas.
+
::<syntaxhighlight lang="php">
−
: 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.
+
$stmt = $conex->query(
−
: 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.
+
'SELECT CONSULTAR_NUM_LIBROS()'
 +
);
  
 +
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
  
−
* Máis información [http://php.net/manual/es/mysqli.quickstart.stored-procedures.php neste enlace].
+
* `fetchColumn()` resulta especialmente útil cando unha consulta devolve un único valor.
  
−
* O primeiro que temos que facer é crear o procedemento almacenado.
+
==== Funcións con parámetros ====
−
* A orde SQL é [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html '''CREATE PROCEDURE'''].
 
  
−
* Creamos o procedemento almacenado:
+
* Supoñamos unha función:
−
:* Cando definimos un procedemento almacenado debemos facer uso da palabra DELIMITER para indicarlle ao Mysql onde acaba a definición do procedemento almacenado. Máis información [http://dev.mysql.com/doc/refman/5.7/en/stored-programs-defining.html neste enlace].
 
  
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
+
::<syntaxhighlight lang="sql">
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
SELECT Suma(5, 7) </syntaxhighlight>
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `NOME_PROCEDEMENTO` ()
+
* Desde PDO:
−
BEGIN
 
  
−
  select col1,col2
+
::<syntaxhighlight lang="php">
−
  from TABOA_1
+
$stmt = $conex->prepare(
−
  order by col1;
+
'SELECT Suma(:a, :b)'
 +
);
  
 +
$stmt->bindValue(
 +
':a',
 +
5,
 +
PDO::PARAM_INT
 +
);
  
−
END
+
$stmt->bindValue(
−
$$
+
':b',
−
</syntaxhighlight>
+
7,
 +
PDO::PARAM_INT
 +
);
  
−
: Como vemos o procedemento está formado por unha consulta SELECT (pode poñerse calquera)
+
$stmt->execute();
−
 
 
−
 
 
−
* Agora dende PHP temos que facer uso do comando CALL 'NOME_PROCEDEMENTO'.
 
  
 +
$resultado = $stmt->fetchColumn(); </syntaxhighlight>
  
 
<br />
 
<br />
−
====Orientado a obxectos====
 
−
: Isto é o que teriamos que poñer se dende Mysql queredes chamar ao procedemento....
 
  
−
::<syntaxhighlight lang="php" enclose="div" highlight="12" >
+
=== Paxinación de resultados ===
−
<?php
 
−
/* Lembrar quitar isto ao probar o script sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.inc.php');  // Onde están definidas as constantes que se empregan na conexión (feito nas seccións anteriores)
+
* En moitas ocasións unha consulta pode devolver unha cantidade elevada de rexistros.
  
−
try {
+
: Para evitar amosalos todos simultaneamente podemos dividir o resultado en páxinas.
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
−
    $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
  
−
} 
+
* En MySQL podemos empregar `LIMIT` e `OFFSET`.
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
  
 +
* Por exemplo:
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="sql">
 +
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT 2 OFFSET 0; </syntaxhighlight>
  
 +
: Devolverá os dous primeiros rexistros.
  
 +
::<syntaxhighlight lang="sql">
 +
SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT 2 OFFSET 2; </syntaxhighlight>
  
−
* A diferenza do caso anterior, cando chamemos a un procedemento almacenado teremos que facer uso dunha das seguintes funcións:
+
: Devolverá os dous seguintes.
−
:: [http://php.net/manual/es/mysqli.real-query.php mysqli::real_query] ou [http://php.net/manual/es/mysqli.multi-query.php mysqli::multi_query]
 
−
 
 
−
: Nos imos facer uso da chamada a '''multi_query''' xa que tamén a imos poder utilizar cando dentro dun mesmo procedemento almacenado teñamos varios select´s e queiramos recuperar o contido de todos eles (cada un será un conxunto de resultados).
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="3,4" >
 
−
        ........................
 
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
  
−
        }
+
==== Pasos necesarios para realizar a paxinación ====
  
−
</syntaxhighlight>
+
* Necesitamos:
  
 +
:# Coñecer o número total de rexistros.
 +
:# Establecer cantos rexistros se amosarán por páxina.
 +
:# Coñecer a páxina actual.
 +
:# Calcular o número total de páxinas.
 +
:# Calcular o desprazamento (`OFFSET`).
 +
:# Realizar a consulta correspondente.
  
−
: Unha vez temos o conxunto de resultados ($result) xa operamos como nos casos anteriores.
 
  
 +
<br />
 +
'''Número total de rexistros'''
  
−
*<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.
+
::<syntaxhighlight lang="php">
−
: A resposta a ese erro a tedes [https://dev.mysql.com/doc/refman/8.0/en/commands-out-of-sync.html neste enlace].
+
$stmt = $conex->query(
 +
'SELECT COUNT(*)
 +
FROM libros'
 +
);
  
 +
$numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight>
  
  
−
* Ao parecer, existen aínda conxuntos de resultados sen 'recoller' (aínda que o procedemento soamente devolva un) e non vos deixará realizar outra chamada ata que leades todos os conxuntos de resultados.
 
−
: Para facelo, podedes engadir o seguinte código:
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="9" >
 
−
        ........................
 
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
                // Procesamos cada fila
 
−
            }
 
−
            while($conex->next_result() && $conex->store_result());
 
  
−
        }
+
<br />
 +
'''Número de rexistros por páxina'''
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="php">
 +
define(
 +
'CONST_NUM_LIBROS_PAXINA',
 +
10
 +
); </syntaxhighlight>
  
  
 +
<br />
 +
'''Obter e validar a páxina actual'''
  
 +
* O número de páxina chegará normalmente mediante `GET`.
  
−
* Como comentei anteriormente, un procedemento pode estar conformado por múltiples 'select'.
+
::<syntaxhighlight lang="php">
−
: 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:
+
$paxinaActual = filter_input(
 +
INPUT_GET,
 +
'paxina',
 +
FILTER_VALIDATE_INT
 +
);
  
−
::<syntaxhighlight lang="php" enclose="div" highlight="8-10" >
+
if (
−
        ........................
+
$paxinaActual === false ||
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
+
$paxinaActual === null ||
−
        if ($conex->multi_query($proc)){
+
$paxinaActual < 1
−
            $result = $conex->store_result();
+
) {
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
            if ($conex->next_result()){
 
−
              $result=$conex->store_result();
 
−
            }
 
  
−
        }
+
$paxinaActual = 1;
  
−
</syntaxhighlight>  
+
} </syntaxhighlight>
  
−
: Se imos procesar todos os conxuntos de resultados da mesma forma poderíamos utilizar un while{}...
+
* `htmlspecialchars()` non serve para validar que un dato sexa numérico.
  
 +
: Neste caso empregamos `FILTER_VALIDATE_INT`.
  
  
 +
<br />
 +
'''Número total de páxinas'''
  
−
<br />
+
::<syntaxhighlight lang="php">
−
====Empregando PDO====
+
$totalPaxinas = (int) ceil(
−
::<syntaxhighlight lang="php" enclose="div" highlight="27" >
+
$numLibros /
−
<?php
+
CONST_NUM_LIBROS_PAXINA
−
/* Lembrar quitar isto ao probar o scripts sen erros */
+
); </syntaxhighlight>
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
  
−
require ('/var/www/confBD/bd.php');
+
* Podemos evitar que o usuario solicite unha páxina superior á existente:
  
−
try {
+
::<syntaxhighlight lang="php">
−
    $dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",SERVIDOR,BD);
+
if (
−
    $conex = new PDO($dsn, USUARIO, PASSWORD);
+
$totalPaxinas > 0 &&
−
    $conex->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
$paxinaActual > $totalPaxinas
 +
) {
  
−
?>
+
$paxinaActual = $totalPaxinas;
  
−
<!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>
 
−
    </head>
 
  
−
    <body>
 
−
    <?php       
 
−
    // Preparar a chamada ao procedemento almacenado
 
−
    $proc = $conex->prepare("CALL " . NOME_PROCEDEMENTO);
 
  
−
    // Se o procedemento necesita parámetros:
+
<br />
−
    // $proc->execute(['param1' => $valor1, 'param2' => $valor2]);
+
'''Calcular o OFFSET'''
−
    // Sen parámetros:
 
−
    $proc->execute();
 
−
       
 
−
    ?>       
 
−
    </body>   
 
−
</html>
 
  
−
<?php
+
::<syntaxhighlight lang="php">
−
}    // Pecha o try do principio da páxina
+
$inicio = (
−
catch (PDOException $e) {
+
$paxinaActual - 1
−
  die("Erro PDO: " . $e->getMessage());
+
) * CONST_NUM_LIBROS_PAXINA; </syntaxhighlight>
−
} finally {
 
−
  $conex = null; // Pechar a conexión
 
−
}
 
−
 
 
−
?>
 
−
</syntaxhighlight>  
 
  
  
  
 
<br />
 
<br />
−
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
+
'''Consulta paxinada'''
  
−
* 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.
+
* Como `LIMIT` e `OFFSET` necesitan valores numéricos, indicaremos explicitamente o tipo mediante `PDO::PARAM_INT`.
  
 +
::<syntaxhighlight lang="php">
 +
$stmt = $conex->prepare(
 +
'SELECT id, titulo
 +
FROM libros
 +
ORDER BY titulo
 +
LIMIT :limite
 +
OFFSET :inicio'
 +
);
  
−
* Creamos o procedemento almacenado:
+
$stmt->bindValue(
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
+
':limite',
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
CONST_NUM_LIBROS_PAXINA,
−
DELIMITER $$
+
PDO::PARAM_INT
 +
);
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS` ()
+
$stmt->bindValue(
−
BEGIN
+
':inicio',
 +
$inicio,
 +
PDO::PARAM_INT
 +
);
  
−
  select ìd,titulo
+
$stmt->execute();
−
  from libros
 
−
  order by titulo;
 
  
 +
$libros = $stmt->fetchAll(); </syntaxhighlight>
  
−
END
 
−
$$
 
−
</syntaxhighlight>
 
  
 +
<br />
 +
'''Amosar os resultados'''
  
 +
::<syntaxhighlight lang="php">
 +
if ($libros) {
  
−
* Definimos nunha constante o nome do procedemento almacenado dentro do arquivo bd.php.
+
echo "<table border='1'>";
  
−
:'''Arquivo: bd.php'''
+
foreach ($libros as $libro) {
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_LIBROS','CONSULTAR_LIBROS()');  // Se levara parámetros faríamos o mesmo que nos casos anteriores coas sentencias select, empregando %s, %d...
 
−
</syntaxhighlight>
 
  
 +
    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.';
  
−
<br />
+
} </syntaxhighlight>
−
=====Empregando procedementos almacenados=====
 
  
−
* Debemos cambiar a chamada a 'query' pola chamada [http://php.net/manual/es/mysqli.real-query.php mysqli::real_query] ou [http://php.net/manual/es/mysqli.multi-query.php mysqli::multi_query]
 
−
:: Nos imos facer uso da chamada a '''multi_query''' xa que tamén a imos poder utilizar cando dentro dun mesmo procedemento almacenado teñamos varios select´s e queiramos recuperar o contido de todos eles (cada un será un conxunto de resultados).
 
  
−
:'''Arquivo: consultar_libros_proc.php'''
 
−
::<syntaxhighlight lang="php" 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');
+
<br />
 +
'''Amosar os enlaces ás páxinas'''
  
−
try {
+
::<syntaxhighlight lang="php">
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,BD); 
+
echo '<div>';
−
    $conex->set_charset('utf8mb4');
 
  
−
?>
+
for (
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
$cont = 1;
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
$cont <= $totalPaxinas;
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
$cont++
−
    <head>
+
) {
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
−
    <body>
 
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
 
−
        <?php       
 
−
        $proc = sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_LIBROS);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<select name='lstLibros' onchange='this.form.submit()'>";
 
−
                echo "<option value='-1'></option>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    if(isset($_POST['lstLibros']) && $row['id']==$_POST['lstLibros']){
 
−
                        $cadea="selected='selected'";
 
−
                    }
 
−
                    else{
 
−
                        $cadea='';
 
−
                    }
 
−
                    printf("<option value='%d' %s>%s</option>",$row["id"],$cadea,$row["titulo"]);
 
−
                }
 
−
                echo '</select>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar...";
 
−
            }
 
−
               
 
−
        }
 
−
        else {
 
−
            echo "Houbo un erro na chamado ao procedemento:" . $conex->error; // A mensaxe do erro ($conex->error) non a teremos que amosar coa aplicación funcionando
 
−
        }
 
−
        ?>       
 
−
       
 
−
       
 
−
  </body>   
 
−
</html>
 
−
<?php
 
−
}  // Pecha o try da parte superior da páxina
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
  
−
:* Liña 24: Creamos a cadea que vai chamar ao procedemento almacenado: CALL CONSULTAR_LIBROS()
+
if ($cont === $paxinaActual) {
−
:* 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.
 
  
 +
    printf(
 +
        '<strong>%d</strong> ',
 +
        $cont
 +
    );
  
 +
} else {
 +
 +
    printf(
 +
        "<a href='?paxina=%d'>%d</a> ",
 +
        $cont,
 +
        $cont
 +
    );
 +
}
 +
 +
}
 +
 +
echo '</div>'; </syntaxhighlight>
 +
 +
 +
 +
==== Exemplo completo de paxinación ====
 +
 +
 +
 +
'''Aquivo bd.php'''
 +
::<syntaxhighlight lang="php">
 +
define(
 +
    'CONST_NUM_LIBROS_PAXINA',
 +
    10
 +
);
 +
::</syntaxhighlight>
 +
 +
 +
'''Páxina php a cargar'''
 +
::<syntaxhighlight lang="php">
  
−
<br />
 
−
=====Empregando PDO=====
 
−
:'''Arquivo: consultar_libros_proc.php'''
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="24-26" >
 
 
<?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);
 
  
−
?>
+
    $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'
−
     </head>
+
     );
  
−
     <body>
+
     $numLibros = (int) $stmt->fetchColumn();
−
    <?php       
 
−
       
 
−
        $proc=$conex->prepare("CALL " . PROCEDEMENTO_CONSULTAR_LIBROS);
 
−
        $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
 
−
}    // Pecha o try do principio da páxina
 
−
catch (PDOException $e) {
 
−
  die("Erro PDO: " . $e->getMessage());
 
−
} finally {
 
−
  $conex = null; // Pechar a conexión
 
−
}
 
  
−
?>
+
    /*
 +
    * Número total de páxinas.
 +
    */
 +
    $totalPaxinas = (int) ceil(
 +
        $numLibros /
 +
        CONST_NUM_LIBROS_PAXINA
 +
    );
  
−
</syntaxhighlight>
 
  
−
=== Consulta chamando a procedementos almacenados con parámetros de entrada ===
+
    /*
 +
    * Páxina solicitada polo usuario.
 +
    */
 +
    $paxinaActual = filter_input(
 +
        INPUT_GET,
 +
        'paxina',
 +
        FILTER_VALIDATE_INT
 +
    );
  
−
* Podemos chamar a un procedemento almacenado enviando datos ao mesmo en forma de parámetros.
+
    if (
−
: 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'''.
+
        $paxinaActual === false ||
−
::* 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.
+
        $paxinaActual === null ||
−
::* 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.
+
        $paxinaActual < 1
−
::* 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].
+
        $paxinaActual = 1;
 +
    }
  
−
* Por defecto, se non especificamos nada, todos os parámetros dun procedemento almacenado son considerados 'de entrada'
 
  
−
* Primeiro teremos que definir o procedemento en Mysql:
+
    if (
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
+
        $totalPaxinas > 0 &&
−
DELIMITER $$
+
        $paxinaActual > $totalPaxinas
 +
    ) {
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100))
+
        $paxinaActual = $totalPaxinas;
−
BEGIN
+
    }
  
−
    SELECT col1,col2
 
−
    FROM TABOA_1
 
−
    WHERE col2=_param1;
 
  
−
END
+
    /*
−
</syntaxhighlight>
+
    * Primeiro rexistro da páxina.
 +
    */
 +
    $inicio = (
 +
        $paxinaActual - 1
 +
    ) * CONST_NUM_LIBROS_PAXINA;
  
−
: Como vemos, xunto ao nome do procedemento hai que especificar o tipo (dentro dos tipos de mysql).
 
−
: 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.
 
  
 +
    /*
 +
    * Consulta dos libros da páxina actual.
 +
    */
 +
    $stmt = $conex->prepare(
 +
        'SELECT id, titulo
 +
        FROM libros
 +
        ORDER BY titulo
 +
        LIMIT :limite
 +
        OFFSET :inicio'
 +
    );
  
−
: Podemos definir máis parámetros, separando por comas cada un deles, por exemplo:
+
    $stmt->bindValue(
−
::<syntaxhighlight lang="php" enclose="div" highlight="3,8,9" >
+
        ':limite',
−
DELIMITER $$
+
        CONST_NUM_LIBROS_PAXINA,
 +
        PDO::PARAM_INT
 +
    );
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100),_param2 int)
+
    $stmt->bindValue(
−
BEGIN
+
        ':inicio',
 +
        $inicio,
 +
        PDO::PARAM_INT
 +
    );
  
−
     SELECT col1,col2
+
     $stmt->execute();
−
    FROM TABOA_1
 
−
    WHERE col2=_param1 and
 
−
          col1=_param2
 
  
−
END
+
    $libros = $stmt->fetchAll();
−
</syntaxhighlight>  
 
  
 +
} catch (PDOException $e) {
  
−
* Unha  vez definido teremos que enviar os datos que queiramos dende PHP.
+
    error_log($e->getMessage());
 +
    die('Erro relacionado coa base de datos.');
  
 +
} catch (Throwable $e) {
  
−
<br />
+
    die('Produciuse un erro na aplicación.');
−
====Orientado a obxectos====
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
 
−
</syntaxhighlight>
 
−
: Fixarse como no caso de datos de tipo numérico non enviamos o dato con comiñas.
 
−
: Fixarse como 'xogamos' coas comiñas simples de dobres para pechar correctamente a cadea.
 
  
−
::<syntaxhighlight lang="php" enclose="div" highlight="5,6" >
+
}
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
 
  
−
        ........................
+
?>
  
−
        $proc = sprintf(CHAMAR_PROCEDEMENTO_CON_PARAMETROS,valor_numérico,'valor_cadea');
+
<!DOCTYPE html>
−
        $proc = sprintf("CALL %s",$proc);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
        }
 
  
−
</syntaxhighlight>  
+
<html lang="gl">
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
+
<head>
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
+
    <meta charset="UTF-8">
 +
    <title>Paxinación de libros</title>
 +
</head>
  
 +
<body>
  
 +
<?php if ($libros): ?>
  
−
<br />
+
<table border="1">
−
====Empregando PDO====
 
  
−
: En PDO, a preparación da chamada se fai coma sempre:
+
    <?php foreach ($libros as $libro): ?>
−
::<syntaxhighlight lang="php" 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.
+
        <tr>
 +
            <td>
 +
                <?= htmlspecialchars(
 +
                    $libro['titulo'],
 +
                    ENT_QUOTES | ENT_SUBSTITUTE,
 +
                    'UTF-8'
 +
                ) ?>
 +
            </td>
 +
        </tr>
  
−
::<syntaxhighlight lang="php" enclose="div" highlight="6,7" >
+
    <?php endforeach; ?>
−
// Valores dos parámetros
 
−
    $valor_num = 42;
 
−
    $valor_text = "exemplo";
 
  
−
    // Asignamos valores e tipos
+
</table>
−
    $proc->bindValue(':param_num', $valor_num, PDO::PARAM_INT);
 
−
    $proc->bindValue(':param_text', $valor_text, PDO::PARAM_STR);
 
  
−
    // Executamos o procedemento
+
<?php else: ?>
−
    $proc->execute();
 
−
    while ($fila = $proc->fetch(PDO::FETCH_ASSOC)) {
 
−
      .............
 
−
    }
 
−
</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>.
 
  
 +
<p>Non hai libros que amosar.</p>
  
 +
<?php endif; ?>
  
 +
<?php if ($totalPaxinas > 1): ?>
  
−
<br />
+
<div>
−
==== Caso Práctico: Consulta chamando a procedementos almacenados con parámetros de entrada ====
 
  
−
* Seguindo có noso exemplo...imos crear unha nova páxina de nome '''buscar_libros_por_autor_proc.php''' que faga uso de procedementos almacenados en vez de sentencias SQL directamente no código.
+
    <?php
 +
    for (
 +
        $cont = 1;
 +
        $cont <= $totalPaxinas;
 +
        $cont++
 +
    ):
 +
    ?>
  
−
'''Solución:'''
+
        <?php if ($cont === $paxinaActual): ?>
−
* '''Procedemento almacenado CONSULTAR_LIBROS_POR_AUTOR en Mysql'''
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS_POR_AUTOR`(IN p_autor_buscar varchar(100))
+
            <strong>
−
BEGIN
+
                <?= $cont ?>
 +
            </strong>
  
−
select CONCAT(apelidos,',',nome) as nome_autor,libros.id as id_libro,titulo from libros
+
        <?php else: ?>
−
            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,titulo;
 
  
−
END$$
+
            <a href="?paxina=<?= $cont ?>">
−
</syntaxhighlight>  
+
                <?= $cont ?>
 +
            </a>
  
−
: <u>Nota:</u> Para comprobar se funciona facede primeiro unha chamada dende o propio Mysql coa orde: call CONSULTAR_LIBROS_POR_AUTOR('dru');
+
        <?php endif; ?>
−
: Fixarse como o parámetro é de tipo IN (valor por defecto, podería non poñerse nada).
 
  
 +
    <?php endfor; ?>
  
 +
</div>
  
−
<br />
+
<?php endif; ?>
−
=====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="php" 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.
+
</body>
 +
</html>
 +
</syntaxhighlight>
  
 +
==== Sobre a optimización da paxinación ====
  
 +
* En xeral non debemos enviar pola URL o número total de rexistros e confiar posteriormente nese dato:
  
−
* '''Arquivo buscar_libros_por_autor_proc.php'''
+
::<syntaxhighlight lang="text">
−
::<syntaxhighlight lang="php" enclose="div" highlight="44-47" >
+
?paxina=2&numtotal=50 </syntaxhighlight>
−
<?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');
+
: Os datos enviados mediante `GET` poden ser modificados polo usuario.
  
−
try {
+
* 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.
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD); 
 
−
    $conex->set_charset('utf8mb4');
 
  
−
    $erro="";  // Variable que a usaremos para amosar os posibles erros que poidan darse
+
* Como primeira solución realizaremos en cada petición:
  
−
?>
+
::<syntaxhighlight lang="sql">
−
 
+
SELECT COUNT(*)
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
FROM libros </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
 
−
catch (mysqli_sql_exception $e) {
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
−
?>
 
−
</syntaxhighlight>
 
−
 
 
−
:* Liñas 44 e 45: Creamos a cadea que vai chamar ao procedemento almacenado de Mysql: CALL CONSULTAR_LIBROS_POR_AUTOR('dru'); (é un exemplo de busca pola palabra dru)
 
−
 
 
−
 
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
=====Empregando PDO=====
 
−
* 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="php" enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_LIBROS_POR_AUTOR','CONSULTAR_LIBROS_POR_AUTOR(:nome)');
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
* '''Arquivo buscar_libros_por_autor_proc.php'''
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="44-47" >
 
−
<?php
 
−
/* Lembrar quitar isto ao probar o scripts sen erros */
 
−
ini_set('display_errors', 1);
 
−
ini_set('display_startup_errors', 1);
 
−
error_reporting(E_ALL);
 
−
 
 
−
require ('/var/www/confBD/bd.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>
 
−
        <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 = $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
 
−
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ñ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)
 
−
 
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
 
 
−
=== Consulta chamando a procedementos almacenados con parámetros de saída ===
 
−
 
 
−
* Agora imos aplicar o concepto de parámetros de saída, que como indicamos no punto anterior, son parámetros que van poder ser modificados o seu valor dentro do procedemento almacenado e recoller o seu valor modificado dende a parte de PHP.
 
−
 
 
−
* Primeiro teremos que definir o procedemento en Mysql:
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="3,7" >
 
−
DELIMITER $$
 
−
 
 
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(OUT _param1 varchar(100))
 
−
BEGIN
 
−
 
 
−
    SELECT col2
 
−
    INTO _param1
 
−
    FROM TABOA_1
 
−
    WHERE col1=1;
 
−
 
 
−
END
 
−
</syntaxhighlight>
 
−
 
 
−
: Agora especificamos que o parámetro é de saída antepoñendo a palabra OUT ao seu nomo.
 
−
: Neste exemplo estamos a supoñer que a consulta devolve unha única fila e estamos gardando o resultado da consulta no parámetro de saída.
 
−
: Podemos modificar o seu valor dentro do procedemento coa orde: SET _param1=valor;
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
====Orientado a obxectos====
 
−
 
 
−
 
 
−
* Unha  vez definido teremos que construír a chamada ao procedemento enviando o nome dunha variable (para recoller o valor modificado).
 
−
: O nome da variable terá este formato: '''@'''nomeVariable:
 
−
::<syntaxhighlight lang="php" 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.
 
−
 
 
−
 
 
−
* 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.
 
−
: Para recoller o dato da variable teremos que facer unha consulta SELECT da forma: SELECT @nomeVariable as valorDevolto
 
−
 
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="10,14" >
 
−
<?php
 
−
        define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS("@nomeVariable")');
 
−
 
 
−
        ........................
 
−
 
 
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
 
 
−
            if (!($resultado = $conex->query("SELECT @nomeVariable as valorDevolto"))) {
 
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
−
            else {
 
−
                $fila = $resultado->fetch_assoc();
 
−
                echo $fila['valorDevolto']);
 
−
            }
 
−
 
 
−
        }
 
−
        ........................
 
−
 
 
−
 
 
−
?>
 
−
 
 
−
</syntaxhighlight>
 
−
 
 
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
 
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
−
 
 
−
 
 
−
 
 
−
: <u>Nota:</u> Non fai falla facer un alias do nome da variable. Podemos empregar o propio nome no fetch_assoc().
 
−
:: Podemos poñer:
 
−
 
 
−
:::<syntaxhighlight lang="php" enclose="div" highlight="5,10" >
 
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
 
 
−
            if (!($resultado = $conex->query("SELECT @nomeVariable"))) {
 
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
−
            else {
 
−
                $fila = $resultado->fetch_assoc();
 
−
                echo $fila['@nomVariable']);
 
−
            }
 
−
 
 
−
        }
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
====Empregando PDO====
 
−
 
 
−
 
 
−
* Unha  vez definido teremos que construír a chamada ao procedemento enviando o nome dunha variable (para recoller o valor modificado).
 
−
: O nome da variable terá este formato: '''@'''nomeVariable:
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(@nomeVariable)');
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
* 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.
 
−
: Para recoller o dato da variable teremos que facer unha consulta SELECT da forma: SELECT @nomeVariable as valorDevolto
 
−
 
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="10,14" >
 
−
<?php
 
−
        define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(@nomeVariable)');
 
−
 
 
−
        ........................
 
−
 
 
−
        $stmt = $conex->prepare("CALL " . CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
 
−
        $stmt->execute();
 
−
 
 
−
        $res = $conex->query("SELECT @nomeVariable AS total")->fetch(PDO::FETCH_ASSOC);
 
−
        if ($res){
 
−
          echo $res['valorDevolto'];
 
−
        }
 
−
        ........................
 
−
 
 
−
 
 
−
?>
 
−
 
 
−
</syntaxhighlight>
 
−
 
 
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
 
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
−
 
 
−
 
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
 
 
−
 
 
−
<br />
 
−
 
 
−
==== Caso práctico: Consulta chamando a procedementos almacenados con parámetros de saída ====
 
−
 
 
−
* Segundo o noso exemplo, imos crear un procedemento almacenado que devolva nun parámetro de saída o número de libros que temos na nosa tenda.
 
−
 
 
−
 
 
−
* '''Procedemento almacenado CONSULTAR_NUM_LIBROS en Mysql'''
 
−
::<syntaxhighlight lang="php" 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.
 
−
 
 
−
 
 
−
<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="php" 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="php" 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
 
−
            $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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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="php" 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"
+
* 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.
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
<br />
−
        <?php
 
−
                // 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'];
 
−
                    }
 
−
                }
 
−
            ?>
 
  
−
           
+
=== Exercicio proposto ===
−
            <?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>   
+
* Crea unha páxina chamada `buscar_libros.php` que permita buscar libros empregando os seguintes criterios:
−
</html>
 
  
−
<?php
+
:* '''Título''': unha caixa de texto que realizará unha busca mediante `LIKE`.
−
}    // Pecha o try do principio da páxina
+
:* '''Prezo''': un campo no que se validará que o valor introducido sexa numérico.
−
catch (mysqli_sql_exception $e) {
+
:* '''Estado''': unha lista `<select>` cos estados dispoñibles.
−
    die("Erro mysql: " . $e->getMessage());
+
:* '''Autor''': unha lista `<select>` cos autores dispoñibles.
−
}
+
:* '''Criterio de ordenación''': título, prezo ou autor.
−
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.
+
* O usuario poderá combinar os diferentes criterios.
−
* 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.
 
  
 +
* 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 />
  
 +
== Resumo das funcións PDO empregadas ==
  
−
<br />
+
::<syntaxhighlight lang="php">
−
===== Optimización =====
+
! Método / constante
−
 
 
−
* O exercicio anterior poderíamos optimizalo para non ter que repetir a chamada a consultar o número de rexistros totais.
 
−
: Esta chamada a estamos a facer en cada consulta á páxina.
 
−
: Se non estamos nun entorno multi-usuario no que se poida dar o caso de que se estean a dar de alta novos rexistros de forma continua, podemos optimizar o código e facer que a chamada a consultar o número de rexistros totais se faga unha vez.
 
−
 
 
−
 
 
−
 
 
−
* Deberemos gardar nalgunha variable o número de rexistros totais e que dita información se manteña entre chamadas cando cambiamos de número de páxina.
 
−
* <u>Posibles solucións</u>:
 
−
:* Utilizar coookies.
 
−
:* Utilizar variables de sesión.
 
−
:* Aproveitar o formulario da paxinación e enviar en forma de campo oculto o número de rexistros totais.
 
−
:* Enviar o dato na URL a maiores do número de páxina.
 
−
 
 
−
 
 
−
 
 
−
* Posible solución enviando o número total de libros na URL:
 
−
 
 
−
'''Arquivo consultar_libros_con_paxinacion.php'''
 
−
::<syntaxhighlight lang="php" enclose="div" highlight="9-11,47" >
 
−
.......................
 
−
 
 
−
 
 
−
 
 
−
            <?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.
 
−
                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>
+
| ! Uso                                                                                            |
 +
| ------------------------------------------------------------------------------------------------- |
 +
| `new PDO(...)`                                                                                    |
 +
| Abrir unha conexión coa base de datos.                                                            |
 +
| -                                                                                                |
 +
| `query()`                                                                                        |
 +
| Executar unha sentenza SQL sen parámetros externos e obter un resultado.                          |
 +
| -                                                                                                |
 +
| `prepare()`                                                                                      |
 +
| Preparar unha sentenza SQL parametrizada.                                                        |
 +
| -                                                                                                |
 +
| `execute()`                                                                                      |
 +
| Executar unha sentenza preparada.                                                                |
 +
| -                                                                                                |
 +
| `bindValue()`                                                                                    |
 +
| Asociar un valor e, opcionalmente, o seu tipo a un parámetro.                                    |
 +
| -                                                                                                |
 +
| `fetch()`                                                                                        |
 +
| Recuperar unha fila do resultado.                                                                |
 +
| -                                                                                                |
 +
| `fetchAll()`                                                                                      |
 +
| Recuperar todas as filas do resultado.                                                            |
 +
| -                                                                                                |
 +
| `fetchColumn()`                                                                                  |
 +
| Recuperar o valor dunha columna, especialmente útil para `COUNT(*)` ou funcións.                  |
 +
| -                                                                                                |
 +
| `closeCursor()`                                                                                  |
 +
| Liberar os resultados pendentes dunha sentenza, especialmente útil con procedementos almacenados. |
 +
| -                                                                                                |
 +
| `PDO::PARAM_INT`                                                                                  |
 +
| Indica un parámetro de tipo enteiro.                                                              |
 +
| -                                                                                                |
 +
| `PDO::PARAM_STR`                                                                                  |
 +
| Indica un parámetro de tipo cadea.                                                                |
 +
| -                                                                                                |
 +
| `PDO::PARAM_BOOL`                                                                                |
 +
| Indica un parámetro booleano.                                                                    |
 +
| -                                                                                                |
 +
| `PDO::PARAM_NULL`                                                                                |
 +
| Indica un valor nulo.                                                                            |
 +
| -                                                                                                |
 +
| `PDO::FETCH_ASSOC`                                                                                |
 +
| Recupera as filas como arrays asociativos.                                                        |
 +
| }                                                                                                |
 +
::</syntaxhighlight>
  
 +
== Regras que empregaremos nos exames ==
  
 +
# As operacións contra a base de datos realizaranse mediante '''PDO'''.
  
−
<br />
+
# Para consultas sen parámetros externos poderá empregarse `query()`.
−
* 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'''
+
# Se hai datos procedentes do usuario ou outros valores externos empregarase `prepare()` + `execute()`.
−
::<syntaxhighlight lang="php" enclose="div" highlight="1-4,9,21,29" >
 
−
<?php
 
−
  session_start();  // Iría ao principio de todo
 
  
−
?>
+
# Non se concatenarán directamente datos externos nas sentenzas SQL.
−
.......................
 
  
 +
# Os datos recibidos mediante formularios ou URL deberán validarse.
  
−
            <?php
+
# Cando o tipo sexa relevante empregarase `bindValue()` indicando `PDO::PARAM_INT`, `PDO::PARAM_STR`, etc.
−
                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>
+
# Non se empregará `rowCount()` para coñecer o número de filas devoltas por un `SELECT`.
  
 +
# 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).