Diferencia entre revisiones de «PHP UD3 OP CONSULTA»
| (No se muestran 86 ediciones intermedias del mismo usuario) | |||
| Línea 1: | Línea 1: | ||
=== Introducción === | === Introducción === | ||
| − | |||
| − | |||
| − | + | * '''<u>Requerimentos previos:</u>''' Ter creada a base de datos indicada [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Creando_as_t.C3.A1boas_en_Mysql nunha sección anterior] e ter gardados os parámetros de conexión nun arquivo separado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro como foi indicado nunha sección anterior] | |
| − | * Funcións para realizar consultas a base de datos e funcións relacionadas: | + | '''IMPORTANTE:''' Nos exames, exercicios e prácticas avaliables empregarase '''PDO''' para realizar as operacións contra a base de datos, salvo que no enunciado se indique expresamente o contrario. |
| + | Os exemplos con mysqli poden manterse nos apuntes a nivel informativo, pero os exemplos de referencia serán os realizados con PDO. | ||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | * O proceso para realizar unha consulta é o seguinte: | ||
| + | :* '''Paso 1: Abrimos a conexión á base de datos''': [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Apertura_da_conexi.C3.B3n Isto xa o vimos nunha sección anterior] | ||
| + | |||
| + | |||
| + | * Necesitamos ter unha consulta SQL para recuperar datos. | ||
| + | : Indicar que o [https://es.wikipedia.org/wiki/SQL SQL (Structure Query Language)] é a linguaxe utilizada para obter datos dunha base de datos relacional. | ||
| + | : Indicar que estos contidos xa deben estar aprendidos do módulo de Base de datos e saber facer joins entre táboas, subconsultas, agrupacións,... | ||
| + | |||
| + | |||
| + | * '''CONSIDERACIÓNS:''' | ||
| + | :* Crear un usuario/password con permisos de acceso a dita base de datos (sempre dádelle os mínimos permisos posibles, neste caso, serán os de SELECT/UPDATE/DELETE/INSERT/EXECUTE de toda a base de datos) | ||
| + | :* Crear nun arquivo separado do sitio web todos os datos necesarios para conectarse á base de datos e onde van estar definidas as constantes coas ordes SQL, tal como [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Gardando_os_datos_de_conexi.C3.B3n_nun_lugar_seguro vimos nun punto anterior desta sección]. | ||
| + | :* Lembrar poñer as ordes para amosar os erros que se poidan dar. Nunha aplicación final esas ordes hai que quitalas. | ||
| + | :* En Linux, lembrar dar permiso 644 (rw,r,r) aos arquivos php e cambiar o propietario a www-data:www-data. Nos cartafoles permisos 755 cando rematedes. | ||
| + | :* Ao principio ides faiar en moitas cousas. Lembrade ter posto o display_errors como vimos nas seccións anteriores. Tamén axuda moito facer un echo da sentenza a enviar á base de datos antes de enviala para ver que estea todo ben (espazos en branco, comillas,...). | ||
| + | :* Lembrar que tamén podedes activar o debugger como está indicado [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Habilitar_depuraci%C3%B3n_en_VS_Code nesta sección da wiki]. | ||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | |||
| + | === 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(): | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->query( | ||
| + | 'SELECT col1, col2 | ||
| + | FROM TABOA_1 | ||
| + | ORDER BY col1' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | : query() devolve un obxecto da clase PDOStatement. | ||
| + | |||
| + | |||
| + | * Se a consulta leva datos procedentes do usuario ou valores que queremos enviar mediante parámetros empregaremos: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | prepare() | ||
| + | execute() | ||
| + | </syntaxhighlight> | ||
| + | : Por exemplo: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'SELECT col1, col2 | ||
| + | FROM TABOA_1 | ||
| + | WHERE col1 = :valor' | ||
| + | ); | ||
| + | $stmt->execute([ | ||
| + | 'valor' => 5 | ||
| + | ]); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | * '''Version procedimental''' | ||
| + | : Funcións para realizar consultas a base de datos e funcións relacionadas: | ||
:* [http://php.net/manual/es/mysqli.query.php mysqli::query]: Realiza a consulta | :* [http://php.net/manual/es/mysqli.query.php mysqli::query]: Realiza a consulta | ||
| Línea 23: | Línea 86: | ||
| + | ::* Versión procedimental: | ||
| + | :::<syntaxhighlight lang="php" enclose="div" highlight="" > | ||
| + | $result=mysqli_query($conex, 'SELECT col1, col2 FROM TABOA_1 ORDER BY col1'); | ||
| + | </syntaxhighlight> | ||
| + | ::<u>Nota:</u> $conex é a conexión aberta feita no paso anterior. | ||
| + | ::* Versión orientada a obxectos e PDO: | ||
| + | :::<syntaxhighlight lang="php" enclose="div" highlight="" > | ||
| + | $result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1'); | ||
| + | </syntaxhighlight> | ||
| + | ::* $result vai ter o conxunto de filas que devolva o resultado. Podemos velo como unha táboa formada polas filas do resultado e polas columnas indicadas no select. | ||
<br /> | <br /> | ||
| − | |||
| − | |||
| + | === Comprobar se unha consulta SELECT devolveu datos === | ||
| + | |||
| + | * '''Non debemos empregar rowCount() para comprobar o número de filas devoltas por un SELECT'''. | ||
| + | : rowCount() está pensado principalmente para coñecer o número de filas afectadas por operacións INSERT, UPDATE ou DELETE. | ||
| + | : Nun SELECT o seu comportamento depende do controlador da base de datos e non está garantido. | ||
| + | |||
| + | |||
| + | * Unha forma sinxela de comprobar se hai datos consiste en realizar un primeiro fetch(): | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->query( | ||
| + | 'SELECT col1, col2 | ||
| + | FROM TABOA_1 | ||
| + | ORDER BY col1' | ||
| + | ); | ||
| + | $fila = $stmt->fetch(); | ||
| + | if ($fila !== false) { | ||
| + | do { | ||
| + | |||
| + | echo htmlspecialchars( | ||
| + | $fila['col1'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ); | ||
| + | |||
| + | echo ' - '; | ||
| + | |||
| + | echo htmlspecialchars( | ||
| + | $fila['col2'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ); | ||
| + | |||
| + | echo '<br>'; | ||
| + | |||
| + | } while ($fila = $stmt->fetch()); | ||
| + | } else { | ||
| + | echo 'Non hai datos que amosar'; | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | * Se sabemos que o número de filas vai ser pequeno tamén podemos recuperar todas as filas mediante '''fetchAll()''': | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->query( | ||
| + | 'SELECT col1, col2 | ||
| + | FROM TABOA_1 | ||
| + | ORDER BY col1' | ||
| + | ); | ||
| + | $filas = $stmt->fetchAll(); | ||
| + | if ($filas) { | ||
| + | foreach ($filas as $fila) { | ||
| + | |||
| + | echo htmlspecialchars( | ||
| + | $fila['col1'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ); | ||
| + | echo ' - '; | ||
| + | echo htmlspecialchars( | ||
| + | $fila['col2'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ); | ||
| + | echo '<br>'; | ||
| + | } | ||
| + | } else { | ||
| + | echo 'Non hai datos que amosar'; | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| − | |||
| − | |||
| − | + | * [https://www.php.net/manual/es/pdostatement.fetchcolumn.php fetchColum] permite recuperar da primeira fila a columna indicada. Se non poñemos ningunha columna recuperar a primeira. | |
| − | : | + | : Por exemplo, se realmente necesitamos coñecer o número de rexistros, realizaremos unha consulta específica empregando COUNT(*) e como sabemos que vai devolver unha fila cunha columna podemos empregar fetchColumn: |
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | + | $stmt = $conex->query( | |
| − | $ | + | 'SELECT COUNT(*) |
| − | </syntaxhighlight> | + | FROM libros' |
| + | ); | ||
| + | $numLibros = $stmt->fetchColumn(); | ||
| + | </syntaxhighlight> | ||
| − | |||
| Línea 56: | Línea 192: | ||
<br /> | <br /> | ||
| − | :* | + | ::* Versión orientada a obxectos: |
::* Se estamos a <u>empregar unha versión PHP superior ou igual á 8.1</u>, os erros deben ser capturados con try-catch: | ::* Se estamos a <u>empregar unha versión PHP superior ou igual á 8.1</u>, os erros deben ser capturados con try-catch: | ||
:::* Versión orientada a obxectos: | :::* Versión orientada a obxectos: | ||
| − | ::::<syntaxhighlight lang=" | + | ::::<syntaxhighlight lang="php" enclose="div" highlight="38-43" > |
<?php | <?php | ||
/* Lembrar quitar isto ao probar o scripts sen erros */ | /* Lembrar quitar isto ao probar o scripts sen erros */ | ||
| Línea 85: | Línea 221: | ||
<?php | <?php | ||
| − | $result=$conex->query( | + | $result=$conex->query('SELECT col1, col2 FROM TABOA_1 ORDER BY col1'); |
if ($result && $result->num_rows>0){ | if ($result && $result->num_rows>0){ | ||
// Aquí procesamos os datos | // Aquí procesamos os datos | ||
| Línea 110: | Línea 246: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | ::* Como vemos, en caso de erro comprobamos se o xestor devolveu un código de erro, indicativo de que a sentenza SQL está mal feita. A información do código de erro non debe amosarse a un usuario final. Normalmente, se pon o echo de 'Non hai datos que amosar' e se estamos a desenvolver a aplicación e non devolve datos, faríamos un echo temporal do código de erro do xestor ou un echo da sentenza SQL, podendo executala no xestor Mysql para ver o erro. | ||
| + | |||
| Línea 116: | Línea 258: | ||
<br /> | <br /> | ||
| − | + | === 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. | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) { | ||
| + | echo htmlspecialchars( | ||
| + | $fila['titulo'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ); | ||
} | } | ||
| − | + | </syntaxhighlight> | |
| − | + | : Fixarse que neste exemplo no facemos un if inicial có primeiro fetch para saber si hai datos ou non. Dependerá de vos como programadores determinar se é necesario para informar ao usuario o facer algún tipo de proceso nese caso. | |
| − | + | ||
| − | + | ||
| − | + | * Se na conexión establecemos: | |
| − | + | ::<syntaxhighlight lang="php"> | |
| − | + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC | |
| − | + | </syntaxhighlight> | |
| − | + | : non será necesario indicar PDO::FETCH_ASSOC en cada chamada: | |
| − | + | ::<syntaxhighlight lang="php"> | |
| − | + | while ($fila = $stmt->fetch()) { | |
| + | echo $fila['titulo']; | ||
} | } | ||
| − | </syntaxhighlight> | + | </syntaxhighlight> |
| + | : PDO::FETCH_ASSOC o que fai é que Php soamente cre un array asociativo cós resultados. Se non o poñemos, devolve dous arrays, un no que se accede aos datos polo índice ($fila[0]) e outro asociativo por nome de columna ($fila['titulo']). | ||
| − | + | * '''Non debemos chamar dúas veces seguidas a fetch() para procesar o mesmo conxunto de resultados'''. | |
| − | ::::<syntaxhighlight lang=" | + | : Por exemplo, isto é incorrecto: |
| − | + | ::<syntaxhighlight lang="php"> | |
| − | + | while ($fila = $stmt->fetch(PDO::FETCH_OBJ)) { | |
| − | + | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { | |
| + | // ... | ||
} | } | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| − | </syntaxhighlight> | + | </syntaxhighlight> |
| + | : O primeiro fetch() xa consume unha fila e o segundo bucle comezaría na seguinte. | ||
| + | : A forma correcta sería: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | while ($row = $stmt->fetch()) { | ||
| + | // Procesamos a fila | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| Línea 164: | Línea 310: | ||
<br /> | <br /> | ||
| − | + | :* 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'. | ::: Debemos entender que, inicialmente, despois de facer a consulta, temos un punteiro que está 'apuntando' xusto antes do primeiro rexistro de resultados. O que fai a función anterior é mover ese punteiro ao seguinte rexistro (a primeira vez apuntará ao primeiro do conxunto de resultados) e devolve nun array a fila á que está 'apuntando'. | ||
::: Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false. | ::: Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false. | ||
| + | ::: Isto o faremos cun bucle, chamando á [http://php.net/manual/es/mysqli-result.fetch-assoc.php función mysqli_result::fetch_assoc(void)]. | ||
::* Versión procedimental: | ::* Versión procedimental: | ||
| − | ::::<syntaxhighlight lang=" | + | ::::<syntaxhighlight lang="php" enclose="div" highlight="3-5" > |
if ($result && (mysqli_num_rows($resullt)>0)){ | if ($result && (mysqli_num_rows($resullt)>0)){ | ||
// Veñen datos | // Veñen datos | ||
| Línea 192: | Línea 332: | ||
::* Versión orientada a obxectos: | ::* Versión orientada a obxectos: | ||
| − | ::::<syntaxhighlight lang=" | + | ::::<syntaxhighlight lang="php" enclose="div" highlight="3-5" > |
if ($result && ($result->num_rows>0)){ | if ($result && ($result->num_rows>0)){ | ||
// Veñen datos | // Veñen datos | ||
| Línea 206: | Línea 346: | ||
::* Versión orientada a obxectos con fetch_object: | ::* Versión orientada a obxectos con fetch_object: | ||
| − | :::<syntaxhighlight lang=" | + | :::<syntaxhighlight lang="php" enclose="div" highlight="3,4" > |
if ($result && ($result->num_rows>0)){ | if ($result && ($result->num_rows>0)){ | ||
// Veñen datos | // Veñen datos | ||
| Línea 216: | Línea 356: | ||
echo 'Non hai datos ou a consulta está mal feita'; | echo 'Non hai datos ou a consulta está mal feita'; | ||
} | } | ||
| − | </syntaxhighlight> | + | </syntaxhighlight> |
| + | |||
| + | === Saída de datos en HTML === | ||
| + | Para amosar datos dentro dunha páxina HTML empregaremos normalmente htmlspecialchars(): | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | echo htmlspecialchars( | ||
| + | $fila['titulo'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | Non debemos empregar urlencode() para codificar texto que simplemente se vai amosar ao usuario. | ||
| + | : Por exemplo, para crear un enlace a un autor: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | printf( | ||
| + | "<a href='detalle_autor.php?id=%d'>%s, %s</a>", | ||
| + | (int) $row['id'], | ||
| + | htmlspecialchars( | ||
| + | $row['apelidos'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ), | ||
| + | htmlspecialchars( | ||
| + | $row['nome'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | : urlencode() ou rawurlencode() utilizaranse cando sexa necesario codificar datos que forman parte dunha URL. | ||
| + | === Consultas con parámetros numéricos === | ||
| + | Podemos empregar parámetros con nome: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | define( | ||
| + | 'CONSULTA_CON_PARAM_NUMERICO', | ||
| + | 'SELECT col1, col2 | ||
| + | FROM TABOA_1 | ||
| + | WHERE col1 = :valor' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | Preparamos e executamos a consulta: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | CONSULTA_CON_PARAM_NUMERICO | ||
| + | ); | ||
| + | $stmt->execute([ | ||
| + | 'valor' => 5 | ||
| + | ]); | ||
| + | </syntaxhighlight> | ||
| + | : '''IMPORTANTE:''' O nome do parámetro debe coincidir co definido na consulta. | ||
| + | : Se na consulta temos: | ||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | WHERE col1 = :valor | ||
| + | </syntaxhighlight> | ||
| + | : debemos empregar: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | 'valor' => 5 | ||
| + | </syntaxhighlight> | ||
| + | : e non outro nome como valor1. | ||
| + | |||
| + | === Tipo dos parámetros === | ||
| + | Cando enviamos directamente un array ao método execute(): | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt->execute([ | ||
| + | 'valor' => 5 | ||
| + | ]); | ||
| + | </syntaxhighlight> | ||
| + | : PDO trata os valores do array como PDO::PARAM_STR. | ||
| + | : O xestor de base de datos pode realizar posteriormente conversións de tipo segundo o contexto da consulta. | ||
| + | Na maioría das consultas sinxelas isto non supón un problema. | ||
| + | Se queremos indicar explicitamente o tipo do parámetro podemos empregar bindValue(): | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'SELECT col1, col2 | ||
| + | FROM TABOA_1 | ||
| + | WHERE col1 = :valor' | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':valor', | ||
| + | 5, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| + | </syntaxhighlight> | ||
| + | Algúns tipos habituais son: | ||
| + | ::<syntaxhighlight lang="text"> | ||
| + | PDO::PARAM_INT | ||
| + | PDO::PARAM_STR | ||
| + | PDO::PARAM_BOOL | ||
| + | PDO::PARAM_NULL | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | |||
| + | * É especialmente recomendable indicar explicitamente o tipo cando este sexa relevante para a consulta. | ||
| + | : Por exemplo, nos parámetros empregados en LIMIT ou OFFSET: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'SELECT id, titulo | ||
| + | FROM libros | ||
| + | ORDER BY titulo | ||
| + | LIMIT :inicio,:cantidade' | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':inicio', | ||
| + | $inicio, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':cantidade', | ||
| + | $cantidade, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| + | </syntaxhighlight> | ||
| + | === Parámetros de tipo cadea === | ||
| + | Para enviar unha cadea empregaremos tamén unha sentenza preparada: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'SELECT id, apelidos, nome | ||
| + | FROM autores | ||
| + | WHERE apelidos = :apelidos' | ||
| + | ); | ||
| + | $stmt->execute([ | ||
| + | 'apelidos' => $apelidos | ||
| + | ]); | ||
| + | </syntaxhighlight> | ||
| + | * Se queremos facer unha busca mediante LIKE, o marcador debe representar o '''valor completo''': | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'SELECT id, apelidos, nome | ||
| + | FROM autores | ||
| + | WHERE apelidos LIKE :apelidos' | ||
| + | ); | ||
| + | $stmt->execute([ | ||
| + | 'apelidos' => '%' . $apelidos . '%' | ||
| + | ]); | ||
| + | </syntaxhighlight> | ||
| − | < | + | * Non debemos escribir: |
| − | : | + | ::<syntaxhighlight lang="sql"> |
| + | WHERE apelidos LIKE '%:apelidos%' | ||
| + | </syntaxhighlight> | ||
| + | : xa que o marcador debe ocupar o lugar dun valor completo (PHP/PDO interpretará :apelidos como texto literal dentro das comiñas e non o substituirá pola variable). | ||
| + | |||
| + | === Validación dos parámetros recibidos === | ||
| + | As sentenzas preparadas evitan que os valores enviados como parámetros sexan interpretados como parte da instrución SQL, pero '''non substitúen a validación dos datos'''. | ||
| + | Por exemplo, se esperamos recibir un identificador numérico desde un formulario: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $libroId = filter_input( | ||
| + | INPUT_POST, | ||
| + | 'lstLibros', | ||
| + | FILTER_VALIDATE_INT | ||
| + | ); | ||
| + | if ( | ||
| + | $libroId !== false && | ||
| + | $libroId !== null && | ||
| + | $libroId !== -1 | ||
| + | ) { | ||
| + | $stmt = $conex->prepare( | ||
| + | CONSULTAR_EXEMPLARES_POR_LIBRO | ||
| + | ); | ||
| − | + | $stmt->bindValue( | |
| − | + | ':libro_id', | |
| − | + | $libroId, | |
| − | + | PDO::PARAM_INT | |
| − | + | ); | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | $stmt->execute(); | ||
| − | + | // Procesamos os resultados | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
} | } | ||
| − | + | </syntaxhighlight> | |
| − | + | === Caso práctico: consultar exemplares dun libro === | |
| − | + | '''Arquivo bd.php''' | |
| − | < | + | ::<syntaxhighlight lang="php"> |
| + | define('CARTAFOL_IMAXES_WEB', 'http://localhost/imaxes/'); | ||
| + | define( | ||
| + | 'CONSULTAR_EXEMPLARES_POR_LIBRO', | ||
| + | 'SELECT libro_id, | ||
| + | id_exemplar, | ||
| + | imaxe, | ||
| + | prezo, | ||
| + | estado_id | ||
| + | FROM exemplares | ||
| + | WHERE libro_id = :libro_id | ||
| + | ORDER BY estado_id' | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | + | * '''Código PHP''' | |
| + | ::<syntaxhighlight lang="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'; | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | echo ' | ||
} | } | ||
| − | |||
| − | |||
} | } | ||
| − | + | } 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> | </html> | ||
| + | </syntaxhighlight> | ||
| − | < | + | == Procedementos almacenados empregando PDO == |
| − | + | Os procedementos almacenados execútanse mediante a sentenza SQL CALL. | |
| − | + | : Por exemplo: | |
| − | + | ::<syntaxhighlight lang="php"> | |
| + | $stmt = $conex->prepare( | ||
| + | 'CALL CONSULTAR_LIBROS()' | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| + | </syntaxhighlight> | ||
| + | Se o procedemento devolve un conxunto de resultados podemos percorrelo da forma habitual: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | while ($fila = $stmt->fetch()) { | ||
| + | echo htmlspecialchars( | ||
| + | $fila['titulo'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ); | ||
} | } | ||
| − | + | </syntaxhighlight> | |
| − | + | === Procedementos con varios conxuntos de resultados === | |
| + | Un procedemento almacenado pode devolver máis dun conxunto de resultados. | ||
| + | Para pasar ao seguinte conxunto empregaremos: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | nextRowset() | ||
| + | </syntaxhighlight> | ||
| + | : Por exemplo: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'CALL PROCEDEMENTO_CON_VARIOS_SELECT()' | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| + | do { | ||
| + | while ($fila = $stmt->fetch()) { | ||
| + | |||
| + | // Procesar as filas deste conxunto | ||
} | } | ||
| − | + | } while ($stmt->nextRowset()); | |
| − | + | </syntaxhighlight> | |
| − | + | ||
| − | + | ||
| + | + Antes de executar outra consulta na mesma conexión debemos asegurarnos de que non queden resultados pendentes. | ||
| + | : Podemos liberar os resultados restantes mediante: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt->closeCursor(); | ||
| + | </syntaxhighlight> | ||
| + | : Isto é especialmente importante despois de chamadas a procedementos almacenados en MySQL. | ||
| + | |||
| + | |||
| + | : Por exemplo: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | <?php | ||
| + | // 1. Executamos o procedemento almacenado que devolve varios SELECTs | ||
| + | $stmt = $conex->prepare('CALL PROCEDEMENTO_CON_VARIOS_SELECT()'); | ||
| + | $stmt->execute(); | ||
| + | |||
| + | // 2. Procesamos só o primeiro conxunto de resultados (por exemplo, os primeiros 3 datos) | ||
| + | if ($fila = $stmt->fetch()) { | ||
| + | echo "Primeiro rexistro: " . $fila['nome'] . "<br>"; | ||
} | } | ||
| + | |||
| + | // 3. Pechamos o cursor para liberar a conexión | ||
| + | // Isto descarta calquera outra fila ou segundo resultset pendente | ||
| + | $stmt->closeCursor(); | ||
| + | |||
| + | // 4. Agora xa podemos executar outra consulta na mesma conexión sen erros | ||
| + | $outraConsulta = $conex->query('SELECT * FROM outra_taboa'); | ||
| + | $datos = $outraConsulta->fetchAll(); | ||
?> | ?> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | === Procedementos con parámetros de entrada === | ||
| + | Os parámetros de entrada enviaranse empregando marcadores: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'CALL PROCEDEMENTO_CON_PARAMETROS( | ||
| + | :dato1,:dato2 | ||
| − | + | )' | |
| − | + | ); | |
| + | $stmt->bindValue( | ||
| + | ':dato1', | ||
| + | $valorNum, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':dato2', | ||
| + | $valorTexto, | ||
| + | PDO::PARAM_STR | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| + | </syntaxhighlight> | ||
| + | : Desta forma non necesitamos construír a chamada mediante sprintf() nin concatenar os valores dentro da sentenza SQL. | ||
| + | === Procedementos con parámetros de saída en MySQL === | ||
| + | PDO define mecanismos xenéricos para parámetros de saída, pero o controlador '''PDO_MYSQL non xestiona correctamente PDO::PARAM_INPUT_OUTPUT'''. | ||
| + | Por este motivo, con MySQL podemos empregar unha variable de usuario de MySQL para recuperar un parámetro OUT. | ||
| + | Supoñamos este procedemento: | ||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | DELIMITER $$ | ||
| + | CREATE PROCEDURE CONSULTAR_NUM_LIBROS( | ||
| + | OUT p_num_libros INT | ||
| + | ) | ||
| + | BEGIN | ||
| + | SELECT COUNT(*) | ||
| + | INTO p_num_libros | ||
| + | FROM libros; | ||
| + | END$$ | ||
| + | DELIMITER ; | ||
| + | </syntaxhighlight> | ||
| + | Desde PHP podemos facer: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'CALL CONSULTAR_NUM_LIBROS( | ||
| + | @numero_libros | ||
| + | )' | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| + | /* | ||
| + | Liberamos todos os resultados da chamada | ||
| + | antes de executar outra sentenza SQL. | ||
| + | */ | ||
| + | $stmt->closeCursor(); | ||
| + | $stmt = $conex->query( | ||
| + | 'SELECT @numero_libros' | ||
| + | ); | ||
| + | $numLibros = $stmt->fetchColumn(); | ||
| + | printf( | ||
| + | '<h1>Número de libros: %d</h1>', | ||
| + | (int) $numLibros | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | : É importante chamar a closeCursor() antes do segundo SELECT, xa que MySQL pode manter conxuntos de resultados pendentes da chamada ao procedemento. | ||
| + | == Funcións almacenadas de MySQL == | ||
| + | As funcións de MySQL poden empregarse dentro dunha instrución SELECT. | ||
| + | Se a función non leva parámetros podemos facer: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->query( | ||
| + | 'SELECT CONSULTAR_NUM_LIBROS()' | ||
| + | ); | ||
| + | $numLibros = $stmt->fetchColumn(); | ||
| + | </syntaxhighlight> | ||
| + | Se leva parámetros empregaremos unha sentenza preparada: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'SELECT Suma(:dato1,:dato2 )' | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':dato1', | ||
| + | 5, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | '':dato2, | ||
| + | 7, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| + | $resultado = $stmt->fetchColumn(); | ||
| + | </syntaxhighlight> | ||
| + | : fetchColumn() resulta especialmente cómodo cando a consulta devolve un único valor. | ||
| − | * | + | == Paxinación de resultados empregando PDO == |
| + | Para paxinar resultados empregaremos: | ||
| + | :* COUNT(*) para coñecer o número total de rexistros. | ||
| + | :* LIMIT para indicar a cantidade de rexistros que queremos recuperar. | ||
| + | :* OFFSET, ou o primeiro parámetro de LIMIT, para indicar desde que rexistro queremos comezar. | ||
| + | Por exemplo: | ||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | SELECT id, titulo | ||
| + | FROM libros | ||
| + | ORDER BY titulo | ||
| + | LIMIT 10 OFFSET 20 | ||
| + | </syntaxhighlight> | ||
| + | : Esta consulta devolvería ata 10 rexistros empezando desde o rexistro situado na posición 20. | ||
| − | + | <br /> | |
| − | : | + | '''Número total de rexistros''' |
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->query( | ||
| + | 'SELECT COUNT(*) | ||
| + | FROM libros' | ||
| + | ); | ||
| + | $numLibros = (int) $stmt->fetchColumn(); | ||
| + | </syntaxhighlight> | ||
| + | <br /> | ||
| + | '''Páxina actual''' | ||
| + | O número de páxina recibido mediante GET é un dato externo e debe validarse. | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $paxinaActual = filter_input( | ||
| + | INPUT_GET, | ||
| + | 'paxina', | ||
| + | FILTER_VALIDATE_INT | ||
| + | ); | ||
| + | if ( | ||
| + | $paxinaActual === false || | ||
| + | $paxinaActual === null || | ||
| + | $paxinaActual < 1 | ||
| + | ) { | ||
| + | $paxinaActual = 1; | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | : htmlspecialchars() non serve para validar que un dato sexa un número. Empregarase posteriormente cando teñamos que inserir unha cadea dentro de HTML. | ||
| + | |||
| + | |||
| + | <br /> | ||
| + | '''Cálculo do número de páxinas''' | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | define( | ||
| + | 'CONST_NUM_LIBROS_PAXINA', | ||
| + | 10 | ||
| + | ); | ||
| + | $totalPaxinas = (int) ceil( | ||
| + | $numLibros / CONST_NUM_LIBROS_PAXINA | ||
| + | ); | ||
| + | </syntaxhighlight> | ||
| + | Podemos evitar que se solicite unha páxina superior á existente: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | if ( | ||
| + | $totalPaxinas > 0 && | ||
| + | $paxinaActual > $totalPaxinas | ||
| + | ) { | ||
| + | $paxinaActual = $totalPaxinas; | ||
| + | } | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | === | + | '''Calcular o desprazamento''' |
| + | ::<syntaxhighlight lang="php"> | ||
| + | $inicio = ( | ||
| + | $paxinaActual - 1 | ||
| + | ) * CONST_NUM_LIBROS_PAXINA; | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | : Por exemplo, se estamos: | ||
| + | :* Na páxina 1 => inicio = 0 | ||
| + | :* Na páxina 2 => inicio = 10 | ||
| + | :* Na páxina 3 => inicio = 20 | ||
| + | |||
| + | |||
| + | <br /> | ||
| + | '''Consulta paxinada''' | ||
| + | Neste caso indicaremos explicitamente que os parámetros son enteiros: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'SELECT id, titulo | ||
| + | FROM libros | ||
| + | ORDER BY titulo | ||
| + | LIMIT :limite | ||
| + | OFFSET :desprazamento' | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':limite', | ||
| + | CONST_NUM_LIBROS_PAXINA, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':desprazamento', | ||
| + | $inicio, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->execute(); | ||
| + | $libros = $stmt->fetchAll(); | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | |||
| + | * Despois podemos amosar os libros: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | if ($libros) { | ||
| + | echo "<table border='1'>"; | ||
| + | |||
| + | foreach ($libros as $libro) { | ||
| + | |||
| + | 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'; | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| − | |||
| − | |||
| − | |||
| + | <br /> | ||
| + | '''Enlaces da paxinación''' | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | echo '<div>'; | ||
| + | for ( | ||
| + | $cont = 1; | ||
| + | $cont <= $totalPaxinas; | ||
| + | $cont++ | ||
| + | ) { | ||
| + | if ($cont === $paxinaActual) { | ||
| + | printf( | ||
| + | '<strong>%d</strong> ', | ||
| + | $cont | ||
| + | ); | ||
| − | + | } else { | |
| − | |||
| − | + | printf( | |
| − | + | "<a href='?paxina=%d'>%d</a> ", | |
| − | + | $cont, | |
| − | </syntaxhighlight> | + | $cont |
| + | ); | ||
| + | } | ||
| + | } | ||
| + | echo '</div>'; | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | ''' | + | '''Sobre a optimización do número total de rexistros''' |
| − | :<syntaxhighlight lang=" | + | En xeral non deberiamos enviar o número total de rexistros na URL confiando despois nese valor: |
| − | < | + | ::<syntaxhighlight lang="text"> |
| − | + | ?paxina=2&numtotal=50 | |
| − | + | </syntaxhighlight> | |
| − | + | : Os datos enviados mediante GET poden ser modificados polo usuario. | |
| − | + | Tampouco debemos asumir que gardar o número total nunha variable de sesión é sempre unha optimización correcta, xa que a base de datos pode cambiar e o valor almacenado quedar desactualizado. | |
| + | Como primeira solución, realizaremos en cada petición: | ||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | SELECT COUNT(*) | ||
| + | FROM libros | ||
| + | </syntaxhighlight> | ||
| + | : Se nunha aplicación real esta operación chegase a ser un problema de rendemento, poderían estudarse posteriormente técnicas de caché ou outras estratexias de optimización. | ||
| + | |||
| + | == Xestión de excepcións == | ||
| − | |||
| + | * Nos exemplos completos empregaremos o seguinte esquema: | ||
| + | ::<syntaxhighlight lang="php"> | ||
try { | try { | ||
| − | $conex = new | + | $conex = new PDO($dsn, $usuario, $contrasinal, [ |
| − | $ | + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, |
| − | + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | |
| + | ]); | ||
| + | |||
| + | // Operacións coa base de datos... | ||
| + | |||
| + | } catch (PDOException $e) { | ||
| + | // Rexistrar o erro nun log (monolog, etc.) | ||
| + | error_log($e->getMessage()); | ||
| + | |||
| + | die('Erro relacionado coa base de datos.'); | ||
| + | } catch (Throwable $e) { | ||
| + | die('Produciuse un erro na aplicación.'); | ||
| + | } | ||
| + | |||
| + | // Non fai falta "finally {$conex = null;}": | ||
| + | // Se o script continúa, destruirasie cando cumpra o seu ciclo; | ||
| + | // se entra nos catch, o die() xa finaliza o proceso e libera a memoria. | ||
| + | </syntaxhighlight> | ||
| + | : PDOException permite tratar especificamente os erros de PDO. | ||
| + | : Throwable permite capturar outros erros ou excepcións producidos durante a execución. | ||
| + | : Os bloques catch deben colocarse desde o máis específico ao máis xeral. | ||
| + | |||
| + | |||
| + | |||
| + | <br /> | ||
| + | == Exercicios prácticos == | ||
| + | * Partimos de que xa temos creada a base de datos nunha [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operacións_BD_Mysql#Creando_as_t.C3.A1boas_en_Mysql sección anterior desta wiki] e gardados os datos necesarios para realizar a conexión no arquivo `bd.php`. | ||
| − | + | * '''IMPORTANTE:''' Nos exames, exercicios e prácticas avaliables empregaremos '''PDO''' para realizar as operacións contra a base de datos, salvo que no enunciado se indique expresamente o contrario. | |
| − | + | * Os exemplos con `mysqli` poden consultarse como información adicional, pero os exemplos de referencia dos apuntes serán os realizados mediante PDO. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | * Nos exemplos completos empregaremos o seguinte esquema: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | : No arquivo bd.php que se atopa fora da ruta /var/www/html gardaremos os datos da conexión e as consultar a empregar: | ||
| + | ::<syntaxhighlight lang="php"> | ||
<?php | <?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> | ||
| + | Nas páxinas PHP que van acceder á base de datos terán este esquema: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | try { | ||
| + | |||
| + | require '/var/www/confBD/bd.php'; | ||
| + | try { | ||
| − | + | $dsn = sprintf( | |
| − | === | + | 'mysql:host=%s;dbname=%s;charset=utf8mb4', |
| + | SERVIDOR, | ||
| + | BD | ||
| + | ); | ||
| + | |||
| + | $conex = new PDO( | ||
| + | $dsn, | ||
| + | USUARIO, | ||
| + | PASSWORD, | ||
| + | [ | ||
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, | ||
| + | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION | ||
| + | ] | ||
| + | ); | ||
| − | + | // 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.'); | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | } </syntaxhighlight> | ||
| + | :* `PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC` fai que, por defecto, as filas recuperadas mediante `fetch()` ou `fetchAll()` sexan arrays asociativos. | ||
| + | :* `PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION` fai que os erros producidos por PDO xeren unha excepción de tipo `PDOException`. | ||
| + | :* `PDOException` permítenos tratar especificamente os erros relacionados con PDO. | ||
| + | :* `Throwable` permite capturar outros erros ou excepcións producidos durante a execución da aplicación. | ||
| + | :* Os bloques `catch` deben ordenarse desde o máis específico ao máis xeral. | ||
| + | :* Nunha aplicación en produción non debemos amosar ao usuario a mensaxe devolta por `$e->getMessage()`. Podemos rexistrala mediante `error_log()` ou outro sistema de logs. | ||
<br /> | <br /> | ||
| − | |||
| − | * | + | === Caso Práctico 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''' |
| + | ::<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: | |
| + | :* 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''' | |
| − | : | + | ::<syntaxhighlight lang="php"> |
| − | |||
| − | |||
<?php | <?php | ||
| − | + | ||
| − | ini_set('display_errors', 1 | + | ini_set('display_errors', '1'); |
| − | |||
error_reporting(E_ALL); | error_reporting(E_ALL); | ||
| − | require | + | 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, | ||
| + | [ | ||
| + | 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 | + | <!DOCTYPE html> |
| − | <html | + | |
| − | + | <html lang="gl"> | |
| − | + | <head> | |
| − | + | <meta charset="UTF-8"> | |
| − | + | <title>Operacións BD</title> | |
| + | </head> | ||
| − | + | <body> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
<?php | <?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> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| − | + | * Neste caso empregamos `query()` porque a sentenza SQL non recibe ningún dato externo. | |
| − | |||
| − | + | * `fetchAll()` recupera todas as filas do resultado. | |
| − | |||
| + | : Esta solución é adecuada cando sabemos que o número de filas que imos recuperar é pequeno. | ||
| + | * Se o resultado puidese ter moitas filas, sería preferible procesalas unha a unha: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | while ($libro = $stmt->fetch()) { | ||
| − | + | echo htmlspecialchars( | |
| − | + | $libro['titulo'], | |
| − | + | ENT_QUOTES | ENT_SUBSTITUTE, | |
| − | + | 'UTF-8' | |
| − | + | ); | |
| − | : | + | |
| − | : | + | } </syntaxhighlight> |
| − | ::<syntaxhighlight lang=" | + | |
| − | + | * '''IMPORTANTE:''' Non empregaremos `rowCount()` para comprobar cantas filas devolveu unha sentenza `SELECT`. | |
| − | + | ||
| − | : | + | : Se realmente necesitamos coñecer o número de rexistros, empregaremos `COUNT(*)`. |
| + | |||
| + | <br /> | ||
| + | |||
| + | === Caso Práctico 2 === | ||
| + | |||
| + | * Imos recuperar todos os autores ordenados polos seus apelidos e nome. | ||
| + | |||
| + | * Cada autor aparecerá como un enlace co seguinte formato: | ||
| + | |||
| + | ::<syntaxhighlight lang="html"> <a href="detalle_autor.php?id=5"> | ||
| + | Apelidos, Nome </a> </syntaxhighlight> | ||
| + | |||
| + | * O identificador será enviado mediante a URL. | ||
| + | |||
| + | : 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''' | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | define( | ||
| + | 'CONSULTAR_AUTORES', | ||
| + | 'SELECT id, apelidos, nome | ||
| + | FROM autores | ||
| + | ORDER BY apelidos, nome' | ||
| + | ); </syntaxhighlight> | ||
| + | ==== Solución Caso Práctico 2 empregando PDO ==== | ||
| − | + | '''Arquivo: consultar_autores.php''' | |
| − | |||
| − | |||
| − | : | + | ::<syntaxhighlight lang="php"> |
| − | |||
<?php | <?php | ||
| − | + | ||
| − | ini_set('display_errors', 1 | + | ini_set('display_errors', '1'); |
| − | |||
error_reporting(E_ALL); | error_reporting(E_ALL); | ||
| − | require | + | require '/var/www/confBD/bd.php'; |
try { | try { | ||
| − | $conex = new | + | |
| − | $conex-> | + | $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 | + | <!DOCTYPE html> |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <html lang="gl"> | |
| − | + | <head> | |
| − | + | <meta charset="UTF-8"> | |
| − | + | <title>Autores</title> | |
| − | + | </head> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | < | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <body> | |
| − | |||
<?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.'; | ||
| + | |||
} | } | ||
| − | + | ||
| − | + | ?> | |
| − | + | ||
| − | + | </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 /> | ||
| + | === Consulta enviando parámetros de tipo numérico === | ||
| + | * Cando unha consulta necesita recibir datos externos empregaremos unha '''sentenza preparada''' mediante: | ||
| − | < | + | ::<syntaxhighlight lang="php"> |
| − | * | + | prepare() |
| + | execute() </syntaxhighlight> | ||
| + | |||
| + | * Por exemplo: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | SELECT col1, col2 | ||
| + | FROM TABOA_1 | ||
| + | WHERE col1 = :valor </syntaxhighlight> | ||
| + | |||
| + | * Definimos a consulta: | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | define( | ||
| + | 'CONSULTA_CON_PARAM_NUMERICO', | ||
| + | 'SELECT col1, col2 | ||
| + | FROM TABOA_1 | ||
| + | WHERE col1 = :valor' | ||
| + | ); </syntaxhighlight> | ||
| + | |||
| + | * Podemos executala desta forma: | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | CONSULTA_CON_PARAM_NUMERICO | ||
| + | ); | ||
| + | |||
| + | $stmt->execute([ | ||
| + | 'valor' => 5 | ||
| + | ]); </syntaxhighlight> | ||
| + | |||
| + | * O nome do parámetro debe coincidir co indicado na sentenza SQL. | ||
| + | |||
| + | : Se a consulta emprega: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | :valor </syntaxhighlight> | ||
| + | : debemos empregar: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt->execute([ | ||
| + | 'valor' => 5 | ||
| + | ]); </syntaxhighlight> | ||
| + | ==== Indicar explicitamente o tipo de dato ==== | ||
| + | * Cando enviamos os valores directamente mediante o array de `execute()`, PDO trátaos por defecto como `PDO::PARAM_STR`. | ||
| − | + | : Na maioría das consultas MySQL realizará as conversións necesarias segundo o contexto. | |
| − | : | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | * Se queremos indicar explicitamente o tipo empregaremos `bindValue()`: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | < | + | ::<syntaxhighlight lang="php"> |
| + | $stmt = $conex->prepare( | ||
| + | CONSULTA_CON_PARAM_NUMERICO | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':valor', | ||
| + | 5, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| + | $stmt->execute(); </syntaxhighlight> | ||
| + | * Algúns dos tipos dispoñibles son: | ||
| + | ::<syntaxhighlight lang="text"> | ||
| + | PDO::PARAM_INT | ||
| + | PDO::PARAM_STR | ||
| + | PDO::PARAM_BOOL | ||
| + | PDO::PARAM_NULL </syntaxhighlight> | ||
| + | * É especialmente recomendable indicar explicitamente o tipo cando este teña importancia para a consulta, por exemplo con `LIMIT` ou `OFFSET`. | ||
<br /> | <br /> | ||
| − | |||
| − | + | ==== 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'. | ||
| − | :::<syntaxhighlight lang=" | + | [[Imagen:Php_BD_9.jpg|800px|center]] |
| − | + | ||
| + | |||
| + | |||
| + | |||
| + | * Primeiro definimos as consultas. | ||
| + | |||
| + | '''Arquivo: bd.php''' | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | define( | ||
| + | 'CONSULTAR_LIBROS', | ||
| + | 'SELECT id, titulo | ||
| + | FROM libros | ||
| + | ORDER BY titulo' | ||
| + | ); | ||
| − | define (' | + | 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/' | ||
| + | ); </syntaxhighlight> | ||
| − | + | ===== Solución empregando PDO ===== | |
| − | + | : Fixarse que | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | '''Arquivo: consultar_exemplares_por_libro.php''' | ||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | |||
<?php | <?php | ||
| − | + | ||
| − | ini_set('display_errors', 1 | + | ini_set('display_errors', '1'); |
| − | |||
error_reporting(E_ALL); | error_reporting(E_ALL); | ||
| − | require | + | require '/var/www/confBD/bd.php'; |
| + | |||
| + | $libros = []; | ||
| + | $exemplares = []; | ||
| + | |||
| + | $libroSeleccionado = filter_input( | ||
| + | INPUT_POST, | ||
| + | 'lstLibros', | ||
| + | FILTER_VALIDATE_INT | ||
| + | ); | ||
try { | try { | ||
| − | $conex = new | + | |
| − | $conex-> | + | $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 | + | <!DOCTYPE html> |
| − | <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): ?> | |
| − | <?php | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <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 | + | <?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> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| − | * | + | * Neste exemplo empregamos `filter_input()` con `FILTER_VALIDATE_INT` para comprobar que o dato recibido é un número enteiro válido. |
| + | |||
| + | * Despois utilizamos unha sentenza preparada e: | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | PDO::PARAM_INT </syntaxhighlight> | ||
| + | : 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. | |
| + | [[Imagen:Php_BD_12.jpg|800px|center]] | ||
| + | '''Arquivo: bd.php''' | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | define( | ||
| + | 'CONSULTAR_EXEMPLARES_POR_LIBRO', | ||
| + | 'SELECT exemplares.libro_id, | ||
| + | exemplares.id_exemplar, | ||
| + | exemplares.imaxe, | ||
| + | exemplares.prezo, | ||
| + | estados.descripcion AS desc_estado | ||
| + | FROM exemplares | ||
| + | INNER JOIN estados | ||
| + | ON estados.id = exemplares.estado_id | ||
| + | WHERE exemplares.libro_id = :libro_id | ||
| + | ORDER BY desc_estado' | ||
| + | ); </syntaxhighlight> | ||
| + | * Para amosar o estado: | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | echo htmlspecialchars( | ||
| + | $exemplar['desc_estado'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ); </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | |||
| − | + | ==== Exercicio proposto ==== | |
| − | |||
| − | |||
| + | * Crea unha páxina chamada `consultar_exemplares_por_estado.php`. | ||
| + | * Debe: | ||
| + | :* Amosar os estados da táboa `ESTADOS` mediante botóns de opción (`radio`). | ||
| + | :* Ao seleccionar un estado, realizarase automaticamente o envío do formulario. | ||
| + | :* O estado seleccionado debe manterse marcado. | ||
| + | :* Amosaranse os exemplares correspondentes ao estado seleccionado. | ||
| + | :* A consulta deberá realizarse mediante PDO e unha sentenza preparada. | ||
| + | :* O identificador do estado deberá validarse como número enteiro. | ||
| + | :* Empregarase `PDO::PARAM_INT` para enviar o parámetro. | ||
| + | [[Imagen:Php_BD_13.jpg|800px|center]] | ||
| − | < | + | :<u>Nota:</u> Podedes darlle un formato máis 'bonito' con CSS. |
| − | + | ||
| + | |||
| + | * Consultas propostas: | ||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | ::<syntaxhighlight lang=" | + | define( |
| − | define ('CONSULTAR_ESTADOS',' | + | 'CONSULTAR_ESTADOS', |
| − | + | 'SELECT id, descripcion | |
| − | + | FROM estados | |
| − | + | ORDER BY descripcion' | |
| − | + | ); | |
| − | </syntaxhighlight> | + | 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 /> | |
| + | === 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: |
| − | + | [[Imagen:Php_BD_11.jpg|800px|center]] | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | : O usuario introducirá nunha caixa de texto o nome, os apelidos ou unha parte deles. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | : Por exemplo, se introduce: | |
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="text"> | |
| − | + | gar </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | </ | ||
| + | : poderían atoparse autores como `García`, `Vargas`, etc., dependendo dos datos existentes na base de datos. | ||
| − | + | * Para realizar a busca empregaremos: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | :* Un formulario HTML con método `POST`. | ||
| + | :* Unha consulta SQL co operador `LIKE`. | ||
| + | :* Unha '''sentenza preparada PDO''' para enviar o texto introducido polo usuario. | ||
| + | :* O parámetro `PDO::PARAM_STR`, xa que o dato enviado é unha cadea. | ||
| + | :* `fetchAll()` para recuperar os resultados. | ||
| + | :* `htmlspecialchars()` cando amosemos datos dentro do HTML. | ||
| + | * 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: | |
| + | ::<syntaxhighlight lang="text"> | ||
| + | detalle_libro.php </syntaxhighlight> | ||
| + | : á que enviaremos o identificador do libro mediante `GET`. | ||
| + | : Para abrir esa páxina nunha nova xanela empregaremos JavaScript e a función `window.open()`. | ||
| − | + | : Polo tanto, o funcionamento xeral será: | |
| − | |||
| − | + | ::<syntaxhighlight lang="text"> | |
| + | Usuario introduce o nome do autor | ||
| + | ↓ | ||
| + | Validamos o texto | ||
| + | ↓ | ||
| + | prepare() + bindValue() | ||
| + | ↓ | ||
| + | Consulta con LIKE | ||
| + | ↓ | ||
| + | Recuperamos os libros | ||
| + | ↓ | ||
| + | Amosamos autor e título | ||
| + | ↓ | ||
| + | Preme sobre un título | ||
| + | ↓ | ||
| + | window.open() | ||
| + | ↓ | ||
| + | detalle_libro.php?libro=ID </syntaxhighlight> | ||
| + | ===== Arquivo bd.php ===== | ||
| − | + | * Engadimos a consulta que permite buscar libros polo nome ou apelidos dun autor: | |
| + | ::<syntaxhighlight lang="php"> | ||
| + | define( | ||
| + | 'CONSULTAR_LIBROS_POR_AUTOR', | ||
| + | "SELECT | ||
| + | CONCAT( | ||
| + | autores.apelidos, | ||
| + | ', ', | ||
| + | autores.nome | ||
| + | ) AS nome_autor, | ||
| + | libros.id AS id_libro, | ||
| + | libros.titulo | ||
| + | FROM libros | ||
| + | INNER JOIN libro_autor | ||
| + | ON libro_autor.libro_id = libros.id | ||
| + | INNER JOIN autores | ||
| + | ON libro_autor.autor_id = autores.id | ||
| + | WHERE CONCAT( | ||
| + | autores.nome, | ||
| + | ' ', | ||
| + | autores.apelidos | ||
| + | ) LIKE :autor | ||
| + | ORDER BY nome_autor, libros.titulo" | ||
| + | ); </syntaxhighlight> | ||
| − | * | + | * A consulta une as táboas: |
| − | : | ||
| − | + | ::<syntaxhighlight lang="text"> | |
| + | libros | ||
| + | libro_autor | ||
| + | autores </syntaxhighlight> | ||
| − | : | + | : para obter os libros correspondentes a cada autor. |
| − | |||
| − | |||
| − | + | * Mediante: | |
| − | |||
| − | : | ||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | CONCAT( | ||
| + | autores.nome, | ||
| + | ' ', | ||
| + | autores.apelidos | ||
| + | ) </syntaxhighlight> | ||
| − | : | + | : construímos unha cadea formada polo nome e os apelidos do autor. |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | * Sobre esa cadea aplicamos: | ||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | LIKE :autor </syntaxhighlight> | ||
| + | : sendo `:autor` o parámetro que enviaremos posteriormente desde PDO. | ||
| − | + | * O parámetro debe representar o valor completo. | |
| − | |||
| − | + | : Polo tanto, non escribiremos: | |
| − | : | ||
| − | |||
| − | |||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | LIKE '%:autor%' </syntaxhighlight> | ||
| − | + | : senón que os caracteres `%` serán engadidos ao valor desde PHP: | |
| − | : | + | ::<syntaxhighlight lang="php"> |
| + | '%' . $textoBuscar . '%' </syntaxhighlight> | ||
| − | + | ===== Solución completa ===== | |
| − | |||
| − | |||
| − | |||
| − | + | '''Arquivo: buscar_libros_por_autor.php''' | |
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="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 | + | <!DOCTYPE html> |
| − | + | ||
| − | <html | + | <html lang="gl"> |
| + | |||
<head> | <head> | ||
| − | <meta | + | |
| − | <title> | + | <meta charset="UTF-8"> |
| − | <script | + | <title>Buscar libros por autor</title> |
| − | function abrirVentana( | + | <script> |
| − | window.open( | + | /* |
| + | * Recibe a URL que queremos abrir | ||
| + | * e solicita ao navegador unha nova xanela. | ||
| + | */ | ||
| + | function abrirVentana(url) { | ||
| + | |||
| + | window.open( | ||
| + | url, | ||
| + | '_blank', | ||
| + | 'popup,width=600,height=400,noopener' | ||
| + | ); | ||
} | } | ||
</script> | </script> | ||
| − | |||
</head> | </head> | ||
| − | < | + | <body> |
| − | + | ||
| − | Introduce o nome do autor | + | <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> | </html> | ||
| + | </syntaxhighlight> | ||
| − | + | ===== Explicación da solución ===== | |
| − | + | ||
| − | + | * Recuperamos o texto enviado polo formulario mediante: | |
| − | + | ||
| − | + | ::<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 /> | ||
| + | '''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> | ||
| + | : A esixencia de tres caracteres é unha regra da nosa aplicación. | ||
| + | : Non é unha medida para evitar inxección SQL. | ||
<br /> | <br /> | ||
| − | === | + | '''Preparación da consulta''' |
| + | |||
| + | * A consulta recibe datos procedentes do usuario, polo que debemos empregar unha sentenza preparada: | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | CONSULTAR_LIBROS_POR_AUTOR | ||
| + | ); </syntaxhighlight> | ||
| + | |||
| + | |||
| + | <br /> | ||
| + | '''Asociación do parámetro''' | ||
| + | |||
| + | * Asociamos o valor ao parámetro `:autor`: | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt->bindValue( | ||
| + | ':autor', | ||
| + | '%' . $textoBuscar . '%', | ||
| + | PDO::PARAM_STR | ||
| + | ); </syntaxhighlight> | ||
| + | |||
| + | * Indicamos: | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | PDO::PARAM_STR </syntaxhighlight> | ||
| + | |||
| + | : porque estamos enviando unha cadea. | ||
| − | |||
| − | |||
| − | + | <br /> | |
| − | + | '''Funcionamento de LIKE''' | |
| − | |||
| + | * Os caracteres `%` actúan como comodíns. | ||
| − | + | : Se o usuario introduce: | |
| − | + | ::<syntaxhighlight lang="text"> | |
| − | + | gar </syntaxhighlight> | |
| − | + | : PHP enviará: | |
| − | : | ||
| − | + | ::<syntaxhighlight lang="text"> | |
| − | ::<syntaxhighlight lang=" | + | %gar% </syntaxhighlight> |
| − | |||
| − | + | * Desta forma poderán coincidir cadeas nas que `gar` apareza ao principio, no medio ou ao final. | |
| − | |||
| − | |||
| − | |||
| − | |||
| + | <br /> | ||
| + | '''Execución e recuperación dos resultados''' | ||
| − | + | * Executamos: | |
| − | |||
| − | |||
| − | : | + | ::<syntaxhighlight lang="php"> |
| + | $stmt->execute(); </syntaxhighlight> | ||
| + | * E recuperamos os resultados: | ||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | + | $libros = $stmt->fetchAll(); </syntaxhighlight> | |
| − | : | + | * Como na conexión configuramos: |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php"> | |
| + | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC </syntaxhighlight> | ||
| − | + | : cada fila recuperada será un array asociativo. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | <br /> | ||
| + | '''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"> | |
| − | ::<syntaxhighlight lang=" | + | García, Ana | Libro A |
| − | + | | Libro B | |
| − | + | | Libro C | |
| − | + | Pérez, Luís | Libro D </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | * Para iso gardamos o último autor procesado: | |
| − | </syntaxhighlight> | + | ::<syntaxhighlight lang="php"> |
| + | $ultimoAutor = ''; </syntaxhighlight> | ||
| + | * En cada iteración comparamos: | ||
| − | : | + | ::<syntaxhighlight lang="php"> |
| + | if ($ultimoAutor ===$libro['nome_autor']) { | ||
| + | $autorAmosar = ''; | ||
| − | + | } else { | |
| − | + | $autorAmosar = $libro['nome_autor']; | |
| + | } | ||
| + | </syntaxhighlight> | ||
| + | * E ao rematar gardamos o autor actual: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $ultimoAutor = | ||
| + | $libro['nome_autor']; </syntaxhighlight> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <br /> | |
| + | '''Abrir o detalle dun libro''' | ||
| − | + | * Cada título será un enlace á páxina: | |
| + | ::<syntaxhighlight lang="text">detalle_libro.php </syntaxhighlight> | ||
| + | * O identificador enviarase mediante `GET`: | ||
| + | ::<syntaxhighlight lang="text">detalle_libro.php?libro=5 </syntaxhighlight> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | * O enlace xerado será: | |
| + | ::<syntaxhighlight lang="php"> | ||
| + | <a href="detalle_libro.php?libro=<?= (int) $libro['id_libro'] ?>" onclick="abrirVentana(this.href); return false;"> | ||
| + | </syntaxhighlight> | ||
| − | + | * Mediante: | |
| − | : | + | ::<syntaxhighlight lang="javascript">this.href </syntaxhighlight> |
| + | : obtemos a URL real almacenada no atributo `href`. | ||
| + | * Chamamos despois á función: | ||
| + | ::<syntaxhighlight lang="javascript">abrirVentana(this.href); </syntaxhighlight> | ||
| − | + | * Finalmente: | |
| − | |||
| − | + | ::<syntaxhighlight lang="javascript">return false; </syntaxhighlight> | |
| + | : evita que o navegador abra tamén o enlace na xanela actual. | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <br /> | |
| − | + | '''Función JavaScript''' | |
| − | |||
| + | * A función empregada é: | ||
| − | + | ::<syntaxhighlight lang="javascript"> | |
| − | + | function abrirVentana(url) { | |
| − | < | ||
| + | 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=" | + | ::<syntaxhighlight lang="text"> |
| − | + | width=600 | |
| − | </syntaxhighlight> | + | height=400 </syntaxhighlight> |
| + | * A opción: | ||
| + | ::<syntaxhighlight lang="text"> | ||
| + | noopener </syntaxhighlight> | ||
| + | : evita que a nova xanela poida acceder á xanela orixinal mediante `window.opener`. | ||
| + | '''NOTA:''' O navegador ten a decisión final sobre se a páxina se abre nunha nova xanela ou nunha pestana e pode ignorar algunhas características solicitadas. | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | <br /> | |
| + | '''Vantaxe de empregar un href real''' | ||
| − | + | * Non empregamos: | |
| − | + | ::<syntaxhighlight lang="html"> <a href="#"> </syntaxhighlight> | |
| − | |||
| − | + | * Empregamos: | |
| − | + | ::<syntaxhighlight lang="html"> <a href="detalle_libro.php?libro=5"> </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | </syntaxhighlight> | ||
| − | : | + | : Desta forma, se JavaScript non funciona ou está desactivado, o enlace segue sendo válido e o usuario poderá acceder igualmente ao detalle do libro. |
| − | |||
| − | |||
<br /> | <br /> | ||
| − | + | '''Páxina detalle_libro.php''' | |
| + | |||
| + | * A páxina de detalle recibirá o identificador do libro mediante: | ||
| + | |||
| + | ::<syntaxhighlight lang="text">$_GET['libro'] </syntaxhighlight> | ||
| + | |||
| + | * Como é un dato externo, debemos validalo: | ||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | : | + | $libroId = filter_input( |
| − | + | INPUT_GET, | |
| − | + | 'libro', | |
| − | + | FILTER_VALIDATE_INT | |
| + | ); | ||
| − | + | if ( | |
| + | $libroId === false || | ||
| + | $libroId === null || | ||
| + | $libroId < 1 | ||
| + | ) { | ||
| − | + | die('Identificador de libro non válido.'); | |
| − | + | } | |
| − | + | </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(); | |
| − | |||
| + | $libro = $stmt->fetch(); | ||
| + | </syntaxhighlight> | ||
| − | : | + | * Se queremos recuperar tamén os exemplares dese libro faremos outra consulta preparada: |
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | + | $stmt = $conex->prepare( | |
| + | 'SELECT id_exemplar,imaxe,prezo,estado_id | ||
| + | FROM exemplares | ||
| + | WHERE libro_id = :libro_id | ||
| + | ORDER BY id_exemplar' | ||
| + | ); | ||
| − | + | $stmt->bindValue(':libro_id',$libroId,PDO::PARAM_INT); | |
| − | + | $stmt->execute(); | |
| − | |||
| − | |||
| − | + | $exemplares = $stmt->fetchAll(); | |
| − | </syntaxhighlight> | + | </syntaxhighlight> |
| + | ===== Resumo do proceso ===== | ||
| − | + | ::<syntaxhighlight lang="text"> | |
| − | ::<syntaxhighlight lang=" | + | $_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> | ||
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| − | + | * Debemos diferenciar claramente: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | < | + | ::<syntaxhighlight lang="text"> |
| − | + | ! Operación | |
| − | |||
| + | | ! Finalidade | | ||
| + | | ------------------------------------------------------ | | ||
| + | | `trim()` e comprobación da lonxitude | | ||
| + | | Validar os datos segundo as regras da aplicación. | | ||
| + | | - | | ||
| + | | `prepare()` + `bindValue()` | | ||
| + | | Separar os datos da estrutura da sentenza SQL. | | ||
| + | | - | | ||
| + | | `PDO::PARAM_STR` | | ||
| + | | Indicar que o parámetro enviado é unha cadea. | | ||
| + | | - | | ||
| + | | `htmlspecialchars()` | | ||
| + | | Escapar datos cando se introducen dentro de HTML. | | ||
| + | | - | | ||
| + | | `window.open()` | | ||
| + | | Abrir o detalle do libro nunha nova xanela ou pestana. | | ||
| + | | - | | ||
| + | | `FILTER_VALIDATE_INT` | | ||
| + | | Validar o identificador recibido por `GET`. | | ||
| + | | } | | ||
| + | </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | |||
| − | * | + | === 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: | |
| − | + | ||
| − | ::<syntaxhighlight lang=" | + | ::<syntaxhighlight lang="sql"> |
| + | CALL NOME_PROCEDEMENTO() </syntaxhighlight> | ||
| + | |||
| + | * Por exemplo, en MySQL: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql"> | ||
DELIMITER $$ | DELIMITER $$ | ||
| − | CREATE PROCEDURE | + | CREATE PROCEDURE CONSULTAR_LIBROS() |
BEGIN | BEGIN | ||
| − | + | SELECT id, titulo | |
| − | + | FROM libros | |
| − | + | ORDER BY titulo; | |
| − | |||
| − | |||
END$$ | END$$ | ||
| − | |||
| − | + | DELIMITER ; </syntaxhighlight> | |
| − | + | ||
| + | ==== Empregando PDO ==== | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'CALL CONSULTAR_LIBROS()' | ||
| + | ); | ||
| + | |||
| + | $stmt->execute(); | ||
| + | |||
| + | $libros = $stmt->fetchAll(); | ||
| + | |||
| + | $stmt->closeCursor(); </syntaxhighlight> | ||
| + | * `closeCursor()` libera os resultados pendentes asociados á chamada. | ||
| + | : É especialmente recomendable ao traballar con procedementos almacenados en MySQL antes de executar outra consulta na mesma conexión. | ||
| − | + | ==== Caso Práctico: Consulta chamando a procedementos almacenados ==== | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | : | + | * Definimos o nome do procedemento: |
| + | ::<syntaxhighlight lang="php"> | ||
| + | define( | ||
| + | 'PROCEDEMENTO_CONSULTAR_LIBROS', | ||
| + | 'CONSULTAR_LIBROS' | ||
| + | ); </syntaxhighlight> | ||
| + | * Realizamos a chamada: | ||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | ::<syntaxhighlight lang=" | + | $stmt = $conex->prepare( |
| − | + | 'CALL ' . | |
| − | + | PROCEDEMENTO_CONSULTAR_LIBROS . | |
| − | + | '()' | |
| − | + | ); | |
| − | |||
| − | + | $stmt->execute(); | |
| − | + | $libros = $stmt->fetchAll(); | |
| − | |||
| − | |||
| − | + | $stmt->closeCursor(); </syntaxhighlight> | |
| − | + | * 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: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | < | + | ::<syntaxhighlight lang="php"> |
| − | + | if ($libros) { | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | // Hai resultados | ||
| + | } else { | ||
| + | // Non hai resultados | ||
| + | } </syntaxhighlight> | ||
<br /> | <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: | ||
| − | + | ::<syntaxhighlight lang="sql"> | |
| − | ::<syntaxhighlight lang=" | ||
DELIMITER $$ | DELIMITER $$ | ||
| − | CREATE PROCEDURE | + | CREATE PROCEDURE PROCEDEMENTO_CON_PARAMETROS( |
| + | IN p_num INT, | ||
| + | IN p_text VARCHAR(100) | ||
| + | ) | ||
BEGIN | BEGIN | ||
| − | + | SELECT col1, col2 | |
| − | + | FROM TABOA_1 | |
| − | + | WHERE col1 = p_num | |
| − | + | AND col2 = p_text; | |
| − | END | + | END$$ |
| − | </syntaxhighlight> | + | |
| + | DELIMITER ; </syntaxhighlight> | ||
| + | |||
| + | ==== Empregando PDO ==== | ||
| − | + | * Preparamos a chamada igual que calquera outra sentenza parametrizada: | |
| − | |||
| − | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->prepare( | ||
| + | 'CALL PROCEDEMENTO_CON_PARAMETROS( | ||
| + | :param_num, | ||
| + | :param_text | ||
| + | )' | ||
| + | ); | ||
| + | $stmt->bindValue( | ||
| + | ':param_num', | ||
| + | $valorNum, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| − | + | $stmt->bindValue( | |
| − | : | + | ':param_text', |
| − | :: | + | $valorTexto, |
| − | + | PDO::PARAM_STR | |
| − | + | ); | |
| − | |||
| + | $stmt->execute(); | ||
| − | + | $filas = $stmt->fetchAll(); | |
| − | |||
| − | + | $stmt->closeCursor(); </syntaxhighlight> | |
| − | |||
| − | |||
| − | + | * 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. | |
| − | + | ::<syntaxhighlight lang="sql"> | |
| − | + | grant execute on php.* to 'user_php'@'localhost'; | |
| − | + | flush privileges; | |
| − | + | </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | |||
| + | * Procedemento almacenado (isto tedes que executalo en Mysql empregando a ferramenta Mysql WorkBench ou PhpMyAdmin): | ||
| − | + | ::<syntaxhighlight lang="sql"> | |
| + | 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 ; </syntaxhighlight> | ||
| − | + | * Chamada desde PDO cambiando no exercicio anterior a chamada ao SELECT pola chamada a este procedemento almacenado: | |
| − | |||
| − | + | ::<syntaxhighlight lang="php" highlight="23-25,58"> | |
| − | + | if ($_SERVER['REQUEST_METHOD'] === 'POST') { | |
| − | |||
| − | |||
| − | + | /* | |
| − | + | * Comprobamos que se introduciu algún texto. | |
| − | + | */ | |
| − | + | if ($textoBuscar === '') { | |
| − | |||
| − | |||
| − | |||
| − | + | $erro = 'Debes introducir un texto para realizar a busca.'; | |
| − | |||
| + | /* | ||
| + | * Como 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.'; | |
| − | + | } | |
| − | + | } | |
| + | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | <br /> | ||
| + | |||
| + | === Consulta chamando a procedementos almacenados con parámetros de saída === | ||
| + | * Un parámetro `OUT` permite que un procedemento devolva un valor. | ||
| − | * | + | * Por exemplo: |
| − | + | ||
| − | ::<syntaxhighlight lang=" | + | ::<syntaxhighlight lang="sql"> |
| − | + | DELIMITER $$ | |
| − | + | ||
| − | + | CREATE PROCEDURE CONSULTAR_NUM_LIBROS( | |
| − | + | OUT p_num_libros INT | |
| − | + | ) | |
| + | BEGIN | ||
| + | |||
| + | SELECT COUNT(*) | ||
| + | INTO p_num_libros | ||
| + | FROM libros; | ||
| + | |||
| + | END$$ | ||
| + | |||
| + | DELIMITER ; </syntaxhighlight> | ||
| + | |||
| + | * Con PDO e MySQL podemos empregar unha variable de usuario de MySQL: | ||
| − | + | ::<syntaxhighlight lang="php"> | |
| + | $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(); </syntaxhighlight> | ||
| + | * Podemos amosar o resultado: | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | printf( | ||
| + | '<h1>Número de libros á venda: %d</h1>', | ||
| + | $numLibros | ||
| + | ); </syntaxhighlight> | ||
<br /> | <br /> | ||
| + | |||
=== Consulta chamando a funcións === | === Consulta chamando a funcións === | ||
| − | * | + | * 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: | ||
| − | + | ::<syntaxhighlight lang="sql"> | |
| − | ::<syntaxhighlight lang=" | ||
DELIMITER $$ | DELIMITER $$ | ||
| − | CREATE FUNCTION | + | CREATE FUNCTION CONSULTAR_NUM_LIBROS() |
| + | RETURNS INT | ||
| + | READS SQL DATA | ||
BEGIN | BEGIN | ||
| − | |||
| − | + | DECLARE num_libros INT; | |
| − | + | ||
| − | + | SELECT COUNT(*) | |
| − | + | INTO num_libros | |
| + | FROM libros; | ||
| − | + | RETURN num_libros; | |
| + | END$$ | ||
| − | + | DELIMITER ; </syntaxhighlight> | |
| − | $$ | + | |
| − | </syntaxhighlight> | + | ==== Empregando PDO ==== |
| + | |||
| + | * Podemos chamar á función mediante un `SELECT`: | ||
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->query( | ||
| + | 'SELECT CONSULTAR_NUM_LIBROS()' | ||
| + | ); | ||
| + | |||
| + | $numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight> | ||
| + | |||
| + | * `fetchColumn()` resulta especialmente útil cando unha consulta devolve un único valor. | ||
| + | |||
| + | ==== Funcións con parámetros ==== | ||
| + | |||
| + | * Supoñamos unha función: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | SELECT Suma(5, 7) </syntaxhighlight> | ||
| − | : | + | * Desde PDO: |
| + | ::<syntaxhighlight lang="php"> | ||
| + | $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(); </syntaxhighlight> | |
| + | <br /> | ||
| − | + | === 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: | |
| − | |||
| − | |||
| − | + | ::<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> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | : Devolverá os dous seguintes. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ==== Pasos necesarios para realizar a paxinación ==== | |
| + | * Necesitamos: | ||
| + | :# Coñecer o número total de rexistros. | ||
| + | :# Establecer cantos rexistros se amosarán por páxina. | ||
| + | :# Coñecer a páxina actual. | ||
| + | :# Calcular o número total de páxinas. | ||
| + | :# Calcular o desprazamento (`OFFSET`). | ||
| + | :# Realizar a consulta correspondente. | ||
<br /> | <br /> | ||
| − | + | '''Número total de rexistros''' | |
| + | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $stmt = $conex->query( | ||
| + | 'SELECT COUNT(*) | ||
| + | FROM libros' | ||
| + | ); | ||
| + | |||
| + | $numLibros = (int) $stmt->fetchColumn(); </syntaxhighlight> | ||
| + | |||
| − | |||
| − | |||
| − | + | <br /> | |
| − | + | '''Número de rexistros por páxina''' | |
| − | |||
| − | + | ::<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`. | ||
| + | ::<syntaxhighlight lang="php"> | ||
| + | $paxinaActual = filter_input( | ||
| + | INPUT_GET, | ||
| + | 'paxina', | ||
| + | FILTER_VALIDATE_INT | ||
| + | ); | ||
| − | + | if ( | |
| − | + | $paxinaActual === false || | |
| − | + | $paxinaActual === null || | |
| − | + | $paxinaActual < 1 | |
| − | + | ) { | |
| − | + | $paxinaActual = 1; | |
| + | } </syntaxhighlight> | ||
| − | * | + | * `htmlspecialchars()` non serve para validar que un dato sexa numérico. |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | : Neste caso empregamos `FILTER_VALIDATE_INT`. | |
| − | |||
| − | |||
| − | |||
| − | + | <br /> | |
| − | + | '''Número total de páxinas''' | |
| − | |||
| − | |||
| − | < | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php"> | |
| + | $totalPaxinas = (int) ceil( | ||
| + | $numLibros / | ||
| + | CONST_NUM_LIBROS_PAXINA | ||
| + | ); </syntaxhighlight> | ||
| − | + | * Podemos evitar que o usuario solicite unha páxina superior á existente: | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | ::<syntaxhighlight lang="php"> | ||
| + | if ( | ||
| + | $totalPaxinas > 0 && | ||
| + | $paxinaActual > $totalPaxinas | ||
| + | ) { | ||
| + | $paxinaActual = $totalPaxinas; | ||
| + | } </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | + | '''Calcular o OFFSET''' | |
| − | * | + | ::<syntaxhighlight lang="php"> |
| − | + | $inicio = ( | |
| + | $paxinaActual - 1 | ||
| + | ) * CONST_NUM_LIBROS_PAXINA; </syntaxhighlight> | ||
| − | |||
| − | |||
| − | + | <br /> | |
| − | + | '''Consulta paxinada''' | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | :: | + | * Como `LIMIT` e `OFFSET` necesitan valores numéricos, indicaremos explicitamente o tipo mediante `PDO::PARAM_INT`. |
| − | + | ::<syntaxhighlight lang="php"> | |
| − | ::<syntaxhighlight lang=" | + | $stmt = $conex->prepare( |
| − | SELECT id,titulo | + | 'SELECT id, titulo |
FROM libros | FROM libros | ||
| − | ORDER | + | ORDER BY titulo |
| − | LIMIT | + | LIMIT :limite |
| − | + | OFFSET :inicio' | |
| − | + | ); | |
| − | |||
| + | $stmt->bindValue( | ||
| + | ':limite', | ||
| + | CONST_NUM_LIBROS_PAXINA, | ||
| + | PDO::PARAM_INT | ||
| + | ); | ||
| − | + | $stmt->bindValue( | |
| − | : | + | ':inicio', |
| − | + | $inicio, | |
| − | + | PDO::PARAM_INT | |
| − | :: | + | ); |
| − | |||
| + | $stmt->execute(); | ||
| + | $libros = $stmt->fetchAll(); </syntaxhighlight> | ||
<br /> | <br /> | ||
| − | + | '''Amosar os resultados''' | |
| + | ::<syntaxhighlight lang="php"> | ||
| + | if ($libros) { | ||
| − | + | echo "<table border='1'>"; | |
| − | + | foreach ($libros as $libro) { | |
| + | echo '<tr>'; | ||
| − | + | printf( | |
| + | '<td>%s</td>', | ||
| + | htmlspecialchars( | ||
| + | $libro['titulo'], | ||
| + | ENT_QUOTES | ENT_SUBSTITUTE, | ||
| + | 'UTF-8' | ||
| + | ) | ||
| + | ); | ||
| − | + | echo '</tr>'; | |
| + | } | ||
| − | + | echo '</table>'; | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | } else { | ||
| − | + | echo 'Non hai datos que amosar.'; | |
| − | + | } </syntaxhighlight> | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | </syntaxhighlight> | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | <br /> | ||
| + | '''Amosar os enlaces ás páxinas''' | ||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | + | echo '<div>'; | |
| − | ::<syntaxhighlight lang=" | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| + | 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>'; </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"> | ||
| − | |||
<?php | <?php | ||
| − | + | ||
| − | ini_set('display_errors', 1 | + | ini_set('display_errors', '1'); |
| − | |||
error_reporting(E_ALL); | error_reporting(E_ALL); | ||
| − | require | + | 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, | ||
| + | [ | ||
| + | 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> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | ==== Sobre a optimización da paxinación ==== | ||
| + | |||
| + | * En xeral non debemos enviar pola URL o número total de rexistros e confiar posteriormente nese dato: | ||
| + | |||
| + | ::<syntaxhighlight lang="text"> | ||
| + | ?paxina=2&numtotal=50 </syntaxhighlight> | ||
| + | |||
| + | : Os datos enviados mediante `GET` poden ser modificados polo usuario. | ||
| + | |||
| + | * Gardar o número total de rexistros nunha variable de sesión tampouco garante que o dato siga sendo correcto, xa que outros usuarios poderían inserir ou eliminar rexistros. | ||
| + | |||
| + | * Como primeira solución realizaremos en cada petición: | ||
| + | |||
| + | ::<syntaxhighlight lang="sql"> | ||
| + | SELECT COUNT(*) | ||
| + | FROM libros </syntaxhighlight> | ||
| + | |||
| + | * Se nunha aplicación real esta operación chegase a ser un problema de rendemento poderían estudarse técnicas de caché ou outras estratexias de optimización. | ||
| + | |||
| + | <br /> | ||
| + | === Exercicio proposto === | ||
| + | |||
| + | * Crea unha páxina chamada `buscar_libros.php` que permita buscar libros empregando os seguintes criterios: | ||
| + | |||
| + | :* '''Título''': unha caixa de texto que realizará unha busca mediante `LIKE`. | ||
| + | :* '''Prezo''': un campo no que se validará que o valor introducido sexa numérico. | ||
| + | :* '''Estado''': unha lista `<select>` cos estados dispoñibles. | ||
| + | :* '''Autor''': unha lista `<select>` cos autores dispoñibles. | ||
| + | :* '''Criterio de ordenación''': título, prezo ou autor. | ||
| + | |||
| + | * O usuario poderá combinar os diferentes criterios. | ||
| + | |||
| + | * Os resultados deberán poder amosarse paxinados. | ||
| + | |||
| + | * A solución deberá: | ||
| + | |||
| + | :* Empregar PDO. | ||
| + | :* Empregar sentenzas preparadas para todos os datos externos. | ||
| + | :* Validar os valores recibidos mediante `GET` ou `POST`. | ||
| + | :* Empregar `bindValue()` co tipo correspondente cando sexa necesario. | ||
| + | :* Non concatenar directamente datos introducidos polo usuario dentro dunha sentenza SQL. | ||
| + | :* Empregar `htmlspecialchars()` cando se amosen cadeas dentro do HTML. | ||
| + | :* Capturar os erros PDO mediante `PDOException`. | ||
| + | :* Rexistrar os detalles do erro mediante `error_log()`. | ||
| + | :* Amosar ao usuario unha mensaxe xenérica en caso de erro. | ||
| + | :* Capturar outros erros da aplicación mediante `Throwable`. | ||
<br /> | <br /> | ||
| − | |||
| − | + | == Resumo das funcións PDO empregadas == | |
| − | |||
| − | |||
| − | |||
| − | + | ::<syntaxhighlight lang="php"> | |
| − | + | ! Método / constante | |
| + | | ! Uso | | ||
| + | | ------------------------------------------------------------------------------------------------- | | ||
| + | | `new PDO(...)` | | ||
| + | | Abrir unha conexión coa base de datos. | | ||
| + | | - | | ||
| + | | `query()` | | ||
| + | | Executar unha sentenza SQL sen parámetros externos e obter un resultado. | | ||
| + | | - | | ||
| + | | `prepare()` | | ||
| + | | Preparar unha sentenza SQL parametrizada. | | ||
| + | | - | | ||
| + | | `execute()` | | ||
| + | | Executar unha sentenza preparada. | | ||
| + | | - | | ||
| + | | `bindValue()` | | ||
| + | | Asociar un valor e, opcionalmente, o seu tipo a un parámetro. | | ||
| + | | - | | ||
| + | | `fetch()` | | ||
| + | | Recuperar unha fila do resultado. | | ||
| + | | - | | ||
| + | | `fetchAll()` | | ||
| + | | Recuperar todas as filas do resultado. | | ||
| + | | - | | ||
| + | | `fetchColumn()` | | ||
| + | | Recuperar o valor dunha columna, especialmente útil para `COUNT(*)` ou funcións. | | ||
| + | | - | | ||
| + | | `closeCursor()` | | ||
| + | | Liberar os resultados pendentes dunha sentenza, especialmente útil con procedementos almacenados. | | ||
| + | | - | | ||
| + | | `PDO::PARAM_INT` | | ||
| + | | Indica un parámetro de tipo enteiro. | | ||
| + | | - | | ||
| + | | `PDO::PARAM_STR` | | ||
| + | | Indica un parámetro de tipo cadea. | | ||
| + | | - | | ||
| + | | `PDO::PARAM_BOOL` | | ||
| + | | Indica un parámetro booleano. | | ||
| + | | - | | ||
| + | | `PDO::PARAM_NULL` | | ||
| + | | Indica un valor nulo. | | ||
| + | | - | | ||
| + | | `PDO::FETCH_ASSOC` | | ||
| + | | Recupera as filas como arrays asociativos. | | ||
| + | | } | | ||
| + | ::</syntaxhighlight> | ||
| − | + | == Regras que empregaremos nos exames == | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | # As operacións contra a base de datos realizaranse mediante '''PDO'''. | |
| + | # Para consultas sen parámetros externos poderá empregarse `query()`. | ||
| + | # Se hai datos procedentes do usuario ou outros valores externos empregarase `prepare()` + `execute()`. | ||
| + | # Non se concatenarán directamente datos externos nas sentenzas SQL. | ||
| + | # Os datos recibidos mediante formularios ou URL deberán validarse. | ||
| + | # Cando o tipo sexa relevante empregarase `bindValue()` indicando `PDO::PARAM_INT`, `PDO::PARAM_STR`, etc. | ||
| + | # Non se empregará `rowCount()` para coñecer o número de filas devoltas por un `SELECT`. | ||
| − | + | # 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 /> | ||
| Línea 2563: | Línea 3575: | ||
| − | <br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- ( | + | <br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2025). |
Revisión actual del 18:12 8 oct 2026
Sumario
- 1 Introducción
- 2 Realización dunha consulta SELECT
- 3 Comprobar se unha consulta SELECT devolveu datos
- 4 Recuperación de filas
- 5 Saída de datos en HTML
- 6 Consultas con parámetros numéricos
- 7 Tipo dos parámetros
- 8 Parámetros de tipo cadea
- 9 Validación dos parámetros recibidos
- 10 Caso práctico: consultar exemplares dun libro
- 11 Procedementos almacenados empregando PDO
- 12 Funcións almacenadas de MySQL
- 13 Paxinación de resultados empregando PDO
- 14 Xestión de excepcións
- 15 Exercicios prácticos
- 15.1 Caso Práctico 1
- 15.2 Caso Práctico 2
- 15.3 Consulta enviando parámetros de tipo numérico
- 15.4 Caso práctico: Enviando parámetros de tipo cadea
- 15.5 Consulta chamando a procedementos almacenados
- 15.6 Consulta chamando a procedementos almacenados con parámetros de entrada
- 15.7 Consulta chamando a procedementos almacenados con parámetros de saída
- 15.8 Consulta chamando a funcións
- 15.9 Paxinación de resultados
- 15.10 Exercicio proposto
- 16 Resumo das funcións PDO empregadas
- 17 Regras que empregaremos nos exames
Introducción
- Requerimentos previos: Ter creada a base de datos indicada nunha sección anterior e ter gardados os parámetros de conexión nun arquivo separado como foi indicado nunha sección anterior
IMPORTANTE: Nos exames, exercicios e prácticas avaliables empregarase PDO para realizar as operacións contra a base de datos, salvo que no enunciado se indique expresamente o contrario.
Os exemplos con mysqli poden manterse nos apuntes a nivel informativo, pero os exemplos de referencia serán os realizados con PDO.
- O proceso para realizar unha consulta é o seguinte:
- Paso 1: Abrimos a conexión á base de datos: Isto xa o vimos nunha sección anterior
- 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).
- void mysqli_result::free ( void ) Libera da memoria o conxunto de resultados.
- mysqli::error: Devolve en forma de cadea o último erro xerado polo xestor de base de datos Mysql.
- mysqli::errno: Devolve o último código de erro devolto polo xestor Mysql
- 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'.
- 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.
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.
- 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:
- 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:
- 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.
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
- As operacións contra a base de datos realizaranse mediante PDO.
- Para consultas sen parámetros externos poderá empregarse `query()`.
- Se hai datos procedentes do usuario ou outros valores externos empregarase `prepare()` + `execute()`.
- Non se concatenarán directamente datos externos nas sentenzas SQL.
- Os datos recibidos mediante formularios ou URL deberán validarse.
- Cando o tipo sexa relevante empregarase `bindValue()` indicando `PDO::PARAM_INT`, `PDO::PARAM_STR`, etc.
- Non se empregará `rowCount()` para coñecer o número de filas devoltas por un `SELECT`.
- 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.
-- Ángel D. Fernández González -- (2025).


