Diferencia entre revisiones de «PHP Operacións BD Mysql»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 216 ediciones intermedias de 2 usuarios)
Línea 1: Línea 1:
 
== Introdución ==
 
== Introdución ==
  
 +
'''<u>IMPORTANTE – API</u> empregada no módulo'''
 +
 +
: Nestes apuntes explícanse diferentes formas de realizar operacións contra unha base de datos MySQL desde PHP, empregando principalmente **mysqli** e **PDO (PHP Data Objects)**.
 +
: As explicacións e exemplos realizados con **mysqli**, tanto na súa versión procedemental como orientada a obxectos, mantéñense nos apuntes con carácter **informativo e comparativo**, para coñecer as diferentes posibilidades que ofrece PHP para acceder a MySQL.
 +
 +
: Non obstante, a partir deste tema a forma de acceso a bases de datos que se empregará como referencia no módulo será **PDO**, utilizando o controlador **PDO_MySQL**.
 +
: Polo tanto, **nos exercicios, prácticas e exames nos que se solicite realizar operacións contra unha base de datos desde PHP, deberá empregarse PDO**, salvo que no enunciado se indique expresamente o contrario.
 +
: Recoméndase, polo tanto, prestar especial atención aos exemplos identificados como **“Empregando PDO”**, xa que serán os que se tomarán como referencia para a avaliación.
 +
 +
 +
 +
* Estes apuntes están actualizados para PHP 8.5. PDO traballa por defecto en modo de excepcións, polo que os erros producidos nas operacións contra a base de datos poden xerar unha PDOException, que poderá ser xestionada mediante try-catch.
 +
 +
 +
<br />
 
* Como comentamos no [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Introduci%C3%B3n_acceso_a_base_de_datos punto anterior], para conectarnos a un xestor de base de datos específico imos ter que ter instalados os drivers correspondentes.
 
* Como comentamos no [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Introduci%C3%B3n_acceso_a_base_de_datos punto anterior], para conectarnos a un xestor de base de datos específico imos ter que ter instalados os drivers correspondentes.
  
Línea 19: Línea 34:
 
* Existen tres APIs principais para conectar a un servidor de bases de datos MySQL:
 
* Existen tres APIs principais para conectar a un servidor de bases de datos MySQL:
  
−
:* Extensión MySQL de PHP
+
:* Extensión MySQL de PHP (eliminada a partires da versión PHP 7.0)
 
:* Extensión mysqli de PHP
 
:* Extensión mysqli de PHP
−
:* Obxectos de Datos de PHP (PDO)
+
:* Obxectos de Datos de PHP (PDO) => '''A QUE IMOS EMPREGAR'''
−
 
 
  
−
* Para conectarnos imos utilizar as funcións mysql'''i''' (mysql improved) que foron introducidas a partires de Mysql 4.1
 
−
: A extensión mysqli ven <u>incluída nas versiones PHP 5 e posteriores</u>.
 
  
 +
* A extensión mysqli ven <u>incluída nas versiones PHP 5 e posteriores</u>.
 
: Algunha das melloras de dita extensión con respecto á extensión mysql:
 
: Algunha das melloras de dita extensión con respecto á extensión mysql:
  
Línea 33: Línea 46:
 
:* Soporte para [http://php.net/manual/es/pdo.transactions.php Transaccións]
 
:* Soporte para [http://php.net/manual/es/pdo.transactions.php Transaccións]
  
 +
== Activar a extensión mysqli e pdo==
  
−
: Dita extensión tamén ten unha versión procedural, de tal forma que imos poder chamar ás funcións de dúas maneiras diferentes:
+
* Se instalamos XAMPP ou calquera das versións para outros S.O. normalmente a extensión xa ven habilitada por defecto.
 +
: Se temos unha instalación personalizada no que instalamos separadamente todo, teremos que habilitala.
  
−
Exemplo de función cun estilo procedimental que pecha unha conexión a unha base de datos ($link é un identificador cando se fai a conexión contra a base de datos):
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
mysqli_close($link);
 
−
</syntaxhighlight>
 
  
−
Exemplo de función cun estilo obxectual que pecha unha conexión a unha base de datos:
+
: Xa vimos anteriormente que PDO permite conexión a múltiples bases de datos, pero tamén engade unha capa más para a conexión de base de datos, aínda que hoxe en día, a diferenza de rendemento é mínima.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
mysqli::close();
 
−
</syntaxhighlight>
 
  
 +
<br />
 +
===Windows===
  
−
* Aquí tedes un cadro comparativo obtido de php.net onde se ve o comentado anteriormente:
+
* En Windows, a extensión mysqli ven desactivada no php.ini.
 +
: Partindo de que xa tedes o arquivo php.ini no cartafol onde se atopa o intérprete de PHP (se non é así mirade a nota de Importante que se atopa [http://wiki.cifprodolfoucha.es/index.php?title=PHP_IDE_Netbeans#Primeiro_proxecto:_Ola_Mundo neste punto da wiki]) temos que:
  
−
[[Imagen:Php_BD_2.jpg|600px|center]]
+
::* Descomentar a liña:
−
 
+
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
−
 
+
;extension=php_mysqli.dll
−
 
 
−
<u>Nota:</u> Máis información [http://php.net/manual/es/mysqli.overview.php neste enlace].
 
−
 
 
−
== Conexión ==
 
−
 
 
−
* Partimos da base de que xa temos instalado:
 
−
:* O xestor de bases de datos Mysql
 
−
:* Unha base de datos creada. Neste manual vai ter de nome '''PHP'''. Facer que o 'default collation' sexa '''utf8_spanish2_ci'''.
 
−
:* Un usuario / password que teña todos os permisos sobre a base de datos creada. Neste manual o usuario/password creado son '''user_php/user_php'''.
 
−
 
 
−
 
 
−
: Todo o anterior xa está indicado no [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Requirimentos_previos_para_acceso_a_datos punto anterior da wiki].
 
−
 
 
−
 
 
−
* Utilizando procedementos: [https://secure.php.net/manual/es/function.mysqli-connect.php mysqli_connect]
 
−
* Utilizando obxectos: [https://secure.php.net/manual/es/mysqli.construct.php mysql::_construct]
 
−
 
 
−
* Sintaxe (se indica a sintaxe do construtor e polo tanto será equivalente a facer un 'new' da clase mysqli):  
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
mysqli::__construct ([ string $host = ini_get("mysqli.default_host") [, string $username = ini_get("mysqli.default_user") [, string $passwd = ini_get("mysqli.default_pw") [, string $dbname = "" [, int $port = ini_get("mysqli.default_port") [, string $socket = ini_get("mysqli.default_socket") ]]]]]] )
 
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
−
: Todos eles son optativos, pero normalmente indicaremos o nome de host, login, password e base de datos.
+
::: Pasa a:
−
: Os parámetros son:  
 
−
:* Host a conectar (cadea): Pode ser o nome ou a dirección IP. Normalmente imos poñer 'localhost' (se o xestor de bases de datos está instalado no mesmo equipo que o servidor web).
 
−
:* Nome do usuario con acceso á base de datos (cadea): Nome do usuario que ten acceso á base de datos.
 
−
:* Password (cadea): Password do usuario.
 
−
:* Dbname  (cadea): Nome da base de datos sobre a que imos traballar.
 
−
:* Porto (numérico): Porto no que está escoitando o xestor de base de datos. No caso de mysql ó o 3306.
 
−
:* Socket (cadea): Socket ou tubería para conectar có xestor de bases de datos. En Linux é un tipo especial de arquivo que serve para 'transmitir' información, neste caso, cara o xestor de bases de datos.
 
−
 
 
  
−
* Aplicado ao noso caso, teremos a seguinte orde:
+
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
extension=php_mysqli.dll
−
$conex = new mysqli('localhost','user_php','user_php','PHP');
 
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
  
−
* En caso de erro, $conex é un obxecto que represente a conexión á base de datos.
+
::* Descomentar a liña:
−
 
+
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
−
:* En caso de ter unha versión PHP anterior á PHP 5.2.9 e 5.3.0 teremos que chamar á función [http://www.w3schools.com/php/func_mysqli_connect_error.asp mysqli_connect_error()] desta forma:
+
;extension_dir = "ext"
−
::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
 
−
$conex = new mysqli('localhost','user_php','user_php','PHP');
 
−
if (mysqli_connect_error()){
 
−
  die('Erro de Conexión (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
−
:* En caso de ter unha versión PHP posterior á PHP 5.2.9 e 5.3.0:
+
::: Pasa a:
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
+
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
−
$conex = new mysqli('localhost','user_php','user_php','PHP');
+
extension_dir = "ext"
−
if ($conex->connect_error) {
+
</syntaxhighlight>
−
  die('Erro de Conexión (' . $conex->connect_errno . ') ' . $conex->connect_error);
+
:: O que fai dita liña é informar a PHP que busque as extensións no cartafol relativo ext, é dicir, nun cartafol chamado ext dentro do cartafol onde se atopa o executable de php.
−
}
 
−
</syntaxhighlight>
 
  
−
: Como vemos amosamos por un lado a mensaxe de erro que devolve o xestor de bases de datos e o número de erro asociado a esa mensaxe.
 
  
−
: Podemos consultar os número de erro e mensaxes asociadas [http://dev.mysql.com/doc/refman/5.6/en/error-messages-server.html neste enlace].
+
<br />
 +
===Linux===
  
 +
* Necesitamos ter instalado o paquete: '''php-mysql'''.
 +
: Para instalalo:
 +
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
sudo apt install php-mysql
 +
</syntaxhighlight>
  
−
* Por exemplo, imos modificar o password do usuario por un que non exista e veremos o resultado:
+
: Reiniciamos o apache: '''sudo systemctl restart apache2'''
−
::<syntaxhighlight lang="java" line enclose="div" highlight="1" >
 
−
$conex = new mysqli('localhost','user_php','password_non_existe','PHP');
 
−
if (mysqli_connect_error()){
 
−
  die('Erro de Conexión (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
−
</syntaxhighlight>
 
  
−
: Dará como resultado:
 
−
[[Imagen:Php_BD_3.jpg|400px|center]]
 
  
−
=== Gardando os datos de conexión nun lugar seguro ===
+
* O anterior vai crear varios arquivos de configuración no cartafol: etc/php/8.1/apache2/conf.d para cargar as extensións mysqli e pdo.
 +
: <u>Nota:</u> O cartafol pode variar dependendo da versión php que teñades instalada.
  
−
* Como é obvio os datos de base de datos, usuario e password son moi importantes e non debemos permitir que alguén poida descubrilos.
 
  
−
* O que faremos será o seguinte:
+
<br />
  
−
:* Definiremos en forma de constantes ditos datos nun arquivo de include.
+
== Creando as táboas en Mysql ==
−
:* Gardaremos dito arquivo '''fora''' do cartafol de acceso do noso sitio web.
 
  
−
* O primeiro punto, definir en forma de constante e nun arquivo de include os datos da conexión:
+
* Unha vez temos a base de datos teremos que facer o [https://es.wikipedia.org/wiki/Modelo_entidad-relaci%C3%B3n Modelo EE/R], pasalo ao [https://es.wikipedia.org/wiki/Modelo_relacional Modelo Relacional] e facer un [https://es.wikipedia.org/wiki/Normalizaci%C3%B3n_de_bases_de_datos#Formas_normales Proceso de Normalización] ata polo menos a forma normal de Boyce-Codd.
  
−
'''Arquivo: bd.inc.php''':
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
<?php
 
  
−
define ('SERVIDOR','localhost');
+
* Imos implementar unha base de datos para xestionar a venda de libros de segunda mano.
−
define ('USUARIO','user_php');
+
: A orde SQL para crear unha base de datos de nome php é:
−
define ('PASSWORD','user_php');
 
−
define ('BD','PHP');
 
−
</syntaxhighlight>
 
−
 
 
−
:* Faremos un '''require''' deste arquivo en todas as páxinas que necesiten unha conexión á base de datos. Desta forma, se necesitamos cambiar algo soamente o cambiaríamos nun arquivo.
 
−
 
 
−
 
 
−
* O segundo punto consiste en descubrir cal é a ruta física á que apunta o noso servidor web e colocar o arquivo de include 'fora' desa ruta.
 
−
:* Temos [http://wiki.cifprodolfoucha.es/index.php?title=DRUPAL_Instalaci%C3%B3n#Instalaci.C3.B3n_do_Apache neste punto da Wiki] información sobre como funciona Apache e como saber a onde apunta o servidor web no seu arquivo de configuración.
 
−
 
 
−
:* Temos varias opcións:
 
−
::* Facendo uso da [http://php.net/manual/es/language.constants.predefined.php constante __DIR__].
 
−
::* Facendo uso do array global [http://php.net/manual/es/reserved.variables.server.php $_SERVER] e dentro deste a entrada $_SERVER['DOCUMENT_ROOT'].
 
−
 
 
−
:Se facemos un echo destas variables dentro dunha páxina do noso sitio web, obteremos unha ruta, coma por exemplo:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
/var/www/html/
 
−
</syntaxhighlight>
 
−
 
 
−
:<u>Nota:</u> Se o facemos coa constante __DIR__ e a páxina que está a facer o echo se atopa dentro dun cartafol, tamén aparecerá o cartafol (por exemplo /var/www/html/cartafol/).
 
−
 
 
−
 
 
−
 
 
−
:* Isto quere dicir que o servidor web (e polo tanto calquera usuario de Internet) vai poder acceder as páxinas que se atopan no cartafol /var/www/html e calquera dentro del.
 
−
 
 
−
:: Unha medida de seguridade é colocar o arquivo de include 'fora' desta ruta. Por exemplo, podemos crear un arquivo no cartafol '''/var/www/confBD'''
 
−
 
 
−
:: A dito cartafol ten que poder acceder o usuario que utiliza o Apache para acceder aos cartafoles a nivel do S.O. (no caso de Apache soe ser www-data, podedes miralo no [http://wiki.cifprodolfoucha.es/index.php?title=DRUPAL_Instalaci%C3%B3n#Instalaci.C3.B3n_do_Apache punto da wiki indicado anteriormente]).
 
−
 
 
−
<gallery caption="Creando o cartafol onde se vai gardar o arquivo de configuración" widths="350" heights="300px" perrow="2">
 
−
Image:Php_BD_4.jpg| Facemos un '''cd /var/www'''. Creamos o cartafol: '''sudo mkdir confBD'''. <u>Movemos o arquivo de include creado no paso anterior ao cartafol:</u> '''mv /cartafol_arquivo_include/bd.inc.php /var/www/confBD'''.
 
−
Image:Php_BD_5.jpg| Cambiamos os permisos: '''sudo chmod -R 550 /var/www/confBD''' e cambiamos o propietario: '''sudo chown -R  www-data:www-data /var/www/confBD'''. <u>Nota:</u> O usuario e grupo pode variar dependendo do S.O. En Linux podedes sabelo mirando [http://wiki.cifprodolfoucha.es/index.php?title=DRUPAL_Instalaci%C3%B3n#Arquivos_de_configuraci.C3.B3n este enlace da wiki]. En Windows o podedes saber [http://httpd.apache.org/docs/2.0/platform/windows.html#winsvc neste enlace].
 
−
</gallery>
 
−
 
 
−
 
 
−
:* Agora, calquera páxina que faga uso destes datos terá que facer o seguinte require:
 
−
 
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="6" >
 
−
<?php
 
−
 
 
−
//error_reporting(0);  // Inabilita os erros de Apache se temos activada a directiva display_erros no php.ini
 
−
 
 
−
 
 
−
require ('/var/www/confBD/bd.inc.php');
 
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
 
  
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
CREATE SCHEMA `php` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci ;
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
===O xogo de caracteres===
 
  
−
* Debemos ter en conta que unha páxina web está 'aberta' a todo o mundo que se conecte a ela.
 
−
: Cada persoa en diferentes países terá diferente xogos de caracteres no que cada carácter está relacionado cun código numérico.
 
−
 
−
* Se gardamos como dato dentro da base de datos unha cadea cun 'ñ' entre os seus datos, dito carácter estará asociado a un código nun xogo de caracteres. Se o usuario que vai cargar a páxina o carga cun xogo de caracteres diferente non vai poder visualizalo.
 
−
 
−
 
−
* Cando creamos aplicacións en PHP con acceso a base de datos teremos que ter en conta os seguintes xogos de caracteres:
 
−
 
−
:* Xogo de caracteres da base de datos (no noso caso Mysql): É o que se coñece como [https://dev.mysql.com/doc/refman/5.5/en/charset-charsets.html '''Charset/Collation'''].
 
−
[[Imagen:Php_BD_7.jpg|300px|center]]
 
−
::: Se vos fixades cando creamos a base de datos en Mysql, escollemos o charset (primeira columna) e o collation (segunda columna).
 
−
::: Como indicamos antes, cando creamos a base de datos o charset/collation indicado foi '''utf8/utf8_spanish2_ci'''.
 
−
::: O charset é como mysql garda internamente os caracteres e os relaciona cun código numérico e o collation indica como comparar e ordenar o texto. Podedes consultar [http://blog.unreal4u.com/2012/08/sobre-collation-y-charset-en-mysql/ esta blog] para obter máis información.
 
−
 
−
:* Xogo de caracteres da páxina HTML.
 
−
 
−
::: Na páxina web indicamos coa etiqueta <meta> o xogo de caracteres para que o navegador web poida visualizalos correctamente.
 
−
 
−
::: No caso que nos ocupa teremos que poñer: <meta http-equiv="Content-Type" content="text/html; <b>charset=UTF-8"</b> />
 
−
 
−
::: Máis información en [http://www.w3schools.com/html/html_charset.asp w3schools].
 
−
 
−
:* No código PHP ao conectar coa base de datos.
 
−
::: Cando conectemos coa base de datos teremos que indicarlle que imos utilizar o xogo de caracteres utf8 con esta liña:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
 
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
 
−
$conex->set_charset('utf8');
 
−
</syntaxhighlight>
 
−
 
−
 
−
* '''E FUNDAMENTAL QUE NOS TRES SITIOS ESTEA POSTO O MESMO XOGO DE CARACTERES'''.
 
−
 
−
== Creando as táboas en Mysql ==
 
−
 
−
* Unha vez temos a base de datos teremos que facer o [https://es.wikipedia.org/wiki/Modelo_entidad-relaci%C3%B3n Modelo EE/R], pasalo ao [https://es.wikipedia.org/wiki/Modelo_relacional Modelo Relacional] e facer un [https://es.wikipedia.org/wiki/Normalizaci%C3%B3n_de_bases_de_datos#Formas_normales Proceso de Normalización] ata polo menos a forma normal de Boyce-Codd.
 
−
 
−
 
−
* Imos implementar unha base de datos para xestionar a venda de libros de segunda mano.
 
  
 +
<br />
 
: O modelo EE/R é o seguinte:
 
: O modelo EE/R é o seguinte:
  
−
[[Imagen:Php_BD_6.jpg|600px|center]]
+
[[Imagen:Php_BD_6.jpg|800px|center]]
  
 
<u>Nota:</u>  
 
<u>Nota:</u>  
Línea 243: Línea 130:
  
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
−
CREATE TABLE `PHP`.`LIBROS` (
+
CREATE TABLE IF NOT EXISTS `php`.`libros` (
−
   `id_libro` INT NOT NULL AUTO_INCREMENT,
+
   `id` INT NOT NULL AUTO_INCREMENT,
 
   `titulo` VARCHAR(300) NOT NULL,
 
   `titulo` VARCHAR(300) NOT NULL,
−
   PRIMARY KEY (`id_libro`));
+
   PRIMARY KEY (`id`));
  
−
CREATE TABLE `PHP`.`ESTADOS` (
+
CREATE TABLE IF NOT EXISTS `php`.`estados` (
−
   `id_estado` INT NOT NULL AUTO_INCREMENT,
+
   `id` INT NOT NULL AUTO_INCREMENT,
 
   `descripcion` VARCHAR(100) NOT NULL,
 
   `descripcion` VARCHAR(100) NOT NULL,
−
   PRIMARY KEY (`id_estado`));
+
   PRIMARY KEY (`id`));
  
−
  CREATE TABLE `PHP`.`EXEMPLARES` (
+
  CREATE TABLE IF NOT EXISTS `php`.`exemplares` (
 
   `libro_id` INT NOT NULL,
 
   `libro_id` INT NOT NULL,
 
   `id_exemplar` INT NOT NULL,
 
   `id_exemplar` INT NOT NULL,
Línea 264: Línea 151:
 
   CONSTRAINT `fk_EXEMPLARES_LIBROS`
 
   CONSTRAINT `fk_EXEMPLARES_LIBROS`
 
     FOREIGN KEY (`libro_id`)
 
     FOREIGN KEY (`libro_id`)
−
     REFERENCES `PHP`.`LIBROS` (`id_libro`)
+
     REFERENCES `php`.`libros` (`id`)
 
     ON DELETE NO ACTION
 
     ON DELETE NO ACTION
 
     ON UPDATE CASCADE,
 
     ON UPDATE CASCADE,
 
   CONSTRAINT `fk_EXEMPLARES_ESTADOS`
 
   CONSTRAINT `fk_EXEMPLARES_ESTADOS`
 
     FOREIGN KEY (`estado_id`)
 
     FOREIGN KEY (`estado_id`)
−
     REFERENCES `PHP`.`ESTADOS` (`id_estado`)
+
     REFERENCES `php`.`estados` (`id`)
 
     ON DELETE NO ACTION
 
     ON DELETE NO ACTION
 
     ON UPDATE CASCADE);
 
     ON UPDATE CASCADE);
  
−
CREATE TABLE `PHP`.`AUTORES` (
+
CREATE TABLE IF NOT EXISTS `php`.`autores` (
−
   `id_autor` INT NOT NULL AUTO_INCREMENT,
+
   `id` INT NOT NULL AUTO_INCREMENT,
−
   `apelido` VARCHAR(200) NOT NULL,
+
   `apelidos` VARCHAR(200) NOT NULL,
 
   `nome` VARCHAR(50) NOT NULL,
 
   `nome` VARCHAR(50) NOT NULL,
−
   PRIMARY KEY (`id_autor`));
+
   PRIMARY KEY (`id`));
  
  
−
CREATE TABLE `PHP`.`LIBROS_AUTORES` (
+
CREATE TABLE IF NOT EXISTS `php`.`libro_autor` (
 
   `libro_id` INT NOT NULL,
 
   `libro_id` INT NOT NULL,
 
   `autor_id` INT NOT NULL,
 
   `autor_id` INT NOT NULL,
 
   PRIMARY KEY (`libro_id`, `autor_id`),
 
   PRIMARY KEY (`libro_id`, `autor_id`),
−
   INDEX `fk_LIBROS_AUTORES_AUTORES_idx` (`autor_id` ASC),
+
   INDEX `fk_LIBROAUTOR_AUTORES_idx` (`autor_id` ASC),
−
   CONSTRAINT `fk_LIBROS_AUTORES_LIBROS`
+
   CONSTRAINT `fk_LIBROAUTOR_LIBROS`
 
     FOREIGN KEY (`libro_id`)
 
     FOREIGN KEY (`libro_id`)
−
     REFERENCES `PHP`.`LIBROS` (`id_libro`)
+
     REFERENCES `php`.`libros` (`id`)
 
     ON DELETE NO ACTION
 
     ON DELETE NO ACTION
 
     ON UPDATE CASCADE,
 
     ON UPDATE CASCADE,
−
   CONSTRAINT `fk_LIBROS_AUTORES_AUTORES`
+
   CONSTRAINT `fk_LIBROAUTOR_AUTORES`
 
     FOREIGN KEY (`autor_id`)
 
     FOREIGN KEY (`autor_id`)
−
     REFERENCES `PHP`.`AUTORES` (`id_autor`)
+
     REFERENCES `php`.`autores` (`id`)
 
     ON DELETE NO ACTION
 
     ON DELETE NO ACTION
 
     ON UPDATE CASCADE);
 
     ON UPDATE CASCADE);
Línea 302: Línea 189:
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
  
−
INSERT INTO `ESTADOS` VALUES (1,'NOVO'),(2,'COMO NOVO'),(3,'DETERIODADO'),(4,'CON ANOTACIÓNS');
+
INSERT INTO `php`.`estados` VALUES (1,'NOVO'),(2,'COMO NOVO'),(3,'DETERIODADO'),(4,'CON ANOTACIÓNS');
  
  
−
INSERT INTO `PHP`.`AUTORES` (`apelido`, `nome`) VALUES ('Drumont', 'Eduardo');
+
INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('Drumont', 'Eduardo');
  
−
INSERT INTO `PHP`.`LIBROS` (`titulo`) VALUES ('El caso Dreyfus, expresión del antisemitismo, temas, Gallica, BNF');
+
INSERT INTO `php`.`libros` (`titulo`) VALUES ('El caso Dreyfus, expresión del antisemitismo, temas, Gallica, BNF');
−
INSERT INTO `PHP`.`LIBROS` (`titulo`) VALUES ('Les amis');
+
INSERT INTO `php`.`libros` (`titulo`) VALUES ('Les amis');
  
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('1', '1', 'dreyfus.jpg', '20,52', '2');
+
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('1', '1', 'dreyfus.jpg', '20.52', '2');
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('1', '2', 'dreyfus.jpg', '15,34', '3');
+
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('1', '2', 'dreyfus.jpg', '15.34', '3');
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '1', 'amis.jpg', '35,35', '1');
+
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '1', 'amis.jpg', '35.35', '1');
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '2', 'amis.jpg', '20,25', '4');
+
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '2', 'amis.jpg', '20.25', '4');
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '3', 'amis.jpg', '25,34', '3');
+
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '3', 'amis.jpg', '25.34', '3');
  
−
INSERT INTO `LIBROS_AUTORES` VALUES (1,1),(2,1);
+
INSERT INTO `php`.`libro_autor` VALUES (1,1),(2,1);
  
 
</syntaxhighlight>  
 
</syntaxhighlight>  
Línea 333: Línea 220:
 
=== Regras de nomeado de táboas e columnas ===
 
=== Regras de nomeado de táboas e columnas ===
  
−
* Aínda que non existen 'regras' únicas sobre como nomear as táboas e campos nos imos seguir as seguintes:
+
* Aínda que non existen 'regras' únicas sobre como nomear as táboas e campos nos imos seguir as seguintes, para que cando vexamos Laravel, coincidan os Modelos de Laravel cós nomes das táboas asociadas:
  
−
:* Nomes de táboas en PLURAL e con MAIÚSCULAS. Desta forma distinguiremos facilmente o nome da táboa e os campos nunha consulta SQL.
+
:* Nomes de táboas en PLURAL e con minúsculas. Por exemplo táboa: '''usuarios'''
 
:* Nomes de campos en minúscula e singular.
 
:* Nomes de campos en minúscula e singular.
−
::* Se o campo é a clave primaria terá de nome: id_nometáboa_en_singular
+
::* Se o campo é a clave primaria terá de nome: '''id'''
−
::* Se o campo é unha clave foránea terá de nome: nometáboa_en_singular_id
+
::* Se o campo é unha clave foránea terá de nome: nometáboa_en_singular_id. Por exemplo, nunha táboa de nome pais, asociada á táboa usuarios cunha relación 1:N, tería unha clave foránea de nome '''usuario_id'''.
 
::* Se o nome de campo está formado por varias palabras, estarán separadas por un guión baixo ( _ )
 
::* Se o nome de campo está formado por varias palabras, estarán separadas por un guión baixo ( _ )
 
:* Non usar til nin caracteres 'raros' como espazos en branco, interrogacións,...
 
:* Non usar til nin caracteres 'raros' como espazos en branco, interrogacións,...
 
:* As claves foráneas irán ao final da lista de columnas da táboa.
 
:* As claves foráneas irán ao final da lista de columnas da táboa.
 +
:* As táboas  que se formen por unha relación N:M terán de nome: taboa1singular_taboa2singular. Por exemplo se temos unha táboa usuarios e unha táboa modulos e existe unha relación N:M entre elas, se crearía a táboa: '''usuario_modulo'''
 +
 +
 +
<br />
 +
 +
== Visualización de erros ==
 +
 +
* Ao principio é habitual cometer erros durante as probas de programación.
 +
  : Ata que aprendamos a utilizar un depurador (debugger), cando se produce un erro en PHP pode aparecer unha páxina en branco, mostrarse só unha parte da páxina ou aparecer unha mensaxe de erro que indique o problema.
 +
 +
* Durante o desenvolvemento é recomendable configurar PHP para que mostre os erros. Isto pode facerse modificando as directivas '''display_errors''' e '''error_reporting''' do ficheiro ''php.ini''.
  
−
== Cambiando de base de datos activa ==
+
: Se non se quere modificar a configuración de PHP, tamén se poden engadir temporalmente ao principio do ficheiro PHP as seguintes instrucións:
  
−
* Cando creamos a cadea de conexión, un dos parámetros que enviamos foi a base de datos na que iamos traballar.
+
:<syntaxhighlight lang="php" enclose="div">
 +
ini_set('display_errors', 1);
 +
ini_set('display_startup_errors', 1);
 +
error_reporting(E_ALL); </syntaxhighlight>
  
−
: Podemos non enviar ese parámetro ou querer cambiar de base de datos dentro dunha páxina php.
+
: Desta forma PHP mostrará os erros, avisos e advertencias que se produzan durante a execución do programa.
  
−
: Se queremos escoller outra diferente teremos que facer uso da [http://php.net/manual/es/mysqli.select-db.php función mysqli::select_db].
+
: '''IMPORTANTE:''' <u>Estas opcións deben utilizarse unicamente durante o desenvolvemento e as probas. Nun servidor en produción non se deben mostrar os erros ao usuario.</u>
  
−
: Por exemplo:
+
: Hai que ter en conta que estas instrucións non permiten detectar determinados erros que se producen antes de que o programa comece a executarse, como algúns erros de sintaxe. Nestes casos será necesario consultar o rexistro de erros do servidor ou activar a visualización de erros na configuración de PHP (''php.ini'').
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4" >
 
−
$conex = new mysqli('localhost','user_php','user_php','PHP');
 
−
$conex->set_charset('utf8');
 
  
−
if (!$conex->select_db('OutraBD')){
+
* Outra ferramenta moi útil é o inspector ou as ferramentas para desenvolvedores '''do navegador'''. Pódense abrir normalmente coa tecla '''F12'''.
−
    die('Erro ao cambiar de base de datos');
 
−
}
 
−
</syntaxhighlight>
 
  
 +
: Desde estas ferramentas pódense revisar, entre outras cousas:
 +
:* O código HTML xerado.
 +
:* As peticións realizadas ao servidor.
 +
:* Os códigos de estado HTTP, por exemplo un erro '''500 Internal Server Error'''.
 +
:* As mensaxes da consola de JavaScript.
  
−
== Consulta ==
+
: Con todo, hai que ter en conta que o navegador '''non pode mostrar o código PHP orixinal''', xa que este se executa no servidor. O navegador só recibe o resultado xerado polo programa PHP.
  
−
* Para conectarnos primeiro teremos que ter a consulta SQL.
+
[[Imagen:Mysql_visualuzar_erros_1.png|400px|center]]
−
* Indicar que o [https://es.wikipedia.org/wiki/SQL SQL (Structure Query Language)] é a linguaxe utilizada para obter datos dunha base de datos relacional.
 
  
−
: Consulta SQL: '''SELECT col1, col2 FROM TABOA_1'''
 
  
  
−
* 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
+
* <u>Outra ferramenta é engadir ao VS Code a extensión que nos permita facer 'debuger' da aplicación</u>. [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Habilitar_depuraci%C3%B3n_en_VS_Code Tedes nesta misma wiki o procedemento para facelo na UD0 de instalación].
−
:* [http://php.net/manual/es/class.mysqli-result.php mysqli_result]: En caso de realizar unha consulta de tipo 'select' este obxecto representa o conxunto de resultados obtidos.
 
−
::* [http://php.net/manual/es/mysqli-result.num-rows.php int $mysqli_result->num_rows]: Devolve o número de filas devoltas en caso de realizar unha consulta.
 
−
::* [http://php.net/manual/es/mysqli-result.fetch-assoc.php 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.
 
  
  
−
* O proceso para realizar unha consulta é o seguinte:
 
−
:* Abrimos a conexión á base de datos.
 
−
:* A idea é chamar ao procedemento query enviando como parámetro a consulta.
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
$result=$conex->query('SELECT col1, col2 FROM TABOA_1');
 
−
</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.
 
−
::: Se a consulta está mal feita (por exemplo, seleccionamos unha táboa que non existe) <u>devolverá false</u>.
 
−
::: Polo tanto, o seguinte será comprobar se temos datos
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
if ($result && ($result->num_rows>0)){
 
−
  // Veñen datos
 
  
−
}
+
* E lembrar que tamén podedes consultar o arquivo de '''error.log''' que se atopa en '''/var/log/apache2/error.log''' coa orde: tail -n 100 '''/var/log/apache2/error.log'''  (sendo 100 o número de liñas que queredes consultar do arquivo empezando polo final)
−
else {
 
−
  echo 'Non hai datos ou a consulta está mal feita';
 
−
}
 
−
</syntaxhighlight>
 
  
−
:* Agora necesitaremos percorrer dende o primeiro rexistro ata o último amosando o resultado.
+
<br />
−
::: Isto o faremos cun bucle, chamando á [http://php.net/manual/es/mysqli-result.fetch-assoc.php función mysqli_result::fetch_assoc(void)].
 
−
::: Debemos entender que, inicialmente, despois de facer a consulta, temos un punteiro que está 'apuntando' xusto antes do primeiro rexistro de resultados. O que fai a función anterior é mover ese punteiro ao seguinte rexistro (a primeira vez apuntará ao primeiro do conxunto de resultados) e devolve nun array a fila á que está 'apuntando'.
 
−
::: Se volvemos a chamar, volve a moverse ao seguinte rexistro e así ata ao final no que devolve false.
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="3-5" >
 
−
if ($result && ($result->num_rows>0)){
 
−
  // Veñen datos
 
−
  while($row = $result->fetch_assoc()) {
 
−
    printf("%s-%s",$row["col1"],$row["col2"]);
 
−
  }
 
−
}
 
−
else {
 
−
  echo 'Non hai datos ou a consulta está mal feita';
 
−
}
 
−
</syntaxhighlight>  
 
  
 +
== Conexión ==
  
−
:* Ao rematar de percorrer o resultado, o liberaremos de memoria.
+
* Partimos da base de que xa temos instalado:
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="6" >
+
:* O xestor de bases de datos Mysql
−
if ($result && ($result->num_rows>0)){
+
:* Unha base de datos creada. Neste manual vai ter de nome '''php'''. Facer que o 'default collation' sexa '''utf8mb4_spanish2_ci''' (máis información sobre o charset/collation [http://blog.unreal4u.com/2012/08/sobre-collation-y-charset-en-mysql/ neste enlace].
−
  // Veñen datos
+
:* Un usuario con permisos suficientes para realizar as operacións necesarias sobre a base de datos. Neste manual o usuario/password creado son '''user_php/user_php'''.
−
  while($row = $result->fetch_assoc()) {
 
−
    printf("%s-%s",$row["col1"],$row["col2"]);
 
−
  }
 
−
  $result->free();  // Liberamos da memoria os recursos
 
−
}
 
−
else {
 
−
  echo 'Non hai datos ou a consulta está mal feita';
 
−
}
 
−
</syntaxhighlight>
 
  
−
:* Pechamos a conexión.
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
$conex->close();  // Pechamos a conexión
 
−
</syntaxhighlight>
 
  
 +
: Todo o anterior xa está indicado no [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Requirimentos_previos_para_acceso_a_datos punto anterior da wiki].
  
  
  
 +
'''NOTA IMPORTANTE:'''
  
−
=== Caso Práctico ===
+
Nos seguintes apartados explícanse distintas formas de realizar operacións contra unha base de datos MySQL desde PHP: utilizando '''mysqli''' de forma procedemental, '''mysqli''' orientado a obxectos e '''PDO'''.
−
: Imos crear unha consulta que nos devolva todos os libros da táboa LIBROS:
 
−
: Para non ter as consultas no medio do código PHP das diferentes páxinas, imos definila no '''arquivo bd.inc.php''' creado no punto anterior.
 
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
Os exemplos realizados con '''mysqli''' mantéñense nos apuntes con carácter '''informativo e comparativo'''.
−
define ('CONSULTAR_LIBROS','select id_libro,titulo from LIBROS order by titulo');
 
−
</syntaxhighlight>
 
  
 +
'''Nos exames, exercicios e prácticas avaliables deberase empregar PDO, salvo que no enunciado se indique expresamente o contrario.'''
  
−
* '''Normas nos nomes das constantes''':
+
A versión de PHP empregada como referencia nestes apuntes e nos exames será '''PHP 8.5'''.
−
:* Comenzarán cun verbo que vai poder ser: CONSULTAR, INSERIR, MODIFICAR e ELIMINAR
 
−
:* Seguirá cun guión baixo.
 
−
:* En caso de consultar os datos dunha única táboa se indicará o nome da táboa. En caso de consultar datos de varias táboas se indicará con varias palabras separadas con guión baixo que indiquen o obxectivo da consulta.
 
  
  
  
 +
<br />
 +
=== Apertura da conexión ===
  
−
* Aplicado ao exemplo que estamos a seguir...
+
::* '''Utilizando PDO''':
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
public PDO::__construct(
 +
    string $dsn,
 +
    ?string $username = null,
 +
    #[\SensitiveParameter] ?string $password = null,
 +
    ?array $options = null
 +
)
 +
</syntaxhighlight>
 +
:: No caso de PDO é obrigatorio indicar o parámetro dsn (Data Source Name), que identifica o controlador e os datos necesarios para realizar a conexión. No noso caso, ao empregar MySQL, indicaremos o servidor, a base de datos e o xogo de caracteres.
  
−
'''Arquivo: Consultar_Libros.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="26-40" >
 
−
<?php
 
  
−
require ('/var/www/confBD/bd.inc.php');
 
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
: Un exemplo:
−
$conex->set_charset('utf8');
+
:::<syntaxhighlight lang="php enclose="div" highlight="" >
 +
$dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
  
−
if ($conex->connect_error){
+
$opciones = [
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
+
    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);
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
</syntaxhighlight>  
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
+
:: Como vemos modificamos varios atributos da conexión no array $opciones. Podemos consultar todas as opcións no [https://www.php.net/manual/en/pdo.setattribute.php seguinte enlace].
−
<?php      
+
:* Neste caso modificamos o atributo ATTR_ERRMODE e damos o valor ERRMODE_EXCEPTION.
−
       
+
:: O que indica dito atributo é o seguinte:
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
−
        if ($result->num_rows>0){
+
Excepción PDO::ERRMODE_EXCEPTION Fai que PDO lance unha PDOException cando se produce un erro. É o modo predeterminado desde PHP 8.0 e será o que empregaremos no módulo.
−
            echo "<table border='1'>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                echo '<tr>';
 
−
                printf("<td>%s</td>",$row["titulo"]);
 
−
                echo '</tr>';
 
−
            }
 
−
            echo '</table>';
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
        $conex->close();  // Pechamos a conexión
 
−
       
 
−
?>       
 
−
    </body>   
 
−
</html>
 
  
−
</syntaxhighlight>
+
Silencioso PDO::ERRMODE_SILENT Non lanza erros. Simplemente devolve false se hai un fallo. Hai que comprobar manualmente cada operación.
 +
Aviso (Warning) PDO::ERRMODE_WARNING Xera un warning de PHP cando ocorre un erro, pero o script continúa. Obsoleta a partires da versión 8.5. Non empregar.
 +
</syntaxhighlight>  
 +
:* PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC: Indica que cando recollamos datos da base de datos os imos recoller nun array asociativo. Se sabemos empregar a POO podemos optar por FETCH_OBJECT.
 +
:* PDO::ATTR_EMULATE_PREPARES  => false: Isto fai que as sentenzas prepared sexan xestionadas polo xestor de base de datos e non polo php.
  
  
−
* Resultado:
+
<br />
−
[[Imagen:Php_BD_8.jpg|300px|center]]
+
:: Fixarse que a conexión a estamos a facer contra Mysql.
 +
:: Unha das vantaxes de PDO é que proporciona unha interface común para traballar con diferentes xestores de bases de datos. No módulo utilizaremos exclusivamente PDO_MySQL, pero a modo informativo existen outros controladores.
 +
:: Se queremos conectarnos con outra base de datos poderíamos poñer os seguintes valores:
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="1" >
 +
mysql mysql:host=localhost;dbname=php;charset=utf8mb4 Conecta a unha base de datos MySQL ou MariaDB
 +
pgsql pgsql:host=localhost;port=5432;dbname=instituto;user=user_pg;password=1234 Conecta a unha base de datos PostgreSQL
 +
sqlite sqlite:/var/www/basedatos.db Conecta a unha base de datos SQLite (sen servidor)
 +
sqlsrv sqlsrv:Server=localhost;Database=instituto Conecta a Microsoft SQL Server
 +
oci oci:dbname=xe;charset=UTF8 Conecta a Oracle
 +
ibm ibm:DRIVER={IBM DB2 ODBC DRIVER};DATABASE=db;HOSTNAME=host;PORT=50000;PROTOCOL=TCPIP;UID=user;PWD=pass; Conecta a IBM DB2
 +
firebird firebird:dbname=localhost:/ruta/a/base.fdb Conecta a Firebird
 +
dblib dblib:host=localhost;dbname=instituto Conecta a Sybase ou Microsoft SQL Server antigo
 +
informix informix:host=host;service=puerto;database=db;server=servidor;protocol=onsoctcp;UID=user;PWD=pass; Conecta a Informix
 +
odbc odbc:mi_dsn Usa unha fonte de datos ODBC definida no sistema
 +
</syntaxhighlight>
  
  
−
* Fixarse como neste exemplo, aínda que a consulta devolve o id_libro, como non o necesitamos non o amosamos ao usuario, xa que dito dato é un código interno que utilizamos como clave primaria.
 
  
  
 +
<br />
 +
'''NOTA:''' O indicado a continuación inclúese a nivel informativo. Nos exames empregarase PDO.
  
−
* O método fetch_assoc() move un punteiro interno á seguinte fila de resultados.
+
* Utilizando procedementos: [https://secure.php.net/manual/es/function.mysqli-connect.php mysqli_connect]
−
: Se queremos modificar dito punteiro e situarnos en calquera fila do resultado, debemos facer uso da [http://php.net/manual/es/mysqli-result.data-seek.php función mysqli_data_seek]·
+
* Utilizando obxectos: [https://secure.php.net/manual/es/mysqli.construct.php mysql::_construct]
  
 +
* Sintaxe (se indica a sintaxe do construtor e polo tanto será equivalente a facer un 'new' da clase mysqli):
  
 +
::* '''Orientada a obxectos''':
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
mysqli::__construct ([ string $host = ini_get("mysqli.default_host") [, string $username = ini_get("mysqli.default_user") [, string $passwd = ini_get("mysqli.default_pw") [, string $dbname = "" [, int $port = ini_get("mysqli.default_port") [, string $socket = ini_get("mysqli.default_socket") ]]]]]] )
 +
</syntaxhighlight>
  
−
=== Consulta enviando parámetros de tipo numérico ===
+
::* '''Por procedementos''':
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
mysqli mysqli_connect ([ string $host = ini_get("mysqli.default_host") [, string $username = ini_get("mysqli.default_user") [, string $passwd = ini_get("mysqli.default_pw") [, string $dbname = "" [, int $port = ini_get("mysqli.default_port") [, string $socket = ini_get("mysqli.default_socket") ]]]]]] )
 +
</syntaxhighlight>
  
−
<u>Nota:</u> Lembra que no paso de datos temos que ter en conta os posibles 'ataques' que poidan intentar facerse. Repasar o dado [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios na wiki anteriormente].
 
  
 +
: Todos eles son optativos, pero normalmente indicaremos o nome de host, login, password e base de datos.
 +
: Os parámetros son:
 +
:* Host a conectar (cadea): Pode ser o nome ou a dirección IP. Normalmente imos poñer 'localhost' (se o xestor de bases de datos está instalado no mesmo equipo que o servidor web).
 +
:* Nome do usuario con acceso á base de datos (cadea): Nome do usuario que ten acceso á base de datos.
 +
:* Password (cadea): Password do usuario.
 +
:* Dbname  (cadea): Nome da base de datos sobre a que imos traballar.
 +
:* Porto (numérico): Porto no que está escoitando o xestor de base de datos. No caso de mysql é o 3306.
 +
:* Socket (cadea): Socket ou tubería para conectar có xestor de bases de datos. En Linux é un tipo especial de arquivo que serve para 'transmitir' información, neste caso, cara o xestor de bases de datos.
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
 
−
: A forma de facelo é utilizando a consulta SQL cun parámetro e substituíndo dito parámetro polo valor que queiramos enviar.
 
  
−
* Así: SELECT col1, col2 FROM TABOA_1 WHERE col1='''%d'''
+
<br />
  
−
: No exemplo, col1 é de tipo numérico.
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define ('CONSULTA_CON_PARAM_NUMERICO','SELECT col1, col2 FROM TABOA_1 WHERE col1=%d');
 
  
−
$query = sprintf(CONSULTA_CON_PARAM_NUMERICO,valor_numérico_a_enviar);
+
* Aplicado ao noso caso, teremos a seguinte orde:
 +
:* Por procedementos:
 +
:::<syntaxhighlight lang="php" line enclose="div" highlight="" >
 +
$conex = mysqli_connect('localhost','user_php','user_php','php');  
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
−
: Agora facemos o mesmo código que no exemplo anterior.
+
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
:* Orientado a obxectos:
−
......
+
:::<syntaxhighlight lang="php" line enclose="div" highlight="" >
−
$result=$conex->query(CONSULTA_CON_PARAM_NUMERICO);
+
$conex = new mysqli('localhost','user_php','user_php','php');  
−
......
 
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
 +
:: $conex é un obxecto que represente a conexión á base de datos.
  
  
−
==== Caso práctico: Enviando parámetros de tipo numérico ====
 
  
−
* Seguindo con exemplo, imos facer que cando o usuario seleccione un libro dentro dunha combobox, aparezan os exemplares do mesmo.
 
−
: Para iso teremos que:
 
−
:* Crear a sentencia SQL correspondente enviando un parámetro que vai ser o código do libro seleccionado.
 
−
:* Obter dito dato. Isto o faremos utilizando un formulario cun método POST.
 
−
:* Modificar o código da páxina anterior (Consultar_Libros.php) e facer que os libros aparezan nunha combobox (dropdownlist ou <select> en html).
 
−
:* Facer que cando o usuario escolla un libro 'active' o formulario e polo tanto provoque o 'submit' do mesmo amosando os exemplares do libro seleccionado.
 
−
:* Manter seleccionado o libro na combo.
 
  
 +
<br />
 +
==== Control de errores ====
  
−
<u>Nota:</u> Ao principio ides ter problema de tipo 'non amosa ningún dato'. O primeiro que tedes que descartar é que a consulta SQL estea ben e devolva datos, polo que vos aconsello que fagades un 'echo' de dita consulta e a executedes no MysqlWorkBench para ver que realmente devolve algún resultado.
+
* '''En versións de PHP igual ou superior á 8.1''': Neste caso, debemos empregar a forma try ... catch da programación orientada a obxectos. '''ESTE É A FORMA QUE IMOS EMPREGAR NOS EXAMES''':
  
 +
:* Versión con chamada empregando pdo:
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
try {
 +
  $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
 +
 
 +
  $usuario = 'user_php';
 +
  $password = 'user_php';
  
 +
  $opciones = [
 +
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
 +
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
    PDO::ATTR_EMULATE_PREPARES  => false
 +
  ];
  
−
* Crearemos unha nova páxina de nome '''Consultar_Exemplares_Por_Libro.php'''.
+
  $conex = new PDO($dsn, $usuario, $password, $opciones);
  
−
: Modificamos o código para que aparezan os libros dentro da etiqueta <select> de html:
+
}
−
: Neste caso, na combo imos gardar o código do libro xa que cando escollamos un, necesitamos obter dito código para facer outra consulta buscando os exemplares dese libro...
+
catch (PDOException $e) {
 +
    die("Error de conexión PDO: (" . $e->getCode() . ") " . $e->getMessage());
 +
}
 +
} catch (Throwable $e) {
 +
    die('Erro xeral: ' . $e->getMessage());
 +
}
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
    <body>
 
−
<?php       
 
−
       
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && $result->num_rows>0){
 
−
            echo "<select name='lstLibros'>";
 
−
            echo "<option value='-1'></option>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                printf("<option value='%d'>%s</option>",$row["id_libro"],$row["titulo"]);
 
−
            }
 
−
            echo '</select>';
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
        $conex->close();  // Pechamos a conexión
 
−
       
 
−
?>       
 
−
    </body>   
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
:* Liña 7: Engadimos unha entrada en branco un valor -1 no seu value, para 'obrigar' a escoller un valor e que por defecto non apareza cun libro seleccionado.
 
−
: Dependerá da aplicación, podedes ter que implementar variantes como ter unha entrada que indique 'TODAS' ou facer que por defecto estea seleccionada algunha das entradas...
 
  
−
: Resultado:
+
:<u>Nota:</u>
−
[[Imagen:Php_BD_9.jpg|300px|center]]
+
:* Normalmente ao usuario non se lle vai amosar o erro que devolve o xestor de base de datos e dito erro sóese gardar nunha táboa interna ou nun arquivo de log para o administrador e que poida ver cal foi o erro.
 +
:* Podemos facer un único try-catch que envolva toda a páxina PHP ou facer un try-catch por cada bloque php que poida xerar unha excepción.
 +
:* Os atributos:
 +
::*PDO::ERRMODE_EXCEPTION: Valor predeterminado a partires da versión 8.0, pero o imos poñer de todas formas.
 +
::*PDO::FETCH_ASSOC: Cando consultemos datos devolverá os mesmos en forma de array asociativo. Se poñemos PDO::FETCH_OBJ obteremos os datos como propiedades de clase coma $row->nome.
 +
::*ATTR_EMULATE_PREPARES  => false : Fai que as sentenzas prepared (o veremos posteriormente) as faga o xestor de base de datos.
 +
 
 +
 
 +
: Por exemplo:
 +
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
try {
 +
 
 +
    $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
 +
 
 +
    $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);
 +
 
 +
} catch (PDOException $e) {
 +
    die("Erro de conexión: (" . $e->getCode() . ') ' . $e->getMessage());
 +
}
 +
} catch (Throwable $e) {
 +
    die('Erro xeral: ' . $e->getMessage());
 +
}
 +
</syntaxhighlight>
  
  
  
 +
<br />
 +
: <u>Nota</u>Lembrar que cando se produce un erro non se debe amosar ao usuario nin o getCode() nin o getMessage(). Isto soamente é necesario en depuración, non en producción.
  
−
* '''Arquivo bd.inc.php'''
+
: Dará como resultado:
−
: Definimos nunha constate a consulta que vai obter os exemplares do libro seleccionado:
+
[[Imagen:Php_BD_3.jpg|400px|center]]
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id from EXEMPLARES where libro_id=%d order by estado_id');
 
−
</syntaxhighlight>
 
−
:: Como vemos imos enviar o código por medio dun parámetro de tipo numérico (usaremos a función sprintf).
 
−
: Definimos nunha constante onde se atopan as imaxes relacionados cos libros:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
define ('CARTAFOL_IMAXES_WEB','http://www.meusitio.es/IMAXES/');
 
−
</syntaxhighlight>
 
−
:: Teredes que cambiar a url pola vosa.
 
  
  
  
−
* Para facer que cando se seleccione un elemento do lista se 'active' o formulario, existen múltiples formas, unha delas pode ser:
 
−
:::            echo "<select name='lstLibros' <b>onchange='this.form.submit()'</b>>";
 
  
−
:* O código que vai amosar os exemplares do libro seleccionado será o seguinte:
+
<br />
 +
'''NOTA:''' O indicado a continuación inclúese a nivel informativo. Nos exames empregarase PDO.
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="3,5,7-17" >
+
:* Por procedementos:
−
        <?php       
+
::<syntaxhighlight lang="php" enclose="div" highlight="" >
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO
+
try {
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
+
    $conex = mysqli_connect('localhost', 'usuario_inexistente', 'contrasena_incorrecta', 'base_de_datos');
−
           
+
} catch (mysqli_sql_exception $e) {
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$_POST['lstLibros']);
+
    die("Erro de conexión: " . $e->getMessage());
−
            $result=$conex->query($query);
+
}
−
            if ($result && ($result->num_rows>0)){
+
} catch (Throwable $e) {
−
                echo "<table border='1'>";
+
    die('Erro xeral: ' . $e->getMessage());
−
                echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
+
}
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<tr>';
 
−
                    printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%s</td><td>%d</td>",$row["id_exemplar"],$row["imaxe"],CARTAFOL_IMAXES_WEB . $row["imaxe"],$row["prezo"],$row["estado_id"]);
 
−
                    echo '</tr>';
 
−
                }
 
−
                echo '</table>';
 
−
                $result->free(); // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
           
 
−
        }
 
−
        $conex->close();  // Pechamos a conexión
 
−
       
 
−
        ?>       
 
  
−
</syntaxhighlight>
+
</syntaxhighlight>  
  
−
::* Liña 3: Comprobamos que veñan datos no formulario (que se escollera algún exemplar da lista) e que sexa de tipo numérico. Tamén comprobamos se seleccionamos o elemento baleiro (valor -1 na combo).
 
−
::* Liña 5: Definimos a consulta a realizar contra a base de datos, utilizando a constante definida e substituíndo o parámetro polo código do libro seleccionado.
 
−
::* Liñas 7-17: Construímos a táboa para que amose os datos. Fixarse que na imaxe temos que definir o atributo 'alt' para que cumpra o estándar XHTML, polo que enviamos dúas veces o nome do arquivo. No dato que vai no atributo 'src' enviamos o nome do arquivo xunto coa URL onde se atopan as imaxes.
 
  
−
::* Liña 23: Pechamos a conexión. Fixarse que esta liña debe aparecer unha vez. Se non teremos que volver a abrir a conexión á base de datos.
 
  
 +
:* Versión con chamada empregando obxectos:
 +
::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
try {
 +
    $conex = new mysqli('localhost', 'usuario_inexistente', 'contrasena_incorrecta', 'base_de_datos');
 +
} catch (mysqli_sql_exception $e) {
 +
    die("Erro de conexión: " . $e->getMessage());
 +
}
 +
} catch (Throwable $e) {
 +
    die('Erro xeral: ' . $e->getMessage());
 +
}
  
 +
</syntaxhighlight>
  
 +
<br />
 +
* Por exemplo, imos modificar o password do usuario por un que non exista e veremos o resultado:
 +
::<syntaxhighlight lang="php" line enclose="div" highlight="2" >
 +
try {
 +
    $conex = mysqli_connect('localhost','user_php','password_incorrecta','php');
 +
} catch (mysqli_sql_exception $e) {
 +
    die("Erro de conexión: (" . $e->getCode() . ') ' . $e->getMessage());
 +
}
 +
} catch (Throwable $e) {
 +
    die('Erro xeral: ' . $e->getMessage());
 +
}
  
−
* Facemos que se manteña seleccionado o libro:
+
</syntaxhighlight>  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="7-13" >
 
−
        <?php       
 
−
        $result=$conex->query(CONSULTAR_LIBROS);
 
−
        if ($result && ($result->num_rows>0)){
 
−
            echo "<select name='lstLibros' onchange='this.form.submit()'>";
 
−
            echo "<option value='-1'></option>";
 
−
            while($row = $result->fetch_assoc()) {
 
−
                if(isset($_POST['lstLibros']) && $row['id_libro']==$_POST['lstLibros']){
 
−
                    $cadea="selected='selected'";
 
−
                }
 
−
                else{
 
−
                    $cadea='';
 
−
                }
 
−
                printf("<option value='%d' %s>%s</option>",$row["id_libro"],$cadea,$row["titulo"]);
 
−
            }
 
−
            echo '</select>';
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
       
 
−
        ?>       
 
−
</syntaxhighlight>
 
  
  
 +
<br />
  
 +
=== Gardando os datos de conexión nun lugar seguro ===
  
 +
* Como é obvio os datos de base de datos, usuario e password son moi importantes e non debemos permitir que alguén poida descubrilos.
  
 +
* O que faremos será o seguinte:
  
−
* O código completo sería o seguinte:
+
:* Definiremos en forma de constantes ditos datos nun arquivo de include.
 +
:* Gardaremos dito arquivo '''fora''' do cartafol de acceso do noso sitio web.
  
−
'''Arquivo: bd.inc.php''':
+
* O primeiro punto, definir en forma de constante e nun arquivo de include os datos da conexión:
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
'''Arquivo: bd.php''':
 +
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
 
<?php
 
<?php
  
Línea 695: Línea 553:
 
define ('USUARIO','user_php');
 
define ('USUARIO','user_php');
 
define ('PASSWORD','user_php');
 
define ('PASSWORD','user_php');
−
define ('BD','PHP');
+
define ('BD','php');
 +
</syntaxhighlight>
 +
 
 +
:* Faremos un '''require''' deste arquivo en todas as páxinas que necesiten unha conexión á base de datos. Desta forma, se necesitamos cambiar algo soamente o cambiaríamos nun arquivo.
 +
 
 +
 
 +
* O segundo punto consiste en descubrir cal é a ruta física á que apunta o noso servidor web e colocar o arquivo de include 'fora' desa ruta.
 +
:* Temos [http://wiki.cifprodolfoucha.es/index.php?title=DRUPAL_Instalaci%C3%B3n#Instalaci.C3.B3n_do_Apache neste punto da Wiki] información sobre como funciona Apache e como saber a onde apunta o servidor web no seu arquivo de configuración.
 +
 
 +
:* Temos varias opcións:
 +
::* Facendo uso da [http://php.net/manual/es/language.constants.predefined.php constante __DIR__].
 +
::* Facendo uso do array global [http://php.net/manual/es/reserved.variables.server.php $_SERVER] e dentro deste a entrada $_SERVER['DOCUMENT_ROOT'].
 +
 
 +
:Se facemos un echo destas variables dentro dunha páxina do noso sitio web, obteremos unha ruta, coma por exemplo:
 +
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
 +
/var/www/html/
 +
</syntaxhighlight>
 +
 
 +
:<u>Nota:</u> Se o facemos coa constante __DIR__ e a páxina que está a facer o echo se atopa dentro dun cartafol, tamén aparecerá o cartafol (por exemplo /var/www/html/cartafol/).
 +
 
  
−
define ('CARTAFOL_IMAXES_WEB','http://www.meusitio.es/IMAXES/');
 
  
−
define ('CONSULTAR_LIBROS','select id_libro,titulo from LIBROS order by titulo');
+
:* Isto quere dicir que o servidor web (e polo tanto calquera usuario de Internet) vai poder acceder as páxinas que se atopan no cartafol /var/www/html e calquera dentro del.
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id from EXEMPLARES where libro_id=%d order by estado_id');
 
  
−
</syntaxhighlight>
+
:: Unha medida de seguridade é colocar o arquivo de include 'fora' desta ruta. Por exemplo, podemos crear un arquivo no cartafol '''/var/www/confBD'''
  
 +
:: A dito cartafol ten que poder acceder o usuario que utiliza o Apache para acceder aos cartafoles a nivel do S.O. (no caso de Apache soe ser www-data, podedes miralo no [http://wiki.cifprodolfoucha.es/index.php?title=DRUPAL_Instalaci%C3%B3n#Instalaci.C3.B3n_do_Apache punto da wiki indicado anteriormente]).
  
−
'''Arquivo: Consultar_Exemplares_Por_Libro.php''':
+
<gallery caption="Creando o cartafol onde se vai gardar o arquivo de configuración" widths="350" heights="300px" perrow="2">
 +
Image:Php_BD_4.jpg| Facemos un '''cd /var/www'''. Creamos o cartafol: '''sudo mkdir confBD'''. <u>Movemos o arquivo de include creado no paso anterior ao cartafol:</u> '''mv /cartafol_arquivo_include/bd.inc.php /var/www/confBD'''.
 +
Image:Php_BD_5.jpg| Cambiamos os permisos: '''sudo chmod -R 550 /var/www/confBD''' e cambiamos o propietario: '''sudo chown -R  www-data:www-data /var/www/confBD'''. <u>Nota:</u> O usuario e grupo pode variar dependendo do S.O. En Linux podedes sabelo mirando [http://wiki.cifprodolfoucha.es/index.php?title=DRUPAL_Instalaci%C3%B3n#Arquivos_de_configuraci.C3.B3n este enlace da wiki]. En Windows o podedes saber [http://httpd.apache.org/docs/2.0/platform/windows.html#winsvc neste enlace].
 +
</gallery>
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
<?php
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
:* Agora, calquera páxina que faga uso destes datos terá que facer o seguinte require:
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
:* No caso de PDO:
−
$conex->set_charset('utf8');
+
::<syntaxhighlight lang="php" line enclose="div" highlight="3,7,8" >
 +
<?php
  
−
if ($conex->connect_error){
+
require '/var/www/confBD/bd.php';
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
−
?>
 
  
 +
try {
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
     $dsn = sprintf(
−
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
         'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
        SERVIDOR,
−
    <head>
+
         BD
−
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
     );
−
         <title>Operacións BD</title>
 
−
     </head>
 
  
−
     <body>
+
     $opciones = [
−
         <form action="<?php echo $_SERVER['PHP_SELF'] ?>" method='post'>
+
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
 +
         PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
        PDO::ATTR_EMULATE_PREPARES  => false
 +
    ];
  
−
        <?php       
+
    $conex = new PDO(
−
        $result=$conex->query(CONSULTAR_LIBROS);
+
         $dsn,
−
         if ($result && ($result->num_rows>0)){
+
         USUARIO,
−
            echo "<select name='lstLibros' onchange='this.form.submit()'>";
+
         PASSWORD,
−
            echo "<option value='-1'></option>";
+
         $opciones
−
            while($row = $result->fetch_assoc()) {
+
    );
−
                if(isset($_POST['lstLibros']) && $row['id_libro']==$_POST['lstLibros']){
 
−
                    $cadea="selected='selected'";
 
−
                }
 
−
                else{
 
−
                    $cadea='';
 
−
                }
 
−
                printf("<option value='%d' %s>%s</option>",$row["id_libro"],$cadea,$row["titulo"]);
 
−
            }
 
−
            echo '</select>';
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
         }
 
−
         else {
 
−
            echo "Non hai datos que amosar!!!!";
 
−
        }
 
−
       
 
−
        ?>       
 
−
        </form>
 
−
       
 
−
       
 
−
        <?php       
 
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO
 
−
         if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
 
−
           
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$_POST['lstLibros']);
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<table border='1'>";
 
−
                echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<tr>';
 
−
                    printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%s</td><td>%d</td>",$row["id_exemplar"],$row["imaxe"],CARTAFOL_IMAXES_WEB . $row["imaxe"],$row["prezo"],$row["estado_id"]);
 
−
                    echo '</tr>';
 
−
                }
 
−
                echo '</table>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
           
 
−
        }
 
−
        $conex->close(); // Pechamos a conexión
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
 +
} catch (PDOException $e) {
 +
    die('Erro de conexión coa base de datos.');
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
'''IMPORTANTE:''' Este será o modelo de conexión que se empregará como referencia nos exames.
  
  
  
  
 +
:* No caso de orientado a obxectos:
 +
::<syntaxhighlight lang="php" line enclose="div" highlight="6" >
 +
<?php
  
 +
//error_reporting(0);  // Inabilita os erros de Apache se temos activada a directiva display_erros no php.ini
  
−
* Imos realizar <u>unha mellora</u> nos datos que se visualizan.
 
−
: Como vemos, aparece o código do estado do libro. O que queremos amosar é a descrición, polo que teremos que facer un Join coa táboa ESTADOS.
 
  
−
'''Arquivo bd.inc.php''':
+
require ('/var/www/confBD/bd.php');
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="14" >
+
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id,ESTADOS.descripcion as desc_estado'
 
−
        . ' from EXEMPLARES INNER JOIN ESTADOS ON (ESTADOS.id_estado=EXEMPLARES.estado_id)'
 
−
        . ' where libro_id=%d order by desc_estado');
 
  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
  
−
'''Arquivo: Consultar_Exemplares_Por_Libro.php''':
 
  
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
* Se queremos facer o mesmo pero sen utilizar rutas absolutas (por exemplo se estamos nun aloxamento compartido):
−
        ..................
+
::<syntaxhighlight lang="php" line enclose="div" highlight="6-7" >
−
 
+
<?php
−
        <?php      
 
−
        // CONSULTAMOS OS EXEMPLARES DUN LIBRO SELECCIONADO
 
−
        if (!empty($_POST['lstLibros']) && filter_var($_POST['lstLibros'],FILTER_VALIDATE_INT) && $_POST['lstLibros']!=-1){
 
−
           
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_LIBRO,$_POST['lstLibros']);
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<table border='1'>";
 
−
                echo "<th>Num. Exemplar</th><th>Imaxe</th><th>Prezo</th><th>Estado</th>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<tr>';
 
−
                    printf("<td>%d</td><td><img alt='%s' src='%s' width='42' height='42' /></td><td>%s</td><td>%s</td>",$row["id_exemplar"],$row["imaxe"],CARTAFOL_IMAXES_WEB . $row["imaxe"],$row["prezo"],$row["desc_estado"]);
 
−
                    echo '</tr>';
 
−
                }
 
−
                echo '</table>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
           
 
−
        }
 
−
        $conex->close();  // Pechamos a conexión
 
−
       
 
−
        ?>       
 
  
−
        ..................
+
  $ruta = substr($_SERVER['DOCUMENT_ROOT'],0,strpos($_SERVER['DOCUMENT_ROOT'],'html')) . 'confBD'; // RUTA al sitio web sin /html pero con /confBD
 +
  require ($ruta.'/bd.php');
  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
* Liña 14: Modificamos o tipo de parámetro que pasa de ser numérico (%d) a cadea (%s) e amosamos a descrición do estado (as dúas columnas do select non deben ter o mesmo nome, descrición do libro e descrición do estado).
 
  
 +
<br />
 +
===O xogo de caracteres===
  
 +
* Debemos ter en conta que unha páxina web está 'aberta' a todo o mundo que se conecte a ela.
 +
: Cada persoa en diferentes países terá diferente xogos de caracteres no que cada carácter está relacionado cun código numérico.
  
−
* Resultado:
+
* Se gardamos como dato dentro da base de datos unha cadea cun 'ñ' entre os seus datos, dito carácter estará asociado a un código nun xogo de caracteres. Se o usuario que vai cargar a páxina o carga cun xogo de caracteres diferente non vai poder visualizalo.
  
−
[[Imagen:Php_BD_12.jpg|400px|center]]
 
  
 +
* Cando creamos aplicacións en PHP con acceso a base de datos teremos que ter en conta os seguintes xogos de caracteres:
  
 +
:* Xogo de caracteres da base de datos (no noso caso Mysql): É o que se coñece como [https://dev.mysql.com/doc/refman/5.5/en/charset-charsets.html '''Charset/Collation'''].
 +
[[Imagen:Php_BD_7.jpg|300px|center]]
 +
::: Se vos fixades cando creamos a base de datos en Mysql, escollemos o charset (primeira columna) e o collation (segunda columna).
 +
::: Como indicamos antes, cando creamos a base de datos o charset/collation indicado foi '''utf8mb4/utf8mb4_spanish2_ci'''.
 +
::: O charset é como mysql garda internamente os caracteres e os relaciona cun código numérico e o collation indica como comparar e ordenar o texto. Podedes consultar [http://blog.unreal4u.com/2012/08/sobre-collation-y-charset-en-mysql/ esta blog] para obter máis información.
  
 +
:* Xogo de caracteres da páxina HTML.
  
 +
::: Na páxina web indicamos coa etiqueta <meta> o xogo de caracteres para que o navegador web poida visualizalos correctamente.
  
 +
::: No caso que nos ocupa teremos que poñer:
 +
:::* Html 5 e posteriores: <meta <b>charset="UTF-8"</b>>
  
−
'''EXERCICIO PROPOSTO''':
+
::: Máis información en [http://www.w3schools.com/html/html_charset.asp w3schools].
−
 
 
−
: Crea unha páxina de nome '''Consultar_Exemplares_Por_Estado.php''' no que aparezan en forma de radiobutton os estados da táboa ESTADOS.  
 
−
: Ao seleccionar un estado, amosaranse en forma de lista ordenada os exemplares (libro_id,num_exemplar,prezo)  ordenados por libro_id (quen queira pode facer un join coa táboa LIBROS e amosar o título en vez do libro_id e ordenar os resultados polo título).
 
−
: O radiobutton debe manterse seleccionado.
 
  
 +
:* No código PHP ao conectar coa base de datos.
 +
::: Cando conectemos coa base de datos teremos que indicarlle que imos utilizar o xogo de caracteres utf8mb4 con esta liña:
  
 +
:::* Versión PDO:
 +
::::<syntaxhighlight lang="php" line enclose="div" highlight="5" >
 +
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
 +
    ];
  
−
'''POSIBLE SOLUCIÓN''':
+
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        $opciones
 +
    );
  
−
'''Arquivo bd.inc.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define ('CONSULTAR_ESTADOS','select id_estado,descripcion from ESTADOS order by descripcion');
 
−
define ('CONSULTAR_EXEMPLARES_POR_ESTADO','select titulo,id_exemplar,prezo '
 
−
        . ' from EXEMPLARES INNER JOIN LIBROS ON (LIBROS.id_libro=EXEMPLARES.libro_id)'
 
−
        .                ' INNER JOIN ESTADOS ON (ESTADOS.id_estado=EXEMPLARES.estado_id)'
 
−
        . ' where estado_id=%d order by titulo');
 
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
−
: Definimos dúas consultas. Unha para buscar os estados e outra para buscar os exemplares que teñan un estado determinado (pasado como parámetros)
+
* '''É importante empregar unha configuración coherente de Unicode en toda a aplicación. Neste módulo empregaremos utf8mb4 en MySQL e na conexión PDO, e UTF-8 nas páxinas HTML.'''.
  
  
−
'''Arquivo Consultar_Estados_Por_Estado.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
<?php
 
  
−
require ('/var/www/confBD/bd.inc.php');
 
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
:::* Versión procedimental
−
$conex->set_charset('utf8');
+
:: Para iso teremos que facer uso da [https://secure.php.net/manual/es/mysqli.set-charset.php función set_charset].
 +
:: En caso de éxito devolve true ou false en caso de existir algún problema.
  
−
if ($conex->connect_error){
+
::::<syntaxhighlight lang="php" line enclose="div" highlight="4" >
−
     die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
+
try {
 +
    require ('/var/www/confBD/bd.php');
 +
    $conex = mysqli_connect(SERVIDOR, USUARIO, PASSWORD, BD);
 +
     mysqli_set_charset($conex,'utf8mb4');
 +
} catch (mysqli_sql_exception $e) {
 +
    die("Erro de conexión: " . $e->getMessage());
 
}
 
}
−
?>
+
</syntaxhighlight>  
 +
 
  
 +
:::* Versión orientada a obxectos:
 +
::::<syntaxhighlight lang="php" line enclose="div" highlight="4" >
 +
try {
 +
    require ('/var/www/confBD/bd.php');
 +
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
 +
    $conex->set_charset('utf8mb4');
 +
} catch (mysqli_sql_exception $e) {
 +
    die("Erro de conexión: " . $e->getMessage());
 +
}
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
</syntaxhighlight>  
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
    </head>
 
  
−
    <body>
 
−
        <form action="<?php echo $_SERVER['PHP_SELF'] ?>" method='post'>
 
  
−
        <?php       
 
−
        $cont=0;    // Conta o número de estados impresos e cando leve múltiplo de dous salta de liña
 
−
        $result=$conex->query(CONSULTAR_ESTADOS);
 
−
        if ($result && $result->num_rows>0){
 
−
            while($row = $result->fetch_assoc()) {
 
−
                $cont++;
 
−
                if(isset($_POST['rbgrpEstado']) && $row['id_estado']==$_POST['rbgrpEstado']){
 
−
                    $cadea="checked='checked'";
 
−
                }
 
−
                else{
 
−
                    $cadea='';
 
−
                }
 
−
                printf("<input onclick='javascript:this.form.submit()' type='radio' name='rbgrpEstado' value='%d' %s>%s</input>",$row["id_estado"],$cadea,$row["descripcion"]);
 
−
                if(($cont%2)==0){
 
−
                    echo "<br />";
 
−
                }
 
−
            }
 
−
            $result->free();  // Liberamos da memoria os recursos
 
−
        }
 
−
        else {
 
−
            echo "Non hai estados que amosar!!!!";
 
−
        }
 
−
       
 
−
        ?>       
 
−
        </form>
 
−
       
 
−
       
 
−
        <?php       
 
−
        // CONSULTAMOS OS EXEMPLARES DUN ESTADO SELECCIONADO
 
−
        if (!empty($_POST['rbgrpEstado']) && filter_var($_POST['rbgrpEstado'],FILTER_VALIDATE_INT)){
 
−
           
 
−
            $query=sprintf(CONSULTAR_EXEMPLARES_POR_ESTADO,$_POST['rbgrpEstado']);
 
−
            $result=$conex->query($query);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<ol>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    echo '<li>';
 
−
                    printf("T&iacute;tulo:%s - Num_exemplar:%d - Prezo:%s",$row["titulo"],$row["id_exemplar"],$row["prezo"]);
 
−
                    echo '</li>';
 
−
                }
 
−
                echo '</ol>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar!!!!";
 
−
            }
 
−
           
 
−
        }
 
−
   
 
−
        $conex->close();  // Pechamos a conexión
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
−
</syntaxhighlight>
 
  
 +
<br />
 +
===Peche da conexión===
  
 +
* Despois de facer as diferentes operacións sobre a base de datos, teremos que pechar a conexión.
 +
: Para iso temos que chamar á [http://php.net/manual/es/mysqli.close.php función mysqil::close] nas versión orientada a obxectos e procedimental. No caso de PDO chega con igual a null o obxecto $conex.
  
 +
:<u>Nota:</u> Neste exemplo partimos que estamos a empregar a verión PHP 8.1 ou superior. En todas as páxinas se opta por facer un único try-catch, pero dependendo de se se quere saber a sección onde foi o erro, se podería facer varios try-catch.
  
−
* Resultado:
 
  
−
[[Imagen:Php_BD_13.jpg|400px|center]]
+
:* '''Versión PHP 8.1 ou superior'''
  
−
=== Consulta enviando parámetros de tipo cadea ===
 
  
−
* Cando utilicemos datos de tipo cadea, debemos lembrar poñelos entre comillas simples ('dato') na orde SQL.
+
:::* Versión PDO:
 +
: En PDO non existe un método close(). Para pechar explicitamente unha conexión podemos asignar null á variable que contén o obxecto PDO. Se non o facemos, PHP pechará automaticamente a conexión ao rematar a execución do script.
  
 +
::::<syntaxhighlight lang="php" enclose="div" highlight="36" >
 +
<?php
  
−
<u>Nota:</u> Lembra que no paso de datos temos que ter en conta os posibles 'ataques' que poidan intentar facerse. Repasar o dado [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios na wiki anteriormente].
+
require ('/var/www/confBD/bd.php');
  
 +
try {
  
−
* Cando facemos unha consulta ou operación contra base de datos (alta, baixa e modificación) imos ter que 'enviar' datos.
+
    $dsn = sprintf(
−
: A forma de facelo é utilizando a consulta SQL cun parámetro e substituíndo dito parámetro polo valor que queiramos enviar.
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
* Así: SELECT col1, col2 FROM TABOA_1 WHERE col2=''''%d''''
+
    $opciones = [
 +
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
 +
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
        PDO::ATTR_EMULATE_PREPARES  => false
 +
    ];
  
−
: No exemplo, col2 é de tipo cadea (por exemplo, varchar ou char).
+
    $conex = new PDO(
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
        $dsn,
−
define ('CONSULTA_CON_PARAM_CADEA',"SELECT col1, col2 FROM TABOA_1 WHERE col2='%s'");
+
        USUARIO,
 +
        PASSWORD,
 +
        $opciones
 +
    );
  
−
$query = sprintf(CONSULTA_CON_PARAM_CADEA,'valor_cadea_a_enviar');
+
    // ..............
−
</syntaxhighlight>
+
    // Operaciones contra a base de datos e amosar información ao usuario
−
: Fixarse como o parámetro col2 vai entrecomillado e o formato do parámetro é '%s'.
+
    // ..............
  
 +
} catch (PDOException $e) {
 +
    die("Error PDO: " . $e->getMessage());
 +
} catch (Throwable $e) {
 +
    die("Error general: " . $e->getMessage());
 +
}
  
−
: Agora facemos o mesmo código que no exemplo anterior.
+
$conex = null;   // Poderíamos non poñer nada
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
......
 
−
$result=$conex->query(CONSULTA_CON_PARAM_CADEA);
 
−
......
 
−
</syntaxhighlight>
 
  
  
 +
</syntaxhighlight>
  
−
==== Caso práctico: Enviando parámetros de tipo cadea ====
 
  
−
* Imos a aplicalo ao noso exemplo e imos crear unha páxina no que buscaremos libros en base a un nome do autor.
 
−
:: A busca farase utilizando a [http://www.w3schools.com/sql/sql_like.asp orde SQL '''like'''], que busca patróns de cadea. Este operador non distingue maiúsculas de minúsculas.
 
−
:: Se queremos distinguir maiúsculas de minúsculas utilizaremos o operador [http://stackoverflow.com/questions/14007450/how-do-you-force-mysql-like-to-be-case-sensitive like binary].
 
−
:: O operador like o imos aplicar ao nome completo do autor, polo que teremos que concatenar o nome e apelido. Usaremos a función [http://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_concat-ws concat_ws] ou [http://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_concat concat].
 
  
  
−
* O usuario introducirá nunha caixa de texto o nome (ou parte del) do autor.
+
:::* Versión procedimental
 +
::::<syntaxhighlight lang="php" enclose="div" highlight="21-25" >
 +
<?php
  
−
: <u>Nota:</u> Lembra que no paso de datos temos que ter en conta os posibles 'ataques' que poidan intentar facerse. Repasar o dado [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios na wiki anteriormente].
+
require ('/var/www/confBD/bd.php');
  
−
:: Mysql incorpora tamén unha función para codificar os caracteres (como espazos en branco, saltos de liña,...[http://php.net/manual/es/mysqli.real-escape-string.php Función string mysqli::real_escape_string ( string $escapestr )]. Devolve a cadea 'codificada'.
+
try {  // O try se pecha na sección catch do final
−
:: Por exemplo:
+
    $conex = mysqli_connect(SERVIDOR, USUARIO, PASSWORD, BD);
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
    mysqli_set_charset($conex,'utf8mb4');
−
$cidade = "'s Hertogenbosch";
+
?>
 +
  ..............
 +
  // Operacións contra a base de datos e amosar información ao usuario
 +
  ..............
  
−
/* esta consulta fallará debido a que no escapa $ciudad */
+
<?php
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
+
}  // Fin do try do principio da páxina
−
     printf("Erro: %s\n", $mysqli->sqlstate);
+
catch (mysqli_sql_exception $e) {
 +
    die("Erro mysql: " . $e->getMessage());
 +
}
 +
catch (Expcetion $e) {
 +
     die("Erro xeral: " . $e->getMessage());
 +
}
 +
finally{
 +
  if(!empty($conex)) {
 +
    mysqli_close($conex);
 +
  }
 
}
 
}
 +
?>
 +
</syntaxhighlight>
 +
 +
:::* Versión orientada a obxectos
 +
::::<syntaxhighlight lang="php" enclose="div" highlight="21-25" >
 +
<?php
 +
require ('/var/www/confBD/bd.php');
 +
 +
try {
 +
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD);
 +
    $conex->set_charset('utf8mb4');
 +
?>
  
−
$cidade = $mysqli->real_escape_string($cidade);
+
  ..............
 +
  // Operacións contra a base de datos e amosar información ao usuario
 +
  ..............
  
−
/* esta consulta con $ciudad escapada funcionará */
+
<?php
−
if (!$mysqli->query("SELECT nome FROM CIDADES where nome_cidade = '$cidade'")) {
+
}    // Fin do try do principio da páxina
−
     printf("Fila inxerida.\n");
+
catch (mysqli_sql_exception $e) {
 +
    die("Erro mysql: " . $e->getMessage());
 +
}
 +
catch (Expcetion $e) {
 +
     die("Erro xeral: " . $e->getMessage());
 +
}
 +
finally{
 +
  if(!empty($conex)){
 +
    $conex->close();
 +
  }
 
}
 
}
 +
?>
  
 
</syntaxhighlight>
 
</syntaxhighlight>
−
::Exemplo obtido de php.net
 
  
  
−
* Pode suceder que un mesmo autor teña varios libros. Como imos a amosar os datos dentro dun grid no que na primeira columna vai ser o nome do autor e a segunda o nome do libro, vai repetirse o nome do autor como se amosa na seguinte imaxe:
 
  
−
[[Imagen:Php_BD_10.jpg|500px|center]]
+
<br />
  
−
:: Para evitalo, teremos que gardar en cada iteración unha vez 'impresa' a liña do autor, o nome do autor e comparalo cando carguemos a seguinte fila de datos. Se o nome do autor da nova liña de datos é igual ao anterior, entón non o debemos de amosar.
+
== Cambiando de base de datos activa ==
  
−
:: O código sería algo parecido a isto:
+
* Cando realizamos unha conexión con PDO podemos indicar no '''DSN''' a base de datos coa que queremos traballar:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
                    $autor='';  // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                    $last_autor=''; // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                    while($row = $result->fetch_assoc()) {    // PROCESAMOS AS FILAS DO RESULTADO DA CONSULTA
 
−
                        if ($last_autor==$row["nome_autor"]){  // Comprobamos se o novo autor é igual ao anterior procesado
 
−
                            $autor='';          // Se é igual entón quere dicir que estamos a ler unha fila co mesmo autor que a anterior. Polo tanto non 'imprimimos o autor'
 
−
                        }
 
−
                        else {
 
−
                            $autor=$row["nome_autor"];  // En caso contrario quere dicir que o autor actual é diferente ao anterior e polo tanto debemos sacalo por pantalla.
 
−
                        }
 
  
−
                        printf(AMOSAMOS OS DATOS DO LIBRO E DA VARIABLE $autor);
+
:<syntaxhighlight lang="php">
 +
$dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4'; </syntaxhighlight>
  
−
                        $last_autor=$row["nome_autor"];  // Gardamos o nome do autor para comparar có autor que veña na seguinte fila de datos.
+
: Neste caso, a base de datos seleccionada inicialmente será '''php'''.
−
                    }
 
  
−
</syntaxhighlight>
+
* Se durante a execución do programa necesitamos traballar con outra base de datos temos, entre outras, dúas posibilidades:
 +
  :* Crear unha nova conexión PDO indicando a nova base de datos no DSN.
 +
  :* Cambiar a base de datos activa da conexión actual mediante a sentenza SQL '''USE'''.
  
 +
=== Crear unha nova conexión PDO ===
  
 +
* Podemos crear un novo obxecto PDO indicando outra base de datos no DSN:
  
−
* Imos facer que cada libro sexa un enlace para abrir unha páxina nova de nome '''Detalle_Libro.php'''. No resultado da busca soamente imos amosar o nome do libro, de tal forma que cando o usuario preme sobre o enlace, apareza unha nova xanela cos datos completos dese libro, incluíndo os exemplares).
+
:<syntaxhighlight lang="php">
  
−
:: Para facelo imos facer uso de javascript e da [http://www.w3schools.com/jsref/met_win_open.asp función window.open].
+
<?php
−
:: Crearei unha función en javascript que espere recibir un parámetro, que sexa a url a abrir e chamarei a window.open cunha serie de modificacións, como o tamaño da xanela, que non poida modificar a url,...
 
−
:: O código será o seguinte:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4-8" >
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
        <script type="text/javascript">
 
−
            function abrirVentana($url){
 
−
                window.open($url,'_blank','channelmode=0,status=0;directories=0,location=0,menubar=0,titlebar=0,toolbar=0,width=600,height=400');
 
−
            }
 
−
        </script>
 
−
       
 
−
    </head>
 
−
</syntaxhighlight>
 
  
 +
try {
  
−
:: Para chamar a esta función usaremos a etiqueta <a> e o evento onclick, coma o seguinte exemplo:
+
    $opciones = [
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4-8" >
+
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
<a href='#' onclick="javascript:abrirVentana('Detalle_Libro.php?libro=1');">Título Libro</a>
+
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
</syntaxhighlight>
+
        PDO::ATTR_EMULATE_PREPARES  => false
 +
    ];
  
−
:: <u>Hai que ter moito coidado cando estamos a traballar con PHP-Javascript polo tema das comillas xa que teremos que 'xogar' coas simples e dobres para que se pechen correctamente</u>.
+
    // Conexión inicial á base de datos php
 +
    $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
  
 +
    $conex = new PDO(
 +
        $dsn,
 +
        'user_php',
 +
        'user_php',
 +
        $opciones
 +
    );
  
 +
    // Creamos unha nova conexión á base de datos outra_db
 +
    $dsn = 'mysql:host=localhost;dbname=outra_db;charset=utf8mb4';
  
−
* '''SOLUCIÓN'''
+
    $conex = new PDO(
 +
        $dsn,
 +
        'user_php',
 +
        'user_php',
 +
        $opciones
 +
    );
  
−
'''Arquivo: bd.inc.php'''
+
} catch (PDOException $e) {
  
−
<u>Nota:</u> Definimos a consulta a realizar á base de datos ('''lembrar que é mellor facer dita consulta directamente no Mysql e despois copiala ao código PHP pasando os parámetros correspondentes''')
+
    die('Erro relacionado coa base de datos: ' . $e->getMessage());
−
::<syntaxhighlight lang="java" enclose="div" highlight="15-19" >
 
−
<?php
 
  
−
define ('SERVIDOR','localhost');
+
} catch (Throwable $e) {
−
define ('USUARIO','user_php');
 
−
define ('PASSWORD','user_php');
 
−
define ('BD','PHP');
 
  
−
define ('CARTAFOL_IMAXES_WEB','http://www.meusitio.es/IMAXES/');
+
    die('Erro xeral: ' . $e->getMessage());
  
−
define ('CONSULTAR_LIBROS','select id_libro,titulo from LIBROS order by titulo');
+
}
−
define ('CONSULTAR_EXEMPLARES_POR_LIBRO','select libro_id,id_exemplar,imaxe,prezo,estado_id,ESTADOS.descripcion as desc_estado'
+
?>
−
        . ' from EXEMPLARES INNER JOIN LIBROS ON (LIBROS.id_libro=EXEMPLARES.libro_id)'
 
−
        .                ' INNER JOIN ESTADOS ON (ESTADOS.id_estado=EXEMPLARES.estado_id)'
 
−
        . ' where id_libro=%d order by titulo,desc_estado');
 
−
define ('CONSULTAR_LIBROS_POR_AUTOR',"select CONCAT(apelido,',',nome) as nome_autor,id_libro,titulo from LIBROS " .
 
−
            ' INNER JOIN LIBROS_AUTORES ON (LIBROS_AUTORES.libro_id=LIBROS.id_libro) ' .
 
−
            ' INNER JOIN AUTORES ON (LIBROS_AUTORES.autor_id=AUTORES.id_autor)' .
 
−
        " where CONCAT(AUTORES.nome,' ',AUTORES.apelido) like '%%%s%%' " .
 
−
        'order by nome_autor,titulo');
 
  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: <u>Nota:</u> Fixarse como o parámetro que recibe 'like' é de tipo cadea e polo tanto vai entre comillas.
+
: Ao crear o segundo obxecto PDO estamos realizando unha nova conexión na que a base de datos seleccionada inicialmente será '''outra_db'''.
 +
 
 +
=== Cambiar a base de datos activa con USE ===
 +
 
 +
* Se queremos manter a mesma conexión e cambiar a base de datos activa podemos executar a sentenza SQL '''USE''':
  
 +
:<syntaxhighlight lang="php">
  
−
'''Arquivo Buscar_Libros_Por_Autor.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
<?php
 
<?php
  
−
require ('/var/www/confBD/bd.inc.php');
+
try {
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
    $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
−
$conex->set_charset('utf8');
 
  
−
if ($conex->connect_error){
+
    $opciones = [
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
+
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
}
+
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
$erro="";   // Variable que a usaremos para amosar os posibles erros que poidan darse
+
        PDO::ATTR_EMULATE_PREPARES  => false
−
?>
+
    ];
  
 +
    $conex = new PDO(
 +
        $dsn,
 +
        'user_php',
 +
        'user_php',
 +
        $opciones
 +
    );
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
     // A base de datos activa inicialmente é php
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
     <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
        <script type="text/javascript">
 
−
            function abrirVentana($url){
 
−
                window.open($url,'_blank','channelmode=0,status=0;directories=0,location=0,menubar=0,titlebar=0,toolbar=0,width=600,height=400');
 
−
            }
 
−
        </script>
 
−
       
 
−
    </head>
 
  
−
     <body>
+
     $conex->exec('USE outra_db');
−
        <form action="<?php echo $_SERVER['PHP_SELF'] ?>" method='post'>
 
−
            Introduce o nome do autor a buscar:
 
−
            <input type='text' size='50' maxlength="100" name="txtAutor" value='<?php echo (isset($_POST['txtAutor']) ? $_POST["txtAutor"]:""); ?>'/>
 
−
            <input type="submit" value="BUSCAR" />
 
−
            <input type='button' onclick="javascript:this.form['txtAutor'].value='';" value="LIMPAR" />
 
−
           
 
−
           
 
−
        <?php       
 
−
        if (!empty($_POST['txtAutor'])){  // Premimos o botón de busca
 
−
            $textoBuscar = filter_var($_POST['txtAutor'],FILTER_SANITIZE_STRING);
 
−
            if (strlen($textoBuscar)>100){
 
−
                $erro="<h2>A cadea a buscar non pode ser maior de 100 caracteres</h2>";
 
−
            }
 
−
            else {
 
−
                $query=sprintf(CONSULTAR_LIBROS_POR_AUTOR,$textoBuscar);
 
−
                $result=$conex->query($query);
 
−
                if ($result->num_rows>0){
 
−
                    $autor='';  // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                    $last_autor=''; // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                    echo "<table border='1'>";
 
−
                    echo "<th>Autor</th><th>Libro</th>";
 
−
                    while($row = $result->fetch_assoc()) {
 
−
                        if ($last_autor==$row["nome_autor"]){
 
−
                            $autor=$row["nome_autor"];
 
−
                        }
 
−
                        else {
 
−
                            $autor=$row["nome_autor"];
 
−
                        }
 
−
                        echo '<tr>';
 
−
                        $onclick=sprintf("javascript:abrirVentana(\"Detalle_Libro.php?libro=%d\");",$row["id_libro"]);
 
−
                        printf("<td>%s</td><td><a href='#' onclick='%s'>%s</a></td>",$autor,$onclick,$row['titulo']);
 
−
                        echo '</tr>';
 
−
                        $last_autor=$row["nome_autor"];
 
−
                    }
 
−
                    $result->free();
 
−
                    echo "</table>";
 
−
                }
 
−
                else {
 
−
                    echo "<h2><b>Non hai resultados</b></h2>";
 
−
                }
 
−
               
 
−
               
 
−
            }
 
−
        }
 
−
       
 
−
        ?>       
 
−
        </form>
 
−
       
 
−
       
 
−
        <?php       
 
−
        if ($erro!='') {
 
−
            printf("<h3>%s</h3>",$erro);
 
−
        }
 
−
        $conex->close();  // Pechamos a conexión
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
−
</syntaxhighlight>
+
    // A partir deste momento a base de datos activa é outra_db
  
−
* Fixarse que nesta páxina non necesitamos conectar ao principio, xa que soamente se carga a caixa de texto para que o usuario introduza o nome do autor a buscar.
+
} catch (PDOException $e) {
−
:: Polo tanto a conexión e o peche da base de datos se podería poñer dentro do if da liña 39.
 
−
* Fixarse que o botón de limpar non é de tipo 'reset' xa que ao cargar o que veña en $_POST o botón tipo reset volvería a amosar dito valor.
 
  
 +
    die('Erro relacionado coa base de datos: ' . $e->getMessage());
  
−
* '''Resultado:'''
+
} catch (Throwable $e) {
−
[[Imagen:Php_BD_11.jpg|500px|center]]
 
  
−
=== Consulta chamando a procedementos almacenados ===
+
    die('Erro xeral: ' . $e->getMessage());
  
−
* Un procedemento almacenado ven a ser o equivalente a unha función en PHP, coa diferenza que as ordes que conforman o procedemento van permitir realizar accións (inserción, borrado, modificación e selección) sobre a base de datos.
+
}
 +
?>
  
−
* Como comentamos [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Soluci.C3.B3n_a_inxecci.C3.B3n_de_CSS anteriormente nesta wiki], unha das mellores formas de evitar ataques de tipo 'inxección de CSS' é utilizar procedementos almacenados.
+
</syntaxhighlight>
  
−
: Tamén é unha forma de aumentar a seguridade de acceso á base de datos Mysql, xa que teremos que dar permiso de execución sobre o procedemento almacenado e o usuario non vai poder acceder ás táboas da base de datos nin examinar a estrutura das mesmas.
+
: A sentenza '''USE''' cambia a base de datos predeterminada da conexión actual. Esta seguirá sendo a base de datos activa ata que se execute outra sentenza '''USE''' ou se peche a conexión.
−
: Desta forma podemos cambiar a estrutura da táboa (como por exemplo cambiar o nome dunha columna) e simplemente cambiando a sentenza SQL que está dentro do procedemento almacenado e poñendo un alias có nome antigo, a aplicación no cliente segue funcionando.
 
−
: Outra vantaxe é que as sentencias SQL non están definidas nas páxinas PHP e polo tanto se realizamos un cambio o faremos no Mysql sendo 'transparente' cara ao usuario.
 
  
 +
: O usuario empregado para realizar a conexión deberá ter os permisos necesarios sobre a nova base de datos.
  
−
* Máis información [http://php.net/manual/es/mysqli.quickstart.stored-procedures.php neste enlace].
+
* Podemos comprobar cal é a base de datos activa empregando:
  
−
* O primeiro que temos que facer é crear o procedemento almacenado.
+
:<syntaxhighlight lang="php">
−
* A orde SQL é [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html '''CREATE PROCEDURE'''].
+
$stmt = $conex->query('SELECT DATABASE()');
  
−
* Creamos o procedemento almacenado:
+
echo $stmt->fetchColumn(); </syntaxhighlight>
−
:* Cando definimos un procedemento almacenado debemos facer uso da palabra DELIMITER para indicarlle ao Mysql onde acaba a definición do procedemento almacenado. Máis información [http://dev.mysql.com/doc/refman/5.7/en/stored-programs-defining.html neste enlace].
 
  
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
+
: Neste caso, despois de executar:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `NOME_PROCEDEMENTO` ()
+
:<syntaxhighlight lang="php">
−
BEGIN
+
$conex->exec('USE outra_db'); </syntaxhighlight>
  
−
  select col1,col2
+
: o código anterior devolvería:
−
  from TABOA_1
 
−
  order by col1;
 
  
 +
:<syntaxhighlight lang="text">
 +
outra_db </syntaxhighlight>
  
−
END
+
'''IMPORTANTE:''' O nome da base de datos empregado na sentenza `USE` non debería obterse directamente de datos introducidos polo usuario. Neste módulo empregaremos nomes de bases de datos definidos previamente no código.
−
$$
 
−
</syntaxhighlight>
 
  
−
: Como vemos o procedemento está formado por unha consulta SELECT (pode poñerse calquera)
+
<br />
  
 +
== Optimización e seguridade: Sentenzas preparadas ==
  
−
* Agora dende PHP temos que facer uso do comando CALL 'NOME_PROCEDEMENTO'.
+
* '''NOTA:''' A explicación de como se fan consultas ven no seguinte punto, pero adianto agora a explicación de como en PDO se empregan sentenzas prepared.
−
: Isto é o que teriamos que poñer se dende Mysql queredes chamar ao procedemento....
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="10" >
 
  
−
        $conex = new mysqli('localhost','user_php','user_php','PHP');
 
−
        $conex->set_charset('utf8');
 
−
       
 
−
        if (!$conex->select_db('OutraBD')){
 
−
            die('Erro ao cambiar de base de datos');
 
−
        }
 
  
 +
* Unha '''sentenza preparada''' permite separar a instrución SQL dos datos que se van empregar nela.
  
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
+
: Primeiro prepárase a sentenza SQL indicando mediante '''marcadores''' os valores que se proporcionarán posteriormente. Despois asócianse os datos a eses marcadores e execútase a sentenza.
−
</syntaxhighlight>
 
  
 +
: Por exemplo:
  
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
SELECT col1
 +
FROM TABOA1
 +
WHERE col2 = ? </syntaxhighlight>
  
−
* A diferenza do caso anterior, cando chamemos a un procedemento almacenado teremos que facer uso dunha das seguintes funcións:
+
: Neste caso a sentenza ten un parámetro, representado polo carácter '''?'''. O valor correspondente proporcionarase posteriormente no momento de executar a sentenza.
−
:: [http://php.net/manual/es/mysqli.real-query.php mysqli::real_query] ou [http://php.net/manual/es/mysqli.multi-query.php mysqli::multi_query]
 
  
−
: Nos imos facer uso da chamada a '''multi_query''' xa que tamén a imos poder utilizar cando dentro dun mesmo procedemento almacenado teñamos varios select´s e queiramos recuperar o contido de todos eles (cada un será un conxunto de resultados).
+
: Unha mesma sentenza preparada pode executarse varias veces empregando valores diferentes.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="3,4" >
 
−
        ........................
 
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
        }
 
  
−
</syntaxhighlight>
+
* PDO permite utilizar dous tipos de marcadores:
  
 +
:* '''Marcadores posicionais''', representados mediante `?`:
  
−
: Unha vez temos o conxunto de resultados ($result) xa operamos como nos casos anteriores.
+
::<syntaxhighlight lang="sql" enclose="div">
 +
SELECT nome, apelido
 +
FROM alumnos
 +
WHERE idade > ? </syntaxhighlight>
  
 +
:* '''Marcadores con nome''', precedidos por `:`:
  
−
* Como comentamos anteriormente, un procedemento pode estar conformado por múltiples 'select'.
+
::<syntaxhighlight lang="sql" enclose="div">
−
: Se queremos 'pasar' ao seguinte 'conxunto de resultados' temos que chamar á [http://php.net/manual/es/mysqli.next-result.php función mysqli::next_result] desta forma:
+
SELECT nome, apelido
 +
FROM alumnos
 +
WHERE idade > :idade </syntaxhighlight>
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="8-10" >
+
: Nos exemplos dos apuntes empregaremos preferentemente '''marcadores con nome''', xa que normalmente fan que o código sexa máis lexible.
−
        ........................
 
−
        $proc = sprintf("CALL %s",NOME_PROCEDEMENTO);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
            if ($conex->next_result()){
 
−
              $result=$conex->store_result();
 
−
            }
 
  
−
        }
+
=== Vantaxes das sentenzas preparadas ===
  
−
</syntaxhighlight>
+
* As sentenzas preparadas presentan, entre outras, as seguintes vantaxes:
  
 +
:* '''Rendemento''': cando unha mesma sentenza se executa repetidamente con distintos valores, a base de datos pode reutilizar parte do traballo realizado durante a preparación da sentenza, como a análise e optimización da consulta. Polo tanto, poden resultar especialmente útiles cando unha mesma operación se executa moitas veces.
  
−
==== Caso Práctico: Consulta chamando a procedementos almacenados ====
+
:* '''Seguridade''': os datos enviados mediante parámetros son tratados como valores e non como parte da instrución SQL. Isto axuda a evitar ataques de '''inxección SQL'''.
  
−
* Vexamos un exemplo. Imos copiar o código da páxina '''Consultar_Libros.php''' e imos crear unha nova de nome '''Consultar_Libros_Proc''' co mesmo código, facendo as modificacións que se indican a continuación para que faga uso da chamada a un procedemento almacenado.
+
: '''IMPORTANTE:''' As sentenzas preparadas protexen os '''valores enviados mediante parámetros''', pero isto non significa que calquera consulta sexa automaticamente segura.
  
 +
: Por exemplo, non debemos construír unha consulta concatenando directamente datos recibidos do usuario:
  
−
* Creamos o procedemento almacenado:
+
::<syntaxhighlight lang="php" enclose="div">
−
'''Creamos un procedemento almacenado dentro do MysqlWorkBench'''
+
$sql = "SELECT * FROM alumnos WHERE nome = '" . $_POST['nome'] . "'"; </syntaxhighlight>
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS` ()
+
: A forma correcta será empregar un parámetro:
−
BEGIN
 
  
−
  select id_libro,titulo
+
::<syntaxhighlight lang="php" enclose="div">
−
  from LIBROS
+
$sql = 'SELECT * FROM alumnos WHERE nome = :nome';
−
  order by titulo;
 
  
 +
$stmt = $conex->prepare($sql);
  
−
END
+
$stmt->execute([
−
$$
+
'nome' => $_POST['nome']
−
</syntaxhighlight>  
+
]); </syntaxhighlight>
  
 +
: Os parámetros dunha sentenza preparada representan '''valores'''. Non poden utilizarse directamente para substituír nomes de táboas, nomes de columnas, palabras reservadas SQL ou outras partes estruturais dunha consulta.
  
 +
: Máis información:
 +
: [https://www.php.net/manual/es/pdo.prepared-statements.php Sentenzas preparadas con PDO]
  
−
* Definimos nunha constante o nome do procedemento almacenado dentro do arquivo bd.inc.php.
+
=== PHP empregando PDO ===
  
−
:'''Arquivo: bd.inc.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.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_LIBROS','CONSULTAR_LIBROS()');  // Se levara parámetros faríamos o mesmo que nos casos anteriores coas sentencias select...
 
−
</syntaxhighlight>
 
  
 +
* Para preparar unha sentenza empregamos o método:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
PDO::prepare() </syntaxhighlight>
  
−
* Debemos cambiar a chamada a 'query' pola chamada [http://php.net/manual/es/mysqli.real-query.php mysqli::real_query] ou [http://php.net/manual/es/mysqli.multi-query.php mysqli::multi_query]
+
* Este método devolve un obxecto da clase `PDOStatement`, sobre o que posteriormente chamaremos ao método:
−
:: Nos imos facer uso da chamada a '''multi_query''' xa que tamén a imos poder utilizar cando dentro dun mesmo procedemento almacenado teñamos varios select´s e queiramos recuperar o contido de todos eles (cada un será un conxunto de resultados).
 
  
−
:'''Arquivo: Consultar_Libros_Proc.php'''
+
::<syntaxhighlight lang="php" enclose="div">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4-6" >
+
PDOStatement::execute() </syntaxhighlight>
−
    <body>
 
−
        <form action="<?php echo $_SERVER['PHP_SELF'] ?>" method='post'>
 
−
        <?php       
 
−
        $proc = sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_LIBROS);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                echo "<select name='lstLibros' onchange='this.form.submit()'>";
 
−
                echo "<option value='-1'></option>";
 
−
                while($row = $result->fetch_assoc()) {
 
−
                    if(isset($_POST['lstLibros']) && $row['id_libro']==$_POST['lstLibros']){
 
−
                        $cadea="selected='selected'";
 
−
                    }
 
−
                    else{
 
−
                        $cadea='';
 
−
                    }
 
−
                    printf("<option value='%d' %s>%s</option>",$row["id_libro"],$cadea,$row["titulo"]);
 
−
                }
 
−
                echo '</select>';
 
−
                $result->free();  // Liberamos da memoria os recursos
 
−
            }
 
−
            else {
 
−
                echo "Non hai datos que amosar...";
 
−
            }
 
−
               
 
−
        }
 
−
        else {
 
−
            echo "Houbo un erro na chamado ao procedemento:" . $conex->error; // A mensaxe do erro ($conex->error) non a teremos que amosar coa aplicación funcionando
 
−
        }
 
−
        ?>       
 
−
        </form>
 
  
 +
: Máis información:
 +
:* [https://www.php.net/manual/es/pdo.prepare.php PDO::prepare()]
 +
:* [https://www.php.net/manual/es/pdostatement.execute.php PDOStatement::execute()]
  
−
</syntaxhighlight>
+
==== Exemplo ====
  
−
:* Liña 4: Creamos a cadea que vai chamar ao procedemento almacenado: CALL CONSULTAR_LIBROS()
+
* Supoñamos que queremos inserir datos na táboa `alumnos`:
−
:* Liña 5: Modificamos a liña e agora chamamos ao procedemento mysqli::multi_query(nome_proc). Dita chamada devolve false en caso de erro.
 
−
:* Liña 6: Gardamos o resultado (o primeiro select do procedemento almacenado) no obxecto $result e xa operamos igual que no caso anterior cando usamos as sentencias 'select' directamente.
 
  
−
=== Consulta chamando a procedementos almacenados con parámetros de entrada ===
+
::<syntaxhighlight lang="sql" enclose="div">
 +
INSERT INTO alumnos (nome, apelido)
 +
VALUES (:nome, :apelido) </syntaxhighlight>
  
−
* Podemos chamar a un procedemento almacenado enviando datos ao mesmo en forma de parámetros.
+
* O código PHP podería ser o seguinte:
−
: Indicar que en case todos os xestores os parámetros poden ser de 3 tipos: '''de entrada''', '''de saída''' e de '''entrada/saída'''.
 
−
::* Os <u>parámetros de saída</u> (OUT) son variables que van poder ser modificadas no interior do procedemento e cuxo valor vai poder ser lido dentro do código da páxina php.
 
−
::* Os <u>parámetros de entrada</u> (IN) son variables que van poder ser utilizadas dentro do procedemento (chegan en forma de parámetros) pero as modificacións que se fagan no seu valor non chegan á paxina PHP.
 
−
::* Os <u>parámetros de entrada/entrada</u> (INOUT) son unha combinación dos dous anteriores.
 
  
−
: Máis información [http://www.mysqltutorial.org/stored-procedures-parameters.aspx neste enlace].
+
::<syntaxhighlight lang="php" line enclose="div">
  
−
* Por defecto, se non especificamos nada, todos os parámetros dun procedemento almacenado son considerados 'de entrada'
+
<?php
  
−
* Primeiro teremos que definir o procedemento en Mysql:
+
$conex = null;
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100))
+
try {
−
BEGIN
 
  
−
     SELECT col1,col2
+
     $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
−
    FROM TABOA_1
 
−
    WHERE col2=_param1;
 
  
−
END
+
    $opciones = [
−
</syntaxhighlight>  
+
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
 +
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
        PDO::ATTR_EMULATE_PREPARES  => false
 +
    ];
  
−
: Como vemos, xunto ao nome do procedemento hai que especificar o tipo (dentro dos tipos de mysql).
+
    $conex = new PDO(
−
: Unha vez definidos podemos utilizalos dentro do procedemento almacenado.
+
        $dsn,
−
: Calquera modificación do valor do parámetro non se verá reflectidos no código PHP. Se no código PHP 'recuperamos' o valor do parámetro, este terá o mesmo valor que cando enviamos o dato ao procedemento.
+
        'user_php',
 +
        'user_php',
 +
        $opciones
 +
    );
  
 +
    // Preparamos a sentenza
 +
    $sql = 'INSERT INTO alumnos (nome, apelido)
 +
            VALUES (:nome, :apelido)';
  
−
: Podemos definir máis parámetros, separando por comas cada un deles, por exemplo:
+
    $stmt = $conex->prepare($sql);
−
::<syntaxhighlight lang="java" line enclose="div" highlight="3,8,9" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(IN _param1 varchar(100),_param2 int)
+
    // Indicamos os datos e executamos a sentenza
−
BEGIN
+
    $nome = 'Ana';
 +
    $apelido = 'López';
  
−
     SELECT col1,col2
+
     $stmt->execute([
−
    FROM TABOA_1
+
        'nome'    => $nome,
−
     WHERE col2=_param1 and
+
        'apelido' => $apelido
−
          col1=_param2
+
     ]);
  
−
END
+
} catch (PDOException $e) {
−
</syntaxhighlight>
 
  
 +
    // Durante o desenvolvemento podemos amosar a mensaxe de erro.
 +
    // Nunha aplicación en produción non se debería mostrar ao usuario.
 +
    printf('Erro PDO: %s', $e->getMessage());
  
−
* Unha  vez definido teremos que enviar os datos que queiramos dende PHP desta forma:
+
} catch (Throwable $e) {
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
 
−
</syntaxhighlight>
 
−
: Fixarse como no caso de datos de tipo numérico non enviamos o dato con comiñas.
 
−
: Fixarse como 'xogamos' coas comiñas simples de dobres para pechar correctamente a cadea.
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="5,6" >
+
    printf('Erro xeral: %s', $e->getMessage());
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS(%d,"%s")');
 
  
−
        ........................
+
} finally {
  
−
        $proc = sprintf(CHAMAR_PROCEDEMENTO_CON_PARAMETROS,valor_numérico,'valor_cadea');
+
    $conex = null;
−
        $proc = sprintf("CALL %s",$proc);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                ................
 
−
            }
 
−
        }
 
  
−
</syntaxhighlight>  
+
}
 +
?>
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
+
</syntaxhighlight>
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
  
 +
==== Executar varias veces a mesma sentenza ====
  
−
==== Caso Práctico: Consulta chamando a procedementos almacenados con parámetros de entrada ====
+
* Unha das vantaxes das sentenzas preparadas é que podemos preparar a instrución unha vez e executala varias veces con valores diferentes.
  
−
* Seguindo có noso exemplo...imos crear unha nova páxina de nome '''Buscar_Libros_Por_Autor_Proc''' que faga uso de procedementos almacenados en vez de sentencias SQL directamente no código.
+
::<syntaxhighlight lang="php" enclose="div">
 +
$sql = 'INSERT INTO alumnos (nome, apelido)
 +
VALUES (:nome, :apelido)';
  
−
'''Solución:'''
+
$stmt = $conex->prepare($sql);
−
* '''Procedemento almacenado CONSULTAR_LIBROS_POR_AUTOR en Mysql'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `CONSULTAR_LIBROS_POR_AUTOR`(IN _autor_buscar varchar(100))
+
// Primeiro alumno
−
BEGIN
+
$stmt->execute([
 +
'nome'    => 'Ana',
 +
'apelido' => 'López'
 +
]);
  
−
select CONCAT(apelido,',',nome) as nome_autor,id_libro,titulo from LIBROS
+
// Segundo alumno
−
            INNER JOIN LIBROS_AUTORES ON (LIBROS_AUTORES.libro_id=LIBROS.id_libro)
+
$stmt->execute([
−
            INNER JOIN AUTORES ON (LIBROS_AUTORES.autor_id=AUTORES.id_autor)
+
'nome'   => 'Brais',
−
where CONCAT(AUTORES.nome,' ',AUTORES.apelido) like  CONCAT('%', _autor_buscar , '%')
+
'apelido' => 'Pérez'
−
order by nome_autor,titulo;
+
]);
  
−
END
+
// Terceiro alumno
−
</syntaxhighlight>  
+
$stmt->execute([
 +
'nome'    => 'Lucía',
 +
'apelido' => 'García'
 +
]);
 +
</syntaxhighlight>
  
−
: <u>Nota:</u> Para comprobar se funciona facede primeiro unha chamada dende o propio Mysql coa orde: call CONSULTAR_LIBROS_POR_AUTOR('dru');
+
: A sentenza SQL prepárase unha vez, pero execútase tres veces con valores diferentes.
−
: Fixarse como o parámetro é de tipo IN (valor por defecto, podería non poñerse nada).  
 
  
  
 +
* '''Exercicio proposto:''' Intenta crear unha páxina PHP que engada á táboa 'autores' os seguintes datos:
 +
:* Benito Pérez Galdós
 +
:* Julio Verne
  
−
* Definimos a constante en PHP no que está definido o nome do procedemento e o parámetro que vai levar.
 
−
:'''Arquivo: bd.inc.php'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_LIBROS_POR_AUTOR','CONSULTAR_LIBROS_POR_AUTOR("%s")');
 
−
</syntaxhighlight>
 
  
−
:<u>Nota:</u> Fixarse como o parámetro ao ser de tipo 'cadea' debe ir entre comillas.
 
  
  
 +
<br />
 +
: '''Solución exercicio:'''
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
ini_set('display_errors', 1);
 +
ini_set('display_startup_errors', 1);
 +
error_reporting(E_ALL);
  
−
* '''Arquivo Buscar_Libros_Por_Autor_Proc'''
+
require ('/var/www/confBD/bd.php');
−
::<syntaxhighlight lang="java" line enclose="div" highlight="45-48" >
 
−
<?php
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
try {
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
    $dsn = sprintf(
−
$conex->set_charset('utf8');
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
 +
        SERVIDOR,
 +
        BD
 +
    );
  
−
if ($conex->connect_error){
+
    $opciones = [
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
+
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
}
+
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
$erro="";   // Variable que a usaremos para amosar os posibles erros que poidan darse
+
        PDO::ATTR_EMULATE_PREPARES  => false
−
?>
+
    ];
  
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        $opciones
 +
    );
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
     echo '<br />Conecta';
−
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
        <script type="text/javascript">
 
−
            function abrirVentana($url){
 
−
                window.open($url,'_blank','channelmode=0,status=0;directories=0,location=0,menubar=0,titlebar=0,toolbar=0,width=600,height=400');
 
−
            }
 
−
        </script>
 
−
       
 
−
    </head>
 
  
−
     <body>
+
     $sql = 'INSERT INTO autores (nome, apelidos)
−
        <form action="<?php echo $_SERVER['PHP_SELF'] ?>" method='post'>
+
             VALUES (:nome, :apelidos)';
−
            Introduce o nome do autor a buscar:
 
−
            <input type='text' size='50' maxlength="100" name="txtAutor" value='<?php echo (isset($_POST['txtAutor']) ? $_POST["txtAutor"]:""); ?>'/>
 
−
            <input type="submit" value="BUSCAR" />
 
−
            <input type='button' onclick="javascript:this.form['txtAutor'].value='';" value="LIMPAR" />
 
−
           
 
−
           
 
−
        <?php       
 
−
        if (!empty($_POST['txtAutor'])){  // Premimos o botón de busca
 
−
            $textoBuscar = filter_var($_POST['txtAutor'],FILTER_SANITIZE_STRING);
 
−
             if (strlen($textoBuscar)>100){
 
−
                $erro="<h2>A cadea a buscar non pode ser maior de 100 caracteres</h2>";
 
−
            }
 
−
            else {
 
−
                $proc = sprintf(PROCEDEMENTO_CONSULTAR_LIBROS_POR_AUTOR,$textoBuscar); // Cambiamos o texto a buscar polo parámetro definido na constante
 
−
                $proc = sprintf("CALL %s",$proc);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();
 
−
                    if ($result->num_rows>0){
 
−
                        $autor='';  // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                        $last_autor=''; // A utilizamos para que non se repita o autor na táboa cando hai varios libros do mesmo autor
 
−
                        echo "<table border='1'>";
 
−
                        echo "<th>Autor</th><th>Libro</th>";
 
−
                        while($row = $result->fetch_assoc()) {
 
−
                            if ($last_autor==$row["nome_autor"]){
 
−
                                $autor='';
 
−
                            }
 
−
                            else {
 
−
                                $autor=$row["nome_autor"];
 
−
                            }
 
−
                            echo '<tr>';
 
−
                            $onclick=sprintf("javascript:abrirVentana(\"Detalle_Libro.php?libro=%d\");",$row["id_libro"]);
 
−
                            printf("<td>%s</td><td><a href='#' onclick='%s'>%s</a></td>",$autor,$onclick,$row['titulo']);
 
−
                            echo '</tr>';
 
−
                            $last_autor=$row["nome_autor"];
 
−
                        }
 
−
                        $result->free();
 
−
                        echo "</table>";
 
−
                    }
 
−
                    else {
 
−
                        echo "<h2><b>Non hai resultados</b></h2>";
 
−
                    }
 
−
                }
 
−
                else{
 
−
                    echo "Houbo un erro na chamado ao procedemento:" . $conex->error; // A mensaxe do erro non a teremos que amosar coa aplicación funcionando
 
−
                }
 
−
               
 
−
            }
 
−
        }
 
−
       
 
−
        ?>       
 
−
        </form>
 
−
       
 
−
       
 
−
        <?php       
 
−
        if ($erro!='') {
 
−
            printf("<h3>%s</h3>",$erro);
 
−
        }
 
−
        $conex->close();  // Pechamos a conexión
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
−
</syntaxhighlight>
+
    $stmt = $conex->prepare($sql);
 +
    $stmt->execute(['nome'  => 'Benito', 'apelidos' => 'Pérez Galdós']);
 +
    $stmt->execute(['nome'  => 'Julio', 'apelidos' => 'Verne']);
  
−
:* Liñas 45 e 46: Creamos a cadea que vai chamar ao procedemento almacenado de Mysql: CALL CONSULTAR_LIBROS_POR_AUTOR('dru');
+
    echo '<br />Datos engadidos';
  
−
=== Consulta chamando a procedementos almacenados con parámetros de saída ===
+
} catch (PDOException $e) {
 +
    die("Error PDO: " . $e->getMessage());
 +
} catch (Throwable $e) {
 +
    die("Error general: " . $e->getMessage());
 +
}
  
−
* Agora imos aplicar o concepto de parámetros de saída, que como indicamos no punto anterior, son parámetros que van poder ser modificados o seu valor dentro do procedemento almacenado e recoller o seu valor modificado dende a parte de PHP.
+
$conex = null;  // Poderíamos non poñer nada
  
−
* Primeiro teremos que definir o procedemento en Mysql:
+
</syntaxhighlight>
−
::<syntaxhighlight lang="java" line enclose="div" highlight="3,7" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_PARAMETROS`(OUT _param1 varchar(100))
 
−
BEGIN
 
  
−
    SELECT col2
 
−
    INTO _param1
 
−
    FROM TABOA_1
 
−
    WHERE col1=1;
 
  
−
END
+
<br />
−
</syntaxhighlight>  
+
==== Asociar explicitamente os parámetros ====
  
−
: Agora especificamos que o parámetro é de saída antepoñendo a palabra OUT ao seu nomo.
+
* Tamén podemos asociar os valores aos parámetros antes de executar a sentenza empregando, por exemplo, o método `bindValue()`.
−
: Neste exemplo estamos a supoñer que a consulta devolve unha única fila e estamos gardando o resultado da consulta no parámetro de saída.
 
−
: Podemos modificar o seu valor dentro do procedemento coa orde: SET _param1=valor;
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$sql = 'SELECT nome, apelido
 +
FROM alumnos
 +
WHERE idade >= :idade';
  
 +
$stmt = $conex->prepare($sql);
  
−
* Unha  vez definido teremos que construír a chamada ao procedemento enviando o nome dunha variable (para recoller o valor modificado).
+
$stmt->bindValue(':idade', 18, PDO::PARAM_INT);
−
: O nome da variable terá este formato: '''@'''nomeVariable:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS("@nomeVariable")');
 
−
</syntaxhighlight>
 
−
: Fixarse como a variable é de tipo cadea e vai entre comiñas.
 
  
 +
$stmt->execute(); </syntaxhighlight>
  
−
* Agora a chamada se fai igual que no caso anterior, pero fixarse que neste caso O PROCEDEMENTO NON DEVOLVE UN CONXUNTO DE RESULTADOS. Podería ser e nese caso obraríamos igual que no caso anterior, a maiores do que imos ver agora.
+
: Neste exemplo indicamos explicitamente que o valor asociado ao parámetro `:idade` é de tipo enteiro mediante:
−
: Para recoller o dato da variable teremos que facer unha consulta SELECT da forma: SELECT @nomeVariable as valorDevolto
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="10,14" >
+
::<syntaxhighlight lang="php" enclose="div">
−
<?php
+
PDO::PARAM_INT </syntaxhighlight>
−
        define('CHAMAR_PROCEDEMENTO_CON_PARAMETROS','PROCEDEMENTO_CON_PARAMETROS("@nomeVariable")');
 
  
−
        ........................
+
: Para datos de tipo cadea podemos empregar:
  
−
        $proc = sprintf("CALL %s",CHAMAR_PROCEDEMENTO_CON_PARAMETROS);
+
::<syntaxhighlight lang="php" enclose="div">
−
        if ($conex->multi_query($proc)){
+
PDO::PARAM_STR </syntaxhighlight>
−
            $result = $conex->store_result();
 
  
−
            if (!($resultado = $conex->query("SELECT @nomeVariable as valorDevolto"))) {
+
: Nos exemplos máis sinxelos tamén podemos enviar directamente os valores ao método `execute()`:
−
                echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
−
            else {
 
−
                $fila = $resultado->fetch_assoc();
 
−
                echo $fila['valorDevolto']);
 
−
            }
 
  
−
        }
+
::<syntaxhighlight lang="php" enclose="div">
−
        ........................
+
$stmt->execute([
 +
'idade' => 18
 +
]); </syntaxhighlight>
  
  
−
?>
+
* '''IMPORTANTE:''' Se enviamos os parámetros directamente mediante o array de execute(), PDO trátaos por defecto como cadeas (PDO::PARAM_STR). O xestor de base de datos poderá realizar as conversións de tipo necesarias segundo o contexto da consulta e o tipo das columnas.
 +
: Se queremos indicar explicitamente o tipo dun parámetro, podemos empregar bindValue() ou bindParam() xunto cunha constante como PDO::PARAM_INT ou PDO::PARAM_STR.
 +
: Pode darse o caso de que teñamos unha columna na base de datos definida como numérica o o valor enviado sexa cadea e o interprete de forma erronéa (por exemplo con Limit de SQL que espara un número)
  
−
</syntaxhighlight>
 
  
−
: Fixarse que ao final o que temos que conseguir é esta cadea: CALL PROCEDEMENTO_CON_PARAMETROS(5,'valor')  // 5 e valor son datos de exemplo
 
−
: Isto o podemos conseguir de diferentes formas. Eu vos amoso unha delas.
 
  
  
  
  
−
==== Caso práctico: Consulta chamando a procedementos almacenados con parámetros de saída ====
 
  
−
* Segundo o noso exemplo, imos crear un procedemento almacenado que devolva nun parámetro de saída o número de libros que temos na nosa tenda.
+
<br />
 +
* '''Exercicio Proposto:''' Empregando esta forma de paso de parámetros (indicando o tipo) agrega dos novos libros tendo en conta que a columna id é autonumérica e non tes que enviar o dato.
  
  
−
* '''Procedemento almacenado CONSULTAR_NUM_LIBROS en Mysql'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_NUM_LIBROS`(OUT _num_libros int)
 
−
BEGIN
 
−
SELECT COUNT(*)
 
−
INTO _num_libros
 
−
FROM LIBROS;
 
  
−
END
 
−
$$
 
−
</syntaxhighlight>
 
  
  
−
: Como se pode ver leva un parámetro de saída (OUT). Indicar que podemos poñer calquera combinación de parámetros na definición dun procedemento almacenado.
+
<br />
  
 +
=== MySQL ===
  
−
* '''Arquivo bd.inc.php''':
+
'''NOTA:''' O seguinte apartado explica como crear sentenzas preparadas directamente desde MySQL. Inclúese a nivel informativo. Nas operacións realizadas desde PHP nos exames empregaremos '''PDO'''.
−
* Definimos a chamada ao procedemento cunha variable no seu interior que vai ser a que garde o valor modificado de dentro do procedemento.
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('PROCEDEMENTO_CONSULTAR_NUM_LIBROS','CONSULTAR_NUM_LIBROS(@numero_libros)');
 
−
</syntaxhighlight>
 
  
 +
: Máis información:
 +
: [https://dev.mysql.com/doc/refman/8.4/en/sql-prepared-statements.html Prepared Statements en MySQL]
  
 +
* Desde o propio xestor MySQL tamén podemos crear e executar sentenzas preparadas mediante:
  
−
* '''Arquivo Consultar_Num_Libros.php'''
+
:* `PREPARE`
−
: Esta páxina está a modo de exemplo. Loxicamente isto podería ser utilizado dentro dunha páxina para obter o número de libros sen necesidade de crear unha páxina específica.
+
:* `EXECUTE`
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
:* `DEALLOCATE PREPARE`
−
<?php
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
* Esta funcionalidade pode resultar útil, por exemplo, para construír SQL dinámico dentro dun '''procedemento almacenado'''.
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
: As sentenzas `PREPARE`, `EXECUTE` e `DEALLOCATE PREPARE` poden empregarse dentro de procedementos almacenados, pero non dentro de funcións almacenadas ou triggers.
−
$conex->set_charset('utf8');
 
  
−
if ($conex->connect_error){
+
==== Exemplo de SQL dinámico ====
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
  
−
?>
+
* Supoñamos que queremos buscar autores empregando dous criterios opcionais:
  
 +
:* Apelido.
 +
:* Nome.
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
: Dependendo dos datos recibidos, a condición podería ser:
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
       
 
−
    </head>
 
  
−
    <body>
+
::<syntaxhighlight lang="sql" enclose="div">
 +
WHERE apelido = ? </syntaxhighlight>
  
−
        <?php
+
: ou:
−
            $proc= sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
 
−
            if ($conex->multi_query($proc)){
 
−
                $result = $conex->store_result();
 
−
                if (!($resultado = $conex->query("SELECT @numero_libros as _p_out"))) {
 
−
                    echo "Fallou a obtención: (" . $conex->errno . ") " . $conex->error;
 
−
                }
 
−
                else {
 
−
                    $fila = $resultado->fetch_assoc();
 
−
                    printf("<h1>NUMERO DE LIBROS A VENDA:%d",$fila['_p_out']);
 
−
                }
 
−
            }
 
−
            else {
 
−
                echo "Fallou a chamada ao proc.: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
  
−
       
+
::<syntaxhighlight lang="sql" enclose="div">
−
            $conex->close();  // Pechamos a conexión
+
WHERE nome = ? </syntaxhighlight>
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
−
</syntaxhighlight>
 
  
−
=== Consulta chamando a funcións ===
+
: ou:
  
−
* Dende Mysql pódense definir funcións, que son procedementos almacenados que devolven un valor dun tipo de datos.
+
::<syntaxhighlight lang="sql" enclose="div">
 +
WHERE apelido = ?
 +
AND nome = ? </syntaxhighlight>
  
−
: [http://dev.mysql.com/doc/refman/5.7/en/create-procedure.html Definicións de funcións dende Mysql].
+
: Se non se indica ningún criterio, seleccionaranse todas as filas.
  
 +
* Podemos construír dinamicamente a sentenza mantendo os valores separados da instrución SQL:
  
−
* Vexamos un exemplo:
+
::<syntaxhighlight lang="sql" enclose="div">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
DELIMITER $$
 
DELIMITER $$
  
−
CREATE FUNCTION `EXEMPLO_FUNCION`() RETURNS int
+
CREATE PROCEDURE proba(
 +
IN _apelido VARCHAR(100),
 +
IN _nome VARCHAR(50)
 +
)
 
BEGIN
 
BEGIN
−
    declare variable int;
 
  
−
SELECT col1
+
```
−
INTO variable
+
SET @cadea = 'SELECT nome, apelido
−
FROM TABLA_1;
+
              FROM AUTORES
 +
              WHERE 1 = 1';
 +
 
 +
-- Gardamos os valores en variables de usuario,
 +
-- xa que EXECUTE ... USING traballa con este tipo de variables
 +
SET @p_apelido = _apelido;
 +
SET @p_nome = _nome;
 +
 
 +
IF _apelido IS NOT NULL THEN
 +
    SET @cadea = CONCAT(
 +
        @cadea,
 +
        ' AND apelido = ?'
 +
    );
 +
END IF;
  
−
return variable;
+
IF _nome IS NOT NULL THEN
 +
    SET @cadea = CONCAT(
 +
        @cadea,
 +
        ' AND nome = ?'
 +
    );
 +
END IF;
  
 +
PREPARE stmt FROM @cadea;
  
−
END
+
IF _apelido IS NOT NULL AND _nome IS NOT NULL THEN
−
</syntaxhighlight>
+
 
 +
    EXECUTE stmt USING @p_apelido, @p_nome;
 +
 
 +
ELSEIF _apelido IS NOT NULL THEN
 +
 
 +
    EXECUTE stmt USING @p_apelido;
 +
 
 +
ELSEIF _nome IS NOT NULL THEN
 +
 
 +
    EXECUTE stmt USING @p_nome;
 +
 
 +
ELSE
  
−
: Como vemos, esta función vai devolver un número de tipo integer.
+
    EXECUTE stmt;
  
 +
END IF;
  
−
* Para chamar á función dende Mysql teremos que 'engadila' dentro dunha cláusula SELECT.
+
DEALLOCATE PREPARE stmt;
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
```
−
define('FUNCION_EXEMPLO_MYSQL','select EXEMPLO_FUNCION() as valor_devolto');
 
−
</syntaxhighlight>
 
  
−
: Como vemos, a tratamos coma dunha cláusula SELECT calquera, dándolle un nome (alias) ao resultado da execución da función.
+
END$$
  
 +
DELIMITER ; </syntaxhighlight>
  
−
* Agora soamente queda obter o resultado como fixemos [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Consulta no exemplo de consulta], anteriormente visto.
+
* Neste exemplo:
  
 +
:* `@cadea` almacena a sentenza SQL que se vai construíndo.
 +
:* Os valores de `_apelido` e `_nome` '''non se concatenan directamente dentro da instrución SQL'''.
 +
:* Cada valor que se necesita engádese mediante un marcador `?`.
 +
:* `PREPARE` prepara a sentenza.
 +
:* `EXECUTE ... USING` asocia os valores aos marcadores e executa a sentenza.
 +
:* `DEALLOCATE PREPARE` libera a sentenza preparada cando xa non se necesita.
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="19" >
+
: '''IMPORTANTE:''' É necesario ter en conta os espazos en branco ao construír dinamicamente a sentenza mediante `CONCAT()`. Por exemplo:
  
−
<?php
+
::<syntaxhighlight lang="sql" enclose="div">
 +
WHERE 1 = 1 AND apelido = ? </syntaxhighlight>
  
−
define('FUNCION_EXEMPLO_MYSQL','select EXEMPLO_FUNCION() as valor_devolto');
+
: debe conter un espazo entre `1 = 1` e `AND`.
  
−
$conex = new mysqli('SERVIDOR','USUARIO','PASSWORD','BD');
 
−
$conex->set_charset('utf8');
 
  
−
if ($conex->connect_error){
 
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
  
−
?>
+
<br />
  
 +
== Operacións contra a base de datos ==
  
−
        <?php
+
* [[PHP_UD3_OP_CONSULTA | Operacións de consulta]]
−
            $result=$conex->query(FUNCION_EXEMPLO_MYSQL);
+
* [[PHP_UD3_OP_ALTA | Operacións de alta]]
−
            if ($result || ($result->num_rows>0)){
+
* [[PHP_UD3_OP_BAIXA | Operacións de baixa]]
−
                $row = $result->fetch_assoc();
+
* [[PHP_UD3_OP_MODIFICAR | Operacións de modificación]]
−
                printf("<h1>Dato devolto:%d</h1>",$row['valor_devolto']);
+
* [[PHP_UD3_EXERCICIOS_PROPOSTOS | Exercicios propostos]]
−
                $result->free();
 
−
            }
 
−
            else {
 
−
                echo "Fallou a chamada á función: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
  
−
       
+
<br />
−
            $conex->close();  // Pechamos a conexión
+
* [[PHP_UD3_TRANSACCIONS | Transaccións]]
−
       
 
−
        ?>      
 
−
</syntaxhighlight>
 
  
−
:<u>Nota:</u> Fixarse como na liña 20 a referencia que utilizamos é o alias posto no DEFINE.
 
  
 +
<br />
  
−
==== Caso Práctico: Consulta chamando a funcións ====
+
== Permisos mínimos ==
  
−
* Imos facer un exemplo aplicado ao noso proxecto.
+
* Sempre debemos ter en mente a seguridade de acceso aos datos gardados nunha base de datos.
−
: Imos facer que o número de libros da nosa tenda sexa obtido utilizando unha función e non o procedemento almacenado con parámetro de saída da sección anterior.
 
  
−
* '''Creamos a función en Mysql'''
+
: Ata o de agora, o usuario que usamos para acceder á base de datos tiña asinado todos os permisos sobre a mesma, pero esta práctica non é aconsellable.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
: O ideal é que teña asinado os permisos mínimos necesarios para que poida utilizar a aplicación.
−
DELIMITER $$
 
  
−
CREATE FUNCTION `CONSULTAR_NUM_LIBROS`() RETURNS int
 
−
BEGIN
 
−
    declare num_libros int;
 
  
−
SELECT COUNT(*)
+
* Cando creamos unha aplicación WEB podemos ter varios supostos no que se refire ao acceso á base de datos.
−
INTO num_libros
+
:* Temos creado en MYSQL un usuario diferente por cada usuario, de tal forma que a cadea de conexión á base de datos a crearemos en base ao usuario/password introducido dende unha páxina PHP.
−
FROM LIBROS;
+
:* Utilizamos o mesmo usuario MYSQL para acceder á base de datos e temos unha táboa de usuario/password. En función do usuario validado, este terá acceso a diferentes páxinas PHP con diferentes funcionalidades.
 +
:* Utilizamos o mesmo usuario MYSQL para acceder á base de datos e todos os usuarios do sitio web poden ter acceso ás mesmas páxinas.
  
−
return num_libros;
+
* En calquera destes supostos, o usuario que empreguemos para acceder a MYSQL debe ter os permisos mínimos para poder funcionar no noso sitio web.
  
  
−
END
 
−
</syntaxhighlight>
 
  
  
 +
* Partimos de que temos creado un usuario en Mysql, que vai ser o usuario que imos utilizar no noso sitio web para conectarnos a MYSQL e queremos darlle os permisos mínimos.
  
−
* '''Aqruivo bd.inc.php'''
 
−
: Definimos a constante que vai chamar á función.
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define('FUNCION_CONSULTAR_NUM_LIBROS','select CONSULTAR_NUM_LIBROS() as num_libros');
 
−
</syntaxhighlight>
 
  
−
: Fixarse no alias da chamada á función.
+
* '''CASO 1:''' A nosa aplicación fai as operacións contra a base de datos empregando sentenzas SQL (SELECT/INSERT/UPDATE/DELETE) deberemos outorgar os permisos e so os permisos necesarios para facer as operacións sobre as táboas da nosa base de datos.
  
 +
:* Para facelo deberemos facer uso da [https://dev.mysql.com/doc/refman/5.5/en/grant.html orde grant] e [https://dev.mysql.com/doc/refman/5.5/en/revoke.html orde revoke].
 +
:: A primeira (grant) serve para outorgar un permiso a un usuario.
 +
:: A segunda (revoke) serve para quitar un permiso a un usuario.
  
−
* '''Creamos unha páxina Consultar_Num_Libros_Con_FuncionMYSQL.php'''
+
:* No caso de SELECT e UPDATE, podemos outorgar permisos a nivel de columnas, de tal forma que podemos especificar que columnas dunha táboa vai ter permiso para poder ser seleccionadas ou actualizadas.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
<?php
 
  
−
require ('/var/www/confBD/bd.inc.php');
 
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
: Se estamos a utilizar o WorkBench, podemos asociar os permisos graficamente.
−
$conex->set_charset('utf8');
 
  
−
if ($conex->connect_error){
+
[[Imagen:Php_Perm_Minimos_1.jpg|600px|center]]
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
  
−
?>
+
::* Prememos na sección 'User and privileges' (1).
 +
::* Prememos o botón 'Add entry' e escollemos o schema (base de datos) sobre a que queremos outorgar os permisos (2).
 +
::* Aparecerá o schema na lista (3)^.
 +
::* Agora na parte baixa aparecerán os permisos que podemos outorgar. Fixarse que os marcados con (4) son os permisos de SELECT/INSERT/UPDATE/DELETE
  
 +
:* Se eliximos este 'tipo de permisos' estaremos dando o permiso sobre todas as táboas creadas na base de datos.
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
 
−
    <head>
 
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
−
        <title>Operacións BD</title>
 
−
       
 
−
    </head>
 
  
−
    <body>
+
: Se estamos a utilizar a '''ferramenta Phpmyadmin''' poderemos facer o mesmo <u>seleccionado a base de datos</u> e indo a opción '''Privilegios'''. Aparecerá o usuario creado e poderemos editar os seus permisos.
 +
[[Imagen:Drupal_instalacion_mysql_7.jpg|600px|center]]
  
−
        <?php
 
−
            $result=$conex->query(FUNCION_CONSULTAR_NUM_LIBROS);
 
−
            if ($result && ($result->num_rows>0)){
 
−
                $row = $result->fetch_assoc();
 
−
                printf("<h1>N&uacute;mero de libros:%d</h1>",$row['num_libros']);
 
−
                $result->free();
 
−
            }
 
−
            else {
 
−
                echo "Fallou a chamada ao proc.: (" . $conex->errno . ") " . $conex->error;
 
−
            }
 
  
−
       
 
−
            $conex->close();  // Pechamos a conexión
 
−
       
 
−
        ?>       
 
−
       
 
−
       
 
−
    </body>   
 
−
</html>
 
  
−
</syntaxhighlight>
 
  
−
=== Paxinación de resultados ===
+
:* Se queremos dar un permiso individual a unha táboa usaremos a orde '''grant'''. <u>Esta será a opción a escoller se queremos otorgar un <b>permisos mínimos</b> a nosa base de datos</u>:
 +
::<u>Nota:</u> A orde grant, cando fai referencia a un obxecto (coma unha táboa) pode indicarse o nome da base de datos e o nome do obxecto desta forma: bd.obxecto
 +
::: Nos exemplos seguintes omitimos o nome da base de datos e supoñemos que xa está seleccionada (activa) a base de datos sobre a que imos a executar as ordes.
  
−
* En moitas ocasións imos necesitar paxinar os resultados dunha consulta.
+
:::Permiso 'select' sobre unha táboa.
−
: Isto é necesario cando o resultado pode devolver unha cantidade moi grande de rexistros que obrigaría ao usuario a facer demasiado scroll.
+
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
grant select on TABOA to 'novo_usuario';
 +
</syntaxhighlight>
  
 +
:::Permiso 'select' sobre unha columna dunha táboa.
 +
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
grant select (columna) on TABOA to 'novo_usuario';
 +
</syntaxhighlight>
  
−
* A idea é moi simple. Utilizando a cláusula [https://dev.mysql.com/doc/refman/5.5/en/limit-optimization.html '''LIMIT''' da orde SQL Select].
+
:::Permiso 'update' sobre unha táboa.
−
: Dita cláusula pode levar dous parámetros separados por coma: LIMIT numero_fila,tamaño e vai ao final da orde SELECT.
+
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
grant update on TABOA to 'novo_usuario';
 +
</syntaxhighlight>
  
−
: Por exemplo:  
+
:::Permiso 'update' sobre unha columna dunha táboa.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4" >
+
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
−
SELECT id_libro,titulo
+
grant update (columna) on TABOA to 'novo_usuario';
−
FROM LIBROS
+
</syntaxhighlight>  
−
ORDER by titulo
 
−
LIMIT 0,2
 
−
</syntaxhighlight>
 
  
−
:: Este exemplo amosaría os dous primeiros rexistros empezando polo primeiro do conxunto de resultados (rexistros 0 e 1)
+
:: Para os permisos DELETE e INSERT soamente é necesario poñer na orde grant a operación igual que nos casos anteriores.
  
−
: Se modifico a orde e poño:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4" >
 
−
SELECT id_libro,titulo
 
−
FROM LIBROS
 
−
ORDER by titulo
 
−
LIMIT 2,2
 
−
</syntaxhighlight>
 
  
−
:: Devolverá os rexistros 2 e 3 do conxunto de resultados. Estamos paxinando os resultados de dous en dous !!!!
 
  
  
−
* Polo tanto para paxinar os datos dunha consulta terei que:
 
−
:* Establecer o número de rexistros por páxina. Isto será o que leve a cláusula LIMIT como segundo dato.
 
−
:* Gardar a páxina actual e mandar dito dato entre as diferentes peticións da páxina PHP (normalmente enlaces có número de páxina).
 
−
:* Para saber o número de páxinas terei que obter o número de rexistros en total, xa que o número de páxinas será igual ao número de rexistros dividido (parte enteira) entre o número de rexistros por páxina.
 
  
 +
* '''CASO 2:''' A nosa aplicación fai as operacións contra a base de datos empregando <u>procedementos almacenados</u>.
  
 +
:* No caso anterior, aínda que melloramos a seguridade, o usuario aínda ten acceso ás táboas directamente e pode ver a estrutura das mesmas.
 +
:: Se queremos aumentar a seguridade podemos utilizar procedementos almacenados.
  
−
==== Caso Práctico: Paxinación de resultados ====
 
  
−
* Aplicado ao noso exemplo, imos engadir máis datos para poder facer unha paxinación.
+
:* Xa vimos anteriormente que empregar ditos procedementos leva consigo:
 +
::* Mellor rendemento xa que as sentenzas xa están compiladas e analizadas polo xestor.
 +
::* Mellor seguridade xa que é imposible facer ataques de inxección de código.
 +
::* Mellor seguridade xa que o usuario non pode ver a estrutura de táboas.
 +
::* Mantemento máis doado, xa que podo cambiar a estrutura de táboas sempre que o procedemento continúe devolvendo o mesmo ao usuario.
  
−
: '''Táboa AUTORES'''
 
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
* <u>IMPORTANTE</u>: Se utilizamos este tipo de 'seguridade' xa non necesitamos outorgar ningún tipo de permiso sobre as táboas.
−
INSERT INTO `PHP`.`AUTORES` (`apelido`, `nome`) VALUES ('ABBOTT', 'RACHEL');
 
−
INSERT INTO `PHP`.`AUTORES` (`apelido`, `nome`) VALUES ('HJORTH', 'MICHAEL');
 
−
INSERT INTO `PHP`.`AUTORES` (`apelido`, `nome`) VALUES ('LAURENS', 'STEPHANIE');
 
−
INSERT INTO `PHP`.`AUTORES` (`apelido`, `nome`) VALUES ('ESTRINGANA', 'MORUENA');
 
−
INSERT INTO `PHP`.`AUTORES` (`apelido`, `nome`) VALUES ('NIXON', 'RITA');
 
−
</syntaxhighlight>
 
  
 +
:* O permiso que debemos outorgar para que poida executar un procedemento almacenado é 'EXECUTE'.
 +
::* Podemos facelo como no punto anterior facendo uso do WORKBENCH (neste caso daremos permiso de execución sobre todos os procedementos almacenados).
 +
::* Usar a orde '''grant''':
  
−
: '''Táboa LIBROS'''
+
::: Dando permiso de execución a todos os procedementos almacenados.
 +
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 +
grant execute on *.* to 'novo_usuario'
 +
</syntaxhighlight>
  
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
::: Dando permiso de execución a un procedemento almacenado.
−
INSERT INTO `PHP`.`LIBROS` (`titulo`) VALUES ('COMO UNA EXTRAÑA');
+
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
−
INSERT INTO `PHP`.`LIBROS` (`titulo`) VALUES ('CRÍMENES DUPLICADOS');
+
grant execute on procedure NOME_PROCEDEMENTO to 'novo_usuario'
−
INSERT INTO `PHP`.`LIBROS` (`titulo`) VALUES ('A SALVO CON SU CAPTOR');
 
−
INSERT INTO `PHP`.`LIBROS` (`titulo`) VALUES ('MI ERROR FUE NO SER YO MISMA. PARTE II');
 
−
INSERT INTO `PHP`.`LIBROS` (`titulo`) VALUES ('EL AMOR NO ES UN INVENTO DE LOS POETAS');
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
== Gardando contrasinais de forma segura ==
 +
 +
* En moitas aplicacións necesitamos gardar na base de datos información relacionada coa autenticación dos usuarios, como por exemplo os seus '''contrasinais'''.
 +
 +
* Os contrasinais '''nunca deben gardarse directamente na base de datos en texto plano'''.
  
−
: '''Táboa LIBROS_AUTORES'''
+
: Se un atacante consegue acceder á base de datos podería coñecer inmediatamente os contrasinais de todos os usuarios.
−
:: Teredes que ter en conta os identificadores autonuméricos xerados nos insert´s anteriores.
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
INSERT INTO `PHP`.`LIBROS_AUTORES` (`libro_id`, `autor_id`) VALUES ('3', '2');
 
−
INSERT INTO `PHP`.`LIBROS_AUTORES` (`libro_id`, `autor_id`) VALUES ('4', '3');
 
−
INSERT INTO `PHP`.`LIBROS_AUTORES` (`libro_id`, `autor_id`) VALUES ('5', '4');
 
−
INSERT INTO `PHP`.`LIBROS_AUTORES` (`libro_id`, `autor_id`) VALUES ('6', '5');
 
−
INSERT INTO `PHP`.`LIBROS_AUTORES` (`libro_id`, `autor_id`) VALUES ('7', '6');
 
−
</syntaxhighlight>
 
  
 +
* Tampouco debemos empregar algoritmos de hash xenéricos como `MD5`, `SHA1` ou `SHA256` directamente para almacenar contrasinais.
  
−
: '''Táboa EXEMPLARES'''
+
: Estes algoritmos están deseñados para calcular hashes de forma moi rápida, característica que facilita que un atacante poida realizar grandes cantidades de intentos mediante ataques de forza bruta.
−
:: Teredes que ter en conta os identificadores autonuméricos xerados nos insert´s da táboa libros.
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `prezo`, `estado_id`) VALUES ('3', '1', '9,99', '1');
 
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `prezo`, `estado_id`) VALUES ('3', '2', '9,99', '1');
 
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('4', '1', '', '29,99', '1');
 
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `prezo`, `estado_id`) VALUES ('5', '1', '23,35', '1');
 
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('6', '1', '', '34,76', '1');
 
−
INSERT INTO `PHP`.`EXEMPLARES` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('7', '1', '', '22,54', '1');
 
−
</syntaxhighlight>
 
  
 +
* Para gardar contrasinais PHP proporciona funcións específicas que empregan algoritmos deseñados para este propósito:
  
−
* Creamos o procedemento almacenado en Mysql para que devolva os libros con paxinación.  
+
:* [https://www.php.net/manual/es/function.password-hash.php password_hash()]
−
: Poderíamos modificar o que xa tiñamos, comprobando se enviamos valores aos parámetros ou non (valor null) e facendo un select ou outro en función dos valores...
+
:* [https://www.php.net/manual/es/function.password-verify.php password_verify()]
 +
:* [https://www.php.net/manual/es/function.password-needs-rehash.php password_needs_rehash()]
  
−
'''Procedemento almacenado CONSULTAR_LIBROS_PAXINACION en Mysql'''
+
=== Recomendacións para os contrasinais ===
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_LIBROS_PAXINACION`(IN _num_rexistro int,IN _tam_paxina INT)
+
* Actualmente considérase que a '''lonxitude''' do contrasinal é un dos factores máis importantes para aumentar a súa seguridade.
−
BEGIN
 
−
SELECT id_libro,titulo
 
−
FROM LIBROS
 
−
ORDER BY titulo
 
−
LIMIT _num_rexistro,_tam_paxina;
 
  
−
END
+
* Como recomendación xeral:
−
</syntaxhighlight>
 
  
 +
:* Se o contrasinal é o único factor de autenticación, debería ter como mínimo '''15 caracteres'''.
 +
:* Se se emprega como parte dun sistema de autenticación multifactor (MFA), poderá admitirse unha lonxitude mínima de '''8 caracteres'''.
 +
:* Débense permitir contrasinais longos ou '''frases de paso (passphrases)'''.
 +
:* Recoméndase permitir unha lonxitude máxima de polo menos '''64 caracteres'''.
 +
:* Non é recomendable obrigar ao usuario a incluír necesariamente maiúsculas, minúsculas, números e caracteres especiais.
 +
:* Débese evitar o emprego de contrasinais moi coñecidos, previsibles ou que apareceran en filtracións de datos.
  
 +
: Por exemplo, unha frase de paso longa pode ser máis fácil de lembrar e proporcionar maior seguridade que un contrasinal curto construído unicamente para cumprir regras de complexidade.
  
−
* Creamos a constante que vai facer uso deste procedemento.
+
: Máis información:
−
'''Arquivo: bd.inc.php'''
+
:* [https://pages.nist.gov/800-63-4/sp800-63b.html NIST SP 800-63B]
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
:* [https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html OWASP - Authentication Cheat Sheet]
−
define('PROCEDEMENTO_CONSULTAR_LIBROS_PAXINACION','CONSULTAR_LIBROS_PAXINACION(%d,%d)');
 
−
</syntaxhighlight>
 
  
−
:: Como vemos imos enviar dous parámetros.
+
=== Función password_hash() ===
  
 +
* Para gardar un contrasinal na base de datos empregaremos a función:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
password_hash() </syntaxhighlight>
  
 +
* Por exemplo:
  
−
* Partimos do código da páxina Consultar_Libros.php e <u>creamos unha nova páxina</u> de nome '''Consultar_Libros_Con_Paxinas.php'''
+
::<syntaxhighlight lang="php" line enclose="div">
−
: '''Arquivo: Consultar_Libros_Con_Paxinas.php'''
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="4,27-42,45-66,69-70" >
 
 
<?php
 
<?php
  
−
    require ('/var/www/confBD/bd.inc.php');
+
$password = 'contrasinal_introducido_polo_usuario';
−
     define('CONST_NUM_LIBROS_PAXINA',2);
+
 
−
     //error_reporting(0);
+
$hash = password_hash(
 +
     $password,
 +
     PASSWORD_DEFAULT
 +
);
  
−
    $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
echo $hash;
−
    $conex->set_charset('utf8');
 
  
−
    if ($conex->connect_error){
 
−
        die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
    }
 
 
?>
 
?>
  
−
           
+
</syntaxhighlight>
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
 
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
* A constante:
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
 
−
    <head>
+
::<syntaxhighlight lang="php" enclose="div">
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
PASSWORD_DEFAULT </syntaxhighlight>
−
        <title>Operacións BD</title>
+
 
−
    </head>
+
: indica que PHP empregará o algoritmo considerado predeterminado para almacenar contrasinais.
 +
 
 +
: En PHP 8.5, `PASSWORD_DEFAULT` emprega actualmente '''bcrypt'''. Non obstante, esta constante está deseñada para que PHP poida cambiar o algoritmo empregado en futuras versións se aparece unha alternativa máis adecuada.
 +
 
 +
: Por este motivo, a lonxitude do hash podería cambiar no futuro e recoméndase gardalo nun campo suficientemente grande da base de datos, por exemplo:
 +
 
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
password VARCHAR(255) </syntaxhighlight>
 +
 
 +
=== O salt ===
  
−
    <body>
+
* Os algoritmos de almacenamento de contrasinais empregan un valor aleatorio denominado '''salt'''.
−
               
 
−
            <?php
 
−
                // OBTEMOS O NUMERO DE LIBROS
 
−
                $numLibros=0;
 
−
                $proc= sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_NUM_LIBROS);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();
 
−
                    if (!($resultado = $conex->query("SELECT @numero_libros as _p_out"))) {
 
−
                        die ("Fallou a obtención: (" . $conex->errno . ") " . $conex->error);
 
−
                    }
 
−
                    else {
 
−
                        $fila = $resultado->fetch_assoc();
 
−
                        $numLibros = $fila['_p_out'];
 
−
                    }
 
−
                }
 
−
                else {
 
−
                    die("Fallou a chamada ao proc.: (" . $conex->errno . ") " . $conex->error);
 
−
                }
 
−
            ?>
 
−
            <?php
 
−
                // AMOSAMOS AS PAXINAS EN BASE AO NUMERO TOTAL E O NUMERO POR PAXINA
 
−
                $total_paxinas = ceil($numLibros/CONST_NUM_LIBROS_PAXINA);
 
−
               
 
−
                // Establecemos cal é a páxina actual. Se o usuario elixiu algunha virá en forma de URL $_GET
 
−
                if(!empty($_GET['paxina'])){
 
−
                    $paxina_actual=$_GET['paxina'];
 
−
                }
 
−
                else{
 
−
                    $paxina_actual=1;  // A primeira vez o usuario non pulsou ningunha páxina e polo tanto a páxina inicial é a 1
 
−
                }
 
−
               
 
−
                // 'Imprimimos as páxinas...  
 
−
                echo "<div>";
 
−
                for($cont=1;$cont<=$total_paxinas;$cont++){
 
−
                    if ($cont==$paxina_actual){  // A páxina actual non leva link
 
−
                        echo $cont . ' ';
 
−
                    }
 
−
                    else{
 
−
                        printf("<a href='%s?paxina=%d'>%d</a> ",$_SERVER['PHP_SELF'],$cont,$cont);
 
−
                    }
 
−
                }
 
−
                echo "</div>";
 
−
            ?>
 
−
            <?php       
 
−
                $rexistro_actual=($paxina_actual-1)*CONST_NUM_LIBROS_PAXINA;  // Restamos 1 á páxina actual xa que a páxina 1 empeza no rexistro 0
 
−
                $proc=sprintf(PROCEDEMENTO_CONSULTAR_LIBROS_PAXINACION,$rexistro_actual,CONST_NUM_LIBROS_PAXINA);   
 
−
                $proc = sprintf("CALL %s",$proc);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();
 
−
                    if ($result->num_rows>0){
 
−
                        echo "<table border='1'>";
 
−
                        while($row = $result->fetch_assoc()) {
 
−
                            echo '<tr>';
 
−
                            printf("<td>%s</td>",$row["titulo"]);
 
−
                            echo '</tr>';
 
−
                        }
 
−
                        echo '</table>';
 
−
                        $result->free();  // Liberamos da memoria os recursos
 
−
                    }
 
−
                    else {
 
−
                        echo "Non hai datos que amosar!!!!";
 
−
                    }
 
−
                }
 
−
               
 
−
                $conex->close();  // Pechamos a conexión
 
−
                   
 
−
                 
 
−
            ?>       
 
−
    </body>   
 
−
</html>
 
  
−
</syntaxhighlight>
+
: O salt fai, entre outras cousas, que dous usuarios que teñan o mesmo contrasinal obteñan normalmente hashes diferentes.
  
−
* Liña 4: Indicamos o número de libros a amosar en cada páxina.
+
: Tamén dificulta o emprego de táboas precalculadas de hashes, como as chamadas '''Rainbow Tables'''.
−
* Liñas 27-42: Obtemos o número de libros que temos na base de datos. Estamos a aproveitar o código dun exercicio anterior no que amosábamos como traballar cun parámetro de saída, pero neste caso sería máis sinxelo crear un novo procedemento ou unha función, que devolvera o número de libros.
 
−
* Liñas 45-66: Amosamos as páxinas en base ao número de rexistros totais e o número de libros a amosar por páxina.
 
−
* Liñas 69-70: Pasamos ao procedemento almacenado a partires de que número de libro e a cantidade de libros que vai devolver o select no procedemento almacenado.
 
  
 +
* '''Non debemos crear nin proporcionar manualmente o salt'''.
  
−
=== Exercicio proposto ===
+
: A función `password_hash()` xera automaticamente un salt seguro e diferente para cada contrasinal.
  
−
* Crea unha páxina de nome '''Buscar_Libros.php''' no que se amose nun formulario as seguintes opcións de busca de libros:
+
::<syntaxhighlight lang="php" enclose="div">
−
:* Busca por título
+
$hash = password_hash(
−
:* Busca por prezo
+
$password,
−
:* Busca por estado
+
PASSWORD_DEFAULT
−
:* Busca por autor
+
); </syntaxhighlight>
  
−
::* A busca por autor estará implementada en forma de combobox (<select> en html)
+
: O algoritmo empregado, os seus parámetros e o salt necesario para comprobar posteriormente o contrasinal están incluídos dentro do propio hash devolto por `password_hash()`.
−
::* A busca por título fará uso de like (busca por patróns)
 
−
::* A busca por estado estará implementada en forma de lista (<select> en html con size)
 
−
::* A busca por prezo estará implementada en forma de caixa limitada a 6 caracteres (3 números, unha coma e dous decimais)
 
  
−
* O usuario vai poder escoller calquera das opcións combinándoas como prefira.
+
: Polo tanto, '''non é necesario gardar o salt nun campo separado da base de datos'''.
  
−
* Implementa a solución utilizando consultas e fai outra versión ('''Buscar_Libros_Proc.php''') utilizando procedementos almacenados.
+
: '''IMPORTANTE:''' En versións antigas de PHP era posible proporcionar manualmente un salt a `password_hash()`. Esta opción está obsoleta desde PHP 7 e foi eliminada en PHP 8.0. Non debe empregarse.
  
−
== Alta ==
+
=== Gardar o contrasinal na base de datos ===
  
−
* Para dar de alta novos rexistros na base de datos temos que facer uso da [http://www.w3schools.com/sql/sql_insert.asp instrución SQL INSERT].
+
* Supoñamos que recibimos o contrasinal desde un formulario:
  
−
* O proceso é moi parecido ao que fixemos coas consultas, coa diferenza que no caso dunha instrución INSERT non temos rexistros de volta.
+
::<syntaxhighlight lang="php" enclose="div">
−
: Soamente debemos comprobar se a inserción a fixo correctamente, comprobando se a chamado ao método query devolve true.
+
$password = $_POST['password']; </syntaxhighlight>
  
−
* O proceso para realizar unha inserción é o seguinte:
+
* Primeiro calcularemos o hash:
−
:* Abrimos a conexión á base de datos.
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
 
−
$conex->set_charset('utf8');
 
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$hash = password_hash(
 +
$password,
 +
PASSWORD_DEFAULT
 +
); </syntaxhighlight>
  
−
:* Construímos a orde SQL: INSERT INTO TABOA_1 (col1,col2) VALUES (valor1,'valor2')
+
* O valor que gardaremos na base de datos será '''$hash''' e nunca o contrasinal orixinal.
−
:: Neste caso a TABOA_1 está constituída por dúas columnas, a primeira numérica e a segunda de tipo cadea.
 
−
:* Executamos a orde e comprobamos que a operación foi correcta:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
$query = "INSERT INTO TABOA_1 (col1,col2) VALUES (valor1,'valor2')";
 
−
$result=$conex->query($query);
 
−
if ($result) {
 
−
  echo "Rexistro dado de alta correctamente";
 
−
}
 
−
else {
 
−
  echo "Erro na inserción:" . $conex->error;
 
−
}
 
−
</syntaxhighlight>
 
  
−
:: Fixarse como a consulta a gardamos nunha variable para non ter problemas coas comiñas na chamada á función query.
+
* Por exemplo, empregando PDO e unha sentenza preparada:
−
:* Pechamos a conexión.
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
$conex->close();  // Pechamos a conexión
 
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="php" line enclose="div">
 +
$sql = 'INSERT INTO usuarios (nome, password)
 +
VALUES (:nome, :password)';
  
 +
$stmt = $conex->prepare($sql);
  
 +
$hash = password_hash(
 +
$_POST['password'],
 +
PASSWORD_DEFAULT
 +
);
  
−
* Cando o rexistro foi dado de alta correctamente podemos comprobar o número de rexistros afectados (no caso dun único rexistro sempre vai devolver 1, pero tamén se podería aplicar a un insert multifila).
+
$stmt->execute([
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
'nome'    => $_POST['nome'],
−
..................
+
'password' => $hash
−
$result=$conex->query($query);
+
]); </syntaxhighlight>
−
if ($result) {
 
−
  echo "Rexistro dado de alta correctamente";
 
−
  echo "Num. rexistros afectados:" . $conex->affected_rows;
 
−
}
 
−
else {
 
−
  echo "Erro na inserción:" . $conex->error;
 
−
}
 
−
$conex->close(); // Pechamos a conexión
 
−
</syntaxhighlight>
 
  
 +
=== Comprobar un contrasinal ===
  
 +
* Cando un usuario inicia sesión non se calcula manualmente o hash do contrasinal para comparalo co gardado.
  
−
=== Obtendo o valor dun campo autonumérico ===
+
* Para comprobar se o contrasinal introducido corresponde co hash almacenado na base de datos empregaremos:
  
−
* Normalmente cando deseñamos unha base de datos, certos campos que son claves os definimos como autonuméricos.
+
::<syntaxhighlight lang="php" enclose="div">
−
: Ao facelo, cando engadimos unha nova fila a un táboa, xérase un novo valor e en certas ocasións necesitamos obter dito valor xa que o podemos utilizar para gardar outros datos noutras táboas que o necesitan por estar relacionadas.
+
password_verify() </syntaxhighlight>
  
−
* Non vale a solución de engadir un dato e inmediatamente facer un 'select' buscando o valor máis alto, xa que non entorno multiusuario, dito valor non ten porqué ser o último engadido.
+
* Por exemplo:
  
−
* Necesitamos utilizar a [http://php.net/manual/es/mysqli.insert-id.php función mysqli::$insert_id]].
+
::<syntaxhighlight lang="php" line enclose="div">
  
−
: Dita función devolve o id autoxerado que se utilizou na última operación.
+
<?php
  
−
* Por exemplo, imaxinemos que temos a táboa TABLE_1 con dúas columnas, col1 int primary key autogenerate e col2 varchar(100).
+
$passwordIntroducido = $_POST['password'];
−
: Cando executamos a orde: INSERT INTO TABLE_1 (col2) VALUES ('valor1')
 
  
−
* O proceso é o seguinte:
+
// Este valor obteríase previamente da base de datos
−
:* Abrimos a conexión á base de datos.
+
$hashGardado = $usuario['password'];
−
:* Executamos a orde SQL que engade unha nova fila.
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="4" >
 
−
$result=$conex->query("INSERT INTO TABLE_1 (col2) VALUES ('valor')");
 
−
if ($result){
 
−
  // Operación correcta
 
−
  printf ("Novo rexistro có id %d.\n", $mysqli->insert_id);
 
−
}
 
−
else {
 
−
  echo 'Erro na execución da operación';
 
−
}
 
−
</syntaxhighlight>
 
−
:* Pechamos a conexión.
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
$conex->close();  // Pechamos a conexión
 
−
</syntaxhighlight>
 
  
 +
if (password_verify($passwordIntroducido, $hashGardado)) {
  
−
=== Caso práctico: Obtendo o valor dun campo autonumérico ===
+
    echo 'Contrasinal correcto';
  
−
* Imos aplicar isto ao noso exemplo.
+
} else {
−
: Imos crear unha páxina de nome '''Alta_Libro_Exemplares.php'''. Dita páxina terá un formulario onde indicaremos o título do libro e o número de exemplares que queremos dar de alta.
 
−
: Ao premer o botón de 'Alta' aparecerán en forma de lista un formulario (unha por cada exemplar) cos datos: imaxe-prezo-estado. Os datos anteriores non se poderán cambiar.
 
−
:: Estado: Será un combobox (select en html) cos estados da táboa estados.
 
−
:: Imaxe: cambiaremos isto máis adiante. Por agora deixade un campo de texto.
 
−
:: Prezo: Prezo da imaxe.
 
  
−
: Como sempre deberedes validar os datos e que teñan o tipo/formato correcto.
+
    echo 'Contrasinal incorrecto';
−
: No segundo formulario aparecerá un botón de 'Alta' e outro de 'Cancelar'.
 
−
:: Ao premer o botón de alta daremos de alta o libro, obteremos o id autoxerado e usaremos ese id para dar de alta aos exemplares.
 
−
:: Se prememos o botón 'Cancelar' desaparecerá o segundo formulario e limparase o primeiro para que poidamos dar de alta un novo libro.
 
  
−
=== Como tratar aos caracteres 'estraños' ===
+
}
  
−
* Con caracteres 'estraños' queremos dicir caracteres fora do abecedario e caracteres numéricos, coma por exemplo: ', '' ,\n,....
+
?>
  
−
* Aquí debemos diferenciar dous casos.
+
</syntaxhighlight>
−
:* Cando queremos gardar caracteres estraños dentro da base de datos.
 
−
:* Cando queremos impedir que o usuario poida utilizar ese tipo de caracteres.
 
  
 +
* `password_verify()` recibe:
  
−
==== Necesitamos gardar caracteres 'estraños' na base de datos ====
+
:# O contrasinal introducido polo usuario.
 +
:# O hash que temos gardado na base de datos.
  
−
* Neste caso, teremos que 'codificar' e 'descodificar' os datos.
+
: A función devolve `true` se o contrasinal se corresponde co hash almacenado ou `false` en caso contrario.
−
:: Codificaremos os datos cando os gardemos (ou modifiquemos) coa orde SQL INSERT e UPDATE.
 
−
:: Decodificaremos: Pode non ser necesario, dependerá do dato e o que queiramos facer con él.  
 
  
−
* As funcións que podemos empregar para isto son:
+
: Toda a información necesaria para realizar a comprobación, incluído o algoritmo, os seus parámetros e o salt, está incluída no propio hash.
−
:* [http://php.net/manual/es/function.htmlentities.php Función htmlentities]: Codifica os caracteres en entidades html. Por exemplo, '<' o codifica como '&lt'.
 
−
:* [http://php.net/manual/es/function.html-entity-decode.php Función html_entity_decode]: Realiza o proceso inverso ao anterior.
 
  
−
* Por exemplo, imaxinade que o usuario engade nunha caixa de texto un código HTML que queredes gardar na base de datos, pero que o navegador non debe 'interpretar'. Por exemplo a cadea: <script>alert('ola);</script>
+
=== Actualización do hash ===
−
: Isto podería ser interpretado como un ataque [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Seguridade_no_uso_dos_formularios como xa vimos anteriormente]. Codificando a cadea coa función htmlentities isto non é posible.
 
−
: Cando amosemos o resultado de aplicar a función, non deberemos decodificalo, xa que o que queremos é que o navegador 'amose' a cadea, non que 'interprete' o código e execute o código javascript.
 
  
 +
* Os algoritmos ou os seus parámetros poden cambiar co paso do tempo.
  
−
<u>IMPORTANTE:</u> Lembrar que para utilizar esta solución, todos os datos (ou os que nos interesen que poidan ter este tipo de caracteres) deben ser codificados.
+
: PHP proporciona a función:
−
: Se <u>un usuario fai unha busca</u> sobre algún destes campos, teremos que codificar tamén a cadea escrita polo usuario.
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
password_needs_rehash() </syntaxhighlight>
  
 +
: que permite comprobar se un hash almacenado debería volver calcularse utilizando a configuración actual.
  
 +
* Podemos facer esta comprobación despois de validar correctamente o contrasinal:
  
−
* Outra forma de facelo, aínda que non tan completa, xa que soamente soluciona o uso de certos caracteres, é a [http://php.net/manual/es/mysqli.real-escape-string.php función  mysqli::real_escape_string].
+
::<syntaxhighlight lang="php" line enclose="div">
−
: Esta función, aplicada a unha cadea de caracteres, 'escapa' os seguintes caracteres:  NUL (ASCII 0), \n, \r, \, ', ", e Control-Z. Isto quere dicir que antepón a ditos caracteres o carácter \ e isto fai que o Mysql interprete o seguinte carácter como o que é.
+
if (password_verify($passwordIntroducido, $hashGardado)) {
  
−
: Vexamos un exemplo coa seguinte cadea:
+
```
−
:: $valor = "O'Really";
+
// O contrasinal é correcto
−
:: Queremos facer a seguinte consulta: INSERT INTO AUTORES (nome) VALUES ('$valor')
 
−
:: Se substituímos $valor polo que vale, vemos que temos un problema coas comiñas simples e daría un erro de sintaxe SQL.
 
  
−
:* Para solucionalo,podemos aplicar a función 'real_escape_string' a $valor da forma:
+
if (password_needs_rehash(
 +
    $hashGardado,
 +
    PASSWORD_DEFAULT
 +
)) {
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
    $novoHash = password_hash(
−
<?php
+
        $passwordIntroducido,
−
$mysqli = new mysqli("localhost", "usuario", "contrasinal", "bd");
+
        PASSWORD_DEFAULT
 +
    );
  
−
if (mysqli_connect_errno()) {
+
     // Actualizar $novoHash na base de datos
−
     printf("Erro na conexión: %s\n", mysqli_connect_error());
 
−
    exit();
 
 
}
 
}
  
 +
// Continuar co proceso de autenticación
 +
```
 +
 +
} </syntaxhighlight>
  
−
$valor = "O'Really";
+
: Desta forma podemos actualizar progresivamente os hashes almacenados cando cambie o algoritmo predeterminado de PHP ou os seus parámetros.
  
−
/* esta consulta faia debido a que non escapa $cidade */
+
=== Hash e cifrado non son o mesmo ===
−
if (!$mysqli->query("INSERT INTO AUTORES (nome) VALUES ('$valor')")) {
+
 
−
    printf("Erro: %s\n", $mysqli->sqlstate);
+
* Un '''hash''' é unha transformación deseñada para non poder obter novamente o valor orixinal.
−
}
+
 
 +
: Por este motivo é adecuada para almacenar contrasinais: non necesitamos coñecer o contrasinal orixinal, unicamente comprobar se o introducido polo usuario é correcto.
  
−
$valor = $mysqli->real_escape_string($valor);
+
* Se necesitamos gardar un dato confidencial pero posteriormente temos que recuperar o seu valor orixinal, '''non debemos empregar `password_hash()`'''.
  
−
/* esta consulta con $valor escapada funcionará */
+
: Nese caso necesitaríamos empregar técnicas de '''cifrado''' adecuadas para protexer os datos.
−
if (!$mysqli->query("INSERT INTO AUTORES (nome) VALUES ('$valor')")) {
 
−
    printf("%d fila inserida.\n", $mysqli->affected_rows);
 
−
}
 
  
−
$mysqli->close();
+
'''IMPORTANTE:'''
−
?>
 
−
</syntaxhighlight>
 
  
 +
:* Para almacenar contrasinais empregaremos `password_hash()`.
 +
:* Para comprobar contrasinais empregaremos `password_verify()`.
 +
:* Non empregaremos `MD5`, `SHA1` nin hashes xenéricos para almacenar contrasinais.
 +
:* Non xeraremos nin gardaremos manualmente o salt.
 +
:* Na base de datos gardaremos unicamente o hash xerado por `password_hash()`.
 +
:* Recoméndase utilizar un campo como `VARCHAR(255)` para almacenar o hash.
  
−
<u>Nota:</u> No momento de escribir esta manual, o escape de caracteres con esta función non funcionaba correctamente (por exemplo non escapaba o carácter ').
+
<br />
−
: Probar a aplicar o [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Filtros_para_a_correcci.C3.B3n filtro FILTER_SANITIZE_MAGIC_QUOTES].
 
  
−
==== Non queremos que o usuario poida utilizar este tipo de caracteres ====
+
== Subindo unha imaxe ao servidor ==
  
−
* A forma máis sinxela é utilizar os filtros [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Funci.C3.B3n_para_validar_.2F_corrixir_campos xa vistos anteriormente nesta wiki].
 
  
−
== Baixa ==
+
* Lembrar que normalmente o que subimos á base de datos é a ruta 'relativa' onde se gardan as imaxes no servidor.
  
−
== Modificación ==
+
* A forma para gardar 'fisicamente' a imaxe no servidor xa a vimos [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Gardando_imaxes_no_servidor anteriormente neste manual].
  
−
== Transaccións ==
 
  
−
== Permisos mínimos ==
 
  
  
Línea 2354: Línea 1798:
  
  
−
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2016).
+
<br> -- [[Usuario:angelfg|Ángel D. Fernández González]] -- (2017).

Revisión actual del 18:03 14 oct 2026

Introdución

IMPORTANTE – API empregada no módulo

Nestes apuntes explícanse diferentes formas de realizar operacións contra unha base de datos MySQL desde PHP, empregando principalmente **mysqli** e **PDO (PHP Data Objects)**.
As explicacións e exemplos realizados con **mysqli**, tanto na súa versión procedemental como orientada a obxectos, mantéñense nos apuntes con carácter **informativo e comparativo**, para coñecer as diferentes posibilidades que ofrece PHP para acceder a MySQL.
Non obstante, a partir deste tema a forma de acceso a bases de datos que se empregará como referencia no módulo será **PDO**, utilizando o controlador **PDO_MySQL**.
Polo tanto, **nos exercicios, prácticas e exames nos que se solicite realizar operacións contra unha base de datos desde PHP, deberá empregarse PDO**, salvo que no enunciado se indique expresamente o contrario.
Recoméndase, polo tanto, prestar especial atención aos exemplos identificados como **“Empregando PDO”**, xa que serán os que se tomarán como referencia para a avaliación.


  • Estes apuntes están actualizados para PHP 8.5. PDO traballa por defecto en modo de excepcións, polo que os erros producidos nas operacións contra a base de datos poden xerar unha PDOException, que poderá ser xestionada mediante try-catch.



  • Como comentamos no punto anterior, para conectarnos a un xestor de base de datos específico imos ter que ter instalados os drivers correspondentes.
Un driver é o software que nos permite comunicarnos cun xestor de base de datos, pero normalmente non imos facer dita comunicación utilizando o driver directamente se non que o usaremos a través dunha API, que son un conxunto de clases, funcións,... que nos permiten facer uso do driver e conectar a un xestor de base de datos.
  • Existen dous tipos de API´s:
  • Procedimentais: Facemos uso de funcións para realizar as operacións contra a base de datos.
  • Orientadas a obxectos: Instanciamos clases e chamamos a métodos dos obxectos creados.

Normalmente faremos uso desta segunda opción.


  • En PHP, ditas API´s se 'activan' facendo uso de Extensións.
Indicar que unha API non é o mesmo que unha extensión en PHP, xa que algunhas extensións non proporcionan ningunha API ao usuario (por exemplo a extensión do driver PDO MySQL, non proporciona ningunha API ó programador PHP).


  • Existen tres APIs principais para conectar a un servidor de bases de datos MySQL:
  • Extensión MySQL de PHP (eliminada a partires da versión PHP 7.0)
  • Extensión mysqli de PHP
  • Obxectos de Datos de PHP (PDO) => A QUE IMOS EMPREGAR


  • A extensión mysqli ven incluída nas versiones PHP 5 e posteriores.
Algunha das melloras de dita extensión con respecto á extensión mysql:

Activar a extensión mysqli e pdo

  • Se instalamos XAMPP ou calquera das versións para outros S.O. normalmente a extensión xa ven habilitada por defecto.
Se temos unha instalación personalizada no que instalamos separadamente todo, teremos que habilitala.


Xa vimos anteriormente que PDO permite conexión a múltiples bases de datos, pero tamén engade unha capa más para a conexión de base de datos, aínda que hoxe en día, a diferenza de rendemento é mínima.


Windows

  • En Windows, a extensión mysqli ven desactivada no php.ini.
Partindo de que xa tedes o arquivo php.ini no cartafol onde se atopa o intérprete de PHP (se non é así mirade a nota de Importante que se atopa neste punto da wiki) temos que:
  • Descomentar a liña:
1 ;extension=php_mysqli.dll
Pasa a:
1 extension=php_mysqli.dll


  • Descomentar a liña:
1 ;extension_dir = "ext"
Pasa a:
1 extension_dir = "ext"
O que fai dita liña é informar a PHP que busque as extensións no cartafol relativo ext, é dicir, nun cartafol chamado ext dentro do cartafol onde se atopa o executable de php.



Linux

  • Necesitamos ter instalado o paquete: php-mysql.
Para instalalo:
1 sudo apt install php-mysql
Reiniciamos o apache: sudo systemctl restart apache2


  • O anterior vai crear varios arquivos de configuración no cartafol: etc/php/8.1/apache2/conf.d para cargar as extensións mysqli e pdo.
Nota: O cartafol pode variar dependendo da versión php que teñades instalada.



Creando as táboas en Mysql


  • Imos implementar unha base de datos para xestionar a venda de libros de segunda mano.
A orde SQL para crear unha base de datos de nome php é:
CREATE SCHEMA `php` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci ;



O modelo EE/R é o seguinte:
Php BD 6.jpg

Nota:

  • A táboa ESTADOS podería eliminarse e crear un campo estado de tipo enum en Mysql se ditos 'estados' van ser sempre os mesmos e non van modificarse...
  • O campo 'imaxe' da táboa EXEMPLARES está nesta táboa e non na táboa LIBROS, xa que a idea é amosar ao comprador unha imaxe do exemplar para amosar o seu estado.


  • O modelo relacional implementado nunha base de datos Mysql é o seguinte:
CREATE TABLE IF NOT EXISTS `php`.`libros` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `titulo` VARCHAR(300) NOT NULL,
  PRIMARY KEY (`id`));

CREATE TABLE IF NOT EXISTS `php`.`estados` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `descripcion` VARCHAR(100) NOT NULL,
  PRIMARY KEY (`id`));

 CREATE TABLE IF NOT EXISTS `php`.`exemplares` (
  `libro_id` INT NOT NULL,
  `id_exemplar` INT NOT NULL,
  `imaxe` VARCHAR(100) NULL,
  `prezo` DECIMAL(6,2) NOT NULL,
  `estado_id` INT NULL,
  INDEX `fk_EXEMPLARES_LIBROS_idx` (`libro_id` ASC),
  INDEX `fk_EXEMPLARES_ESTADOS_idx` (`estado_id` ASC),
  PRIMARY KEY ( `libro_id`,`id_exemplar`),
  CONSTRAINT `fk_EXEMPLARES_LIBROS`
    FOREIGN KEY (`libro_id`)
    REFERENCES `php`.`libros` (`id`)
    ON DELETE NO ACTION
    ON UPDATE CASCADE,
  CONSTRAINT `fk_EXEMPLARES_ESTADOS`
    FOREIGN KEY (`estado_id`)
    REFERENCES `php`.`estados` (`id`)
    ON DELETE NO ACTION
    ON UPDATE CASCADE);

CREATE TABLE IF NOT EXISTS `php`.`autores` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `apelidos` VARCHAR(200) NOT NULL,
  `nome` VARCHAR(50) NOT NULL,
  PRIMARY KEY (`id`));


CREATE TABLE IF NOT EXISTS `php`.`libro_autor` (
  `libro_id` INT NOT NULL,
  `autor_id` INT NOT NULL,
  PRIMARY KEY (`libro_id`, `autor_id`),
  INDEX `fk_LIBROAUTOR_AUTORES_idx` (`autor_id` ASC),
  CONSTRAINT `fk_LIBROAUTOR_LIBROS`
    FOREIGN KEY (`libro_id`)
    REFERENCES `php`.`libros` (`id`)
    ON DELETE NO ACTION
    ON UPDATE CASCADE,
  CONSTRAINT `fk_LIBROAUTOR_AUTORES`
    FOREIGN KEY (`autor_id`)
    REFERENCES `php`.`autores` (`id`)
    ON DELETE NO ACTION
    ON UPDATE CASCADE);
  • Para ter algún dato inicial imos a executar estas ordes:
INSERT INTO `php`.`estados` VALUES (1,'NOVO'),(2,'COMO NOVO'),(3,'DETERIODADO'),(4,'CON ANOTACIÓNS');


INSERT INTO `php`.`autores` (`apelidos`, `nome`) VALUES ('Drumont', 'Eduardo');

INSERT INTO `php`.`libros` (`titulo`) VALUES ('El caso Dreyfus, expresión del antisemitismo, temas, Gallica, BNF');
INSERT INTO `php`.`libros` (`titulo`) VALUES ('Les amis');

INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('1', '1', 'dreyfus.jpg', '20.52', '2');
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('1', '2', 'dreyfus.jpg', '15.34', '3');
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '1', 'amis.jpg', '35.35', '1');
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '2', 'amis.jpg', '20.25', '4');
INSERT INTO `php`.`exemplares` (`libro_id`, `id_exemplar`, `imaxe`, `prezo`, `estado_id`) VALUES ('2', '3', 'amis.jpg', '25.34', '3');

INSERT INTO `php`.`libro_autor` VALUES (1,1),(2,1);


  • Agora faremos o seguinte:
  • Crearemos dentro do noso sitio web (normalmente /var/www/html/) un cartafol de nome IMAXES
  • Copiamos as imaxes que veñen a continuación a dito cartafol.
  • Cambiaremos o propietario coa orde chown -R www-data:www-data IMAXES/

Amis.jpg Nome: amis.jpg Dreyfus.jpg Nome: dreyfus.jpg


Regras de nomeado de táboas e columnas

  • Aínda que non existen 'regras' únicas sobre como nomear as táboas e campos nos imos seguir as seguintes, para que cando vexamos Laravel, coincidan os Modelos de Laravel cós nomes das táboas asociadas:
  • Nomes de táboas en PLURAL e con minúsculas. Por exemplo táboa: usuarios
  • Nomes de campos en minúscula e singular.
  • Se o campo é a clave primaria terá de nome: id
  • Se o campo é unha clave foránea terá de nome: nometáboa_en_singular_id. Por exemplo, nunha táboa de nome pais, asociada á táboa usuarios cunha relación 1:N, tería unha clave foránea de nome usuario_id.
  • Se o nome de campo está formado por varias palabras, estarán separadas por un guión baixo ( _ )
  • Non usar til nin caracteres 'raros' como espazos en branco, interrogacións,...
  • As claves foráneas irán ao final da lista de columnas da táboa.
  • As táboas que se formen por unha relación N:M terán de nome: taboa1singular_taboa2singular. Por exemplo se temos unha táboa usuarios e unha táboa modulos e existe unha relación N:M entre elas, se crearía a táboa: usuario_modulo



Visualización de erros

  • Ao principio é habitual cometer erros durante as probas de programación.
 : Ata que aprendamos a utilizar un depurador (debugger), cando se produce un erro en PHP pode aparecer unha páxina en branco, mostrarse só unha parte da páxina ou aparecer unha mensaxe de erro que indique o problema.
  • Durante o desenvolvemento é recomendable configurar PHP para que mostre os erros. Isto pode facerse modificando as directivas display_errors e error_reporting do ficheiro php.ini.
Se non se quere modificar a configuración de PHP, tamén se poden engadir temporalmente ao principio do ficheiro PHP as seguintes instrucións:
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
Desta forma PHP mostrará os erros, avisos e advertencias que se produzan durante a execución do programa.
IMPORTANTE: Estas opcións deben utilizarse unicamente durante o desenvolvemento e as probas. Nun servidor en produción non se deben mostrar os erros ao usuario.
Hai que ter en conta que estas instrucións non permiten detectar determinados erros que se producen antes de que o programa comece a executarse, como algúns erros de sintaxe. Nestes casos será necesario consultar o rexistro de erros do servidor ou activar a visualización de erros na configuración de PHP (php.ini).
  • Outra ferramenta moi útil é o inspector ou as ferramentas para desenvolvedores do navegador. Pódense abrir normalmente coa tecla F12.
Desde estas ferramentas pódense revisar, entre outras cousas:
  • O código HTML xerado.
  • As peticións realizadas ao servidor.
  • Os códigos de estado HTTP, por exemplo un erro 500 Internal Server Error.
  • As mensaxes da consola de JavaScript.
Con todo, hai que ter en conta que o navegador non pode mostrar o código PHP orixinal, xa que este se executa no servidor. O navegador só recibe o resultado xerado polo programa PHP.
Mysql visualuzar erros 1.png




  • E lembrar que tamén podedes consultar o arquivo de error.log que se atopa en /var/log/apache2/error.log coa orde: tail -n 100 /var/log/apache2/error.log (sendo 100 o número de liñas que queredes consultar do arquivo empezando polo final)


Conexión

  • Partimos da base de que xa temos instalado:
  • O xestor de bases de datos Mysql
  • Unha base de datos creada. Neste manual vai ter de nome php. Facer que o 'default collation' sexa utf8mb4_spanish2_ci (máis información sobre o charset/collation neste enlace.
  • Un usuario con permisos suficientes para realizar as operacións necesarias sobre a base de datos. Neste manual o usuario/password creado son user_php/user_php.


Todo o anterior xa está indicado no punto anterior da wiki.


NOTA IMPORTANTE:

Nos seguintes apartados explícanse distintas formas de realizar operacións contra unha base de datos MySQL desde PHP: utilizando mysqli de forma procedemental, mysqli orientado a obxectos e PDO.

Os exemplos realizados con mysqli mantéñense nos apuntes con carácter informativo e comparativo.

Nos exames, exercicios e prácticas avaliables deberase empregar PDO, salvo que no enunciado se indique expresamente o contrario.

A versión de PHP empregada como referencia nestes apuntes e nos exames será PHP 8.5.



Apertura da conexión

  • Utilizando PDO:
public PDO::__construct(
    string $dsn,
    ?string $username = null,
    #[\SensitiveParameter] ?string $password = null,
    ?array $options = null
)
No caso de PDO é obrigatorio indicar o parámetro dsn (Data Source Name), que identifica o controlador e os datos necesarios para realizar a conexión. No noso caso, ao empregar MySQL, indicaremos o servidor, a base de datos e o xogo de caracteres.


Un exemplo:
$dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';

$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);
Como vemos modificamos varios atributos da conexión no array $opciones. Podemos consultar todas as opcións no seguinte enlace.
  • Neste caso modificamos o atributo ATTR_ERRMODE e damos o valor ERRMODE_EXCEPTION.
O que indica dito atributo é o seguinte:
Excepción	PDO::ERRMODE_EXCEPTION	Fai que PDO lance unha PDOException cando se produce un erro. É o modo predeterminado desde PHP 8.0 e será o que empregaremos no módulo.

Silencioso	PDO::ERRMODE_SILENT	Non lanza erros. Simplemente devolve false se hai un fallo. Hai que comprobar manualmente cada operación.
Aviso (Warning)	PDO::ERRMODE_WARNING	Xera un warning de PHP cando ocorre un erro, pero o script continúa. Obsoleta a partires da versión 8.5. Non empregar.
  • PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC: Indica que cando recollamos datos da base de datos os imos recoller nun array asociativo. Se sabemos empregar a POO podemos optar por FETCH_OBJECT.
  • PDO::ATTR_EMULATE_PREPARES => false: Isto fai que as sentenzas prepared sexan xestionadas polo xestor de base de datos e non polo php.



Fixarse que a conexión a estamos a facer contra Mysql.
Unha das vantaxes de PDO é que proporciona unha interface común para traballar con diferentes xestores de bases de datos. No módulo utilizaremos exclusivamente PDO_MySQL, pero a modo informativo existen outros controladores.
Se queremos conectarnos con outra base de datos poderíamos poñer os seguintes valores:
mysql	mysql:host=localhost;dbname=php;charset=utf8mb4	Conecta a unha base de datos MySQL ou MariaDB
pgsql	pgsql:host=localhost;port=5432;dbname=instituto;user=user_pg;password=1234	Conecta a unha base de datos PostgreSQL
sqlite	sqlite:/var/www/basedatos.db	Conecta a unha base de datos SQLite (sen servidor)
sqlsrv	sqlsrv:Server=localhost;Database=instituto	Conecta a Microsoft SQL Server
oci	oci:dbname=xe;charset=UTF8	Conecta a Oracle
ibm	ibm:DRIVER={IBM DB2 ODBC DRIVER};DATABASE=db;HOSTNAME=host;PORT=50000;PROTOCOL=TCPIP;UID=user;PWD=pass;	Conecta a IBM DB2
firebird	firebird:dbname=localhost:/ruta/a/base.fdb	Conecta a Firebird
dblib	dblib:host=localhost;dbname=instituto	Conecta a Sybase ou Microsoft SQL Server antigo
informix	informix:host=host;service=puerto;database=db;server=servidor;protocol=onsoctcp;UID=user;PWD=pass;	Conecta a Informix
odbc	odbc:mi_dsn	Usa unha fonte de datos ODBC definida no sistema




NOTA: O indicado a continuación inclúese a nivel informativo. Nos exames empregarase PDO.

  • Sintaxe (se indica a sintaxe do construtor e polo tanto será equivalente a facer un 'new' da clase mysqli):
  • Orientada a obxectos:
mysqli::__construct ([ string $host = ini_get("mysqli.default_host") [, string $username = ini_get("mysqli.default_user") [, string $passwd = ini_get("mysqli.default_pw") [, string $dbname = "" [, int $port = ini_get("mysqli.default_port") [, string $socket = ini_get("mysqli.default_socket") ]]]]]] )
  • Por procedementos:
mysqli mysqli_connect ([ string $host = ini_get("mysqli.default_host") [, string $username = ini_get("mysqli.default_user") [, string $passwd = ini_get("mysqli.default_pw") [, string $dbname = "" [, int $port = ini_get("mysqli.default_port") [, string $socket = ini_get("mysqli.default_socket") ]]]]]] )


Todos eles son optativos, pero normalmente indicaremos o nome de host, login, password e base de datos.
Os parámetros son:
  • Host a conectar (cadea): Pode ser o nome ou a dirección IP. Normalmente imos poñer 'localhost' (se o xestor de bases de datos está instalado no mesmo equipo que o servidor web).
  • Nome do usuario con acceso á base de datos (cadea): Nome do usuario que ten acceso á base de datos.
  • Password (cadea): Password do usuario.
  • Dbname (cadea): Nome da base de datos sobre a que imos traballar.
  • Porto (numérico): Porto no que está escoitando o xestor de base de datos. No caso de mysql é o 3306.
  • Socket (cadea): Socket ou tubería para conectar có xestor de bases de datos. En Linux é un tipo especial de arquivo que serve para 'transmitir' información, neste caso, cara o xestor de bases de datos.




  • Aplicado ao noso caso, teremos a seguinte orde:
  • Por procedementos:
1 $conex = mysqli_connect('localhost','user_php','user_php','php');


  • Orientado a obxectos:
1 $conex = new mysqli('localhost','user_php','user_php','php');
$conex é un obxecto que represente a conexión á base de datos.




Control de errores

  • En versións de PHP igual ou superior á 8.1: Neste caso, debemos empregar a forma try ... catch da programación orientada a obxectos. ESTE É A FORMA QUE IMOS EMPREGAR NOS EXAMES:
  • Versión con chamada empregando pdo:
try {
  $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
  
  $usuario = 'user_php';
  $password = 'user_php';

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

}
catch (PDOException $e) {
    die("Error de conexión PDO: (" . $e->getCode() . ") " . $e->getMessage());
}
} catch (Throwable $e) {
    die('Erro xeral: ' . $e->getMessage());
}


Nota:
  • Normalmente ao usuario non se lle vai amosar o erro que devolve o xestor de base de datos e dito erro sóese gardar nunha táboa interna ou nun arquivo de log para o administrador e que poida ver cal foi o erro.
  • Podemos facer un único try-catch que envolva toda a páxina PHP ou facer un try-catch por cada bloque php que poida xerar unha excepción.
  • Os atributos:
  • PDO::ERRMODE_EXCEPTION: Valor predeterminado a partires da versión 8.0, pero o imos poñer de todas formas.
  • PDO::FETCH_ASSOC: Cando consultemos datos devolverá os mesmos en forma de array asociativo. Se poñemos PDO::FETCH_OBJ obteremos os datos como propiedades de clase coma $row->nome.
  • ATTR_EMULATE_PREPARES => false : Fai que as sentenzas prepared (o veremos posteriormente) as faga o xestor de base de datos.


Por exemplo:
try {

    $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';

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

} catch (PDOException $e) {
    die("Erro de conexión: (" . $e->getCode() . ') ' . $e->getMessage());
}
} catch (Throwable $e) {
    die('Erro xeral: ' . $e->getMessage());
}



NotaLembrar que cando se produce un erro non se debe amosar ao usuario nin o getCode() nin o getMessage(). Isto soamente é necesario en depuración, non en producción.
Dará como resultado:
Php BD 3.jpg




NOTA: O indicado a continuación inclúese a nivel informativo. Nos exames empregarase PDO.

  • Por procedementos:
try {
    $conex = mysqli_connect('localhost', 'usuario_inexistente', 'contrasena_incorrecta', 'base_de_datos');
} catch (mysqli_sql_exception $e) {
    die("Erro de conexión: " . $e->getMessage());
}
} catch (Throwable $e) {
    die('Erro xeral: ' . $e->getMessage());
}


  • Versión con chamada empregando obxectos:
try {
    $conex = new mysqli('localhost', 'usuario_inexistente', 'contrasena_incorrecta', 'base_de_datos');
} catch (mysqli_sql_exception $e) {
    die("Erro de conexión: " . $e->getMessage());
}
} catch (Throwable $e) {
    die('Erro xeral: ' . $e->getMessage());
}


  • Por exemplo, imos modificar o password do usuario por un que non exista e veremos o resultado:
1 try {
2     $conex = mysqli_connect('localhost','user_php','password_incorrecta','php');
3 } catch (mysqli_sql_exception $e) {
4     die("Erro de conexión: (" . $e->getCode() . ') ' . $e->getMessage());
5 }
6 } catch (Throwable $e) {
7     die('Erro xeral: ' . $e->getMessage());
8 }



Gardando os datos de conexión nun lugar seguro

  • Como é obvio os datos de base de datos, usuario e password son moi importantes e non debemos permitir que alguén poida descubrilos.
  • O que faremos será o seguinte:
  • Definiremos en forma de constantes ditos datos nun arquivo de include.
  • Gardaremos dito arquivo fora do cartafol de acceso do noso sitio web.
  • O primeiro punto, definir en forma de constante e nun arquivo de include os datos da conexión:

Arquivo: bd.php:

1 <?php
2 
3 define ('SERVIDOR','localhost');
4 define ('USUARIO','user_php');
5 define ('PASSWORD','user_php');
6 define ('BD','php');
  • Faremos un require deste arquivo en todas as páxinas que necesiten unha conexión á base de datos. Desta forma, se necesitamos cambiar algo soamente o cambiaríamos nun arquivo.


  • O segundo punto consiste en descubrir cal é a ruta física á que apunta o noso servidor web e colocar o arquivo de include 'fora' desa ruta.
  • Temos neste punto da Wiki información sobre como funciona Apache e como saber a onde apunta o servidor web no seu arquivo de configuración.
  • Temos varias opcións:
Se facemos un echo destas variables dentro dunha páxina do noso sitio web, obteremos unha ruta, coma por exemplo:
1 /var/www/html/
Nota: Se o facemos coa constante __DIR__ e a páxina que está a facer o echo se atopa dentro dun cartafol, tamén aparecerá o cartafol (por exemplo /var/www/html/cartafol/).


  • Isto quere dicir que o servidor web (e polo tanto calquera usuario de Internet) vai poder acceder as páxinas que se atopan no cartafol /var/www/html e calquera dentro del.
Unha medida de seguridade é colocar o arquivo de include 'fora' desta ruta. Por exemplo, podemos crear un arquivo no cartafol /var/www/confBD
A dito cartafol ten que poder acceder o usuario que utiliza o Apache para acceder aos cartafoles a nivel do S.O. (no caso de Apache soe ser www-data, podedes miralo no punto da wiki indicado anteriormente).


  • Agora, calquera páxina que faga uso destes datos terá que facer o seguinte require:
  • No caso de PDO:
 1 <?php 
 2 
 3 require '/var/www/confBD/bd.php';
 4 
 5 try {
 6 
 7     $dsn = sprintf(
 8         'mysql:host=%s;dbname=%s;charset=utf8mb4',
 9         SERVIDOR,
10         BD
11     );
12 
13     $opciones = [
14         PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
15         PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
16         PDO::ATTR_EMULATE_PREPARES   => false
17     ];
18 
19     $conex = new PDO(
20         $dsn,
21         USUARIO,
22         PASSWORD,
23         $opciones
24     );
25 
26 } catch (PDOException $e) {
27     die('Erro de conexión coa base de datos.');
28 }

IMPORTANTE: Este será o modelo de conexión que se empregará como referencia nos exames.



  • No caso de orientado a obxectos:
1 <?php
2 
3 //error_reporting(0);  // Inabilita os erros de Apache se temos activada a directiva display_erros no php.ini
4 
5 
6 require ('/var/www/confBD/bd.php');
7 $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);


  • Se queremos facer o mesmo pero sen utilizar rutas absolutas (por exemplo se estamos nun aloxamento compartido):
1 <?php
2 
3   $ruta = substr($_SERVER['DOCUMENT_ROOT'],0,strpos($_SERVER['DOCUMENT_ROOT'],'html')) . 'confBD'; // RUTA al sitio web sin /html pero con /confBD
4   require ($ruta.'/bd.php');



O xogo de caracteres

  • Debemos ter en conta que unha páxina web está 'aberta' a todo o mundo que se conecte a ela.
Cada persoa en diferentes países terá diferente xogos de caracteres no que cada carácter está relacionado cun código numérico.
  • Se gardamos como dato dentro da base de datos unha cadea cun 'ñ' entre os seus datos, dito carácter estará asociado a un código nun xogo de caracteres. Se o usuario que vai cargar a páxina o carga cun xogo de caracteres diferente non vai poder visualizalo.


  • Cando creamos aplicacións en PHP con acceso a base de datos teremos que ter en conta os seguintes xogos de caracteres:
  • Xogo de caracteres da base de datos (no noso caso Mysql): É o que se coñece como Charset/Collation.
Php BD 7.jpg
Se vos fixades cando creamos a base de datos en Mysql, escollemos o charset (primeira columna) e o collation (segunda columna).
Como indicamos antes, cando creamos a base de datos o charset/collation indicado foi utf8mb4/utf8mb4_spanish2_ci.
O charset é como mysql garda internamente os caracteres e os relaciona cun código numérico e o collation indica como comparar e ordenar o texto. Podedes consultar esta blog para obter máis información.
  • Xogo de caracteres da páxina HTML.
Na páxina web indicamos coa etiqueta <meta> o xogo de caracteres para que o navegador web poida visualizalos correctamente.
No caso que nos ocupa teremos que poñer:
  • Html 5 e posteriores: <meta charset="UTF-8">
Máis información en w3schools.
  • No código PHP ao conectar coa base de datos.
Cando conectemos coa base de datos teremos que indicarlle que imos utilizar o xogo de caracteres utf8mb4 con esta liña:
  • Versión PDO:
 1 require ('/var/www/confBD/bd.php');
 2 
 3 try {
 4     $dsn = sprintf(
 5         'mysql:host=%s;dbname=%s;charset=utf8mb4',
 6         SERVIDOR,
 7         BD
 8     );
 9 
10     $opciones = [
11         PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
12         PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
13         PDO::ATTR_EMULATE_PREPARES   => false
14     ];
15 
16     $conex = new PDO(
17         $dsn,
18         USUARIO,
19         PASSWORD,
20         $opciones
21     );
  • É importante empregar unha configuración coherente de Unicode en toda a aplicación. Neste módulo empregaremos utf8mb4 en MySQL e na conexión PDO, e UTF-8 nas páxinas HTML..



  • Versión procedimental
Para iso teremos que facer uso da función set_charset.
En caso de éxito devolve true ou false en caso de existir algún problema.
1 try {
2     require ('/var/www/confBD/bd.php');
3     $conex = mysqli_connect(SERVIDOR, USUARIO, PASSWORD, BD);
4      mysqli_set_charset($conex,'utf8mb4');
5 } catch (mysqli_sql_exception $e) {
6     die("Erro de conexión: " . $e->getMessage());
7 }


  • Versión orientada a obxectos:
1 try {
2     require ('/var/www/confBD/bd.php');
3     $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
4     $conex->set_charset('utf8mb4');
5 } catch (mysqli_sql_exception $e) {
6     die("Erro de conexión: " . $e->getMessage());
7 }




Peche da conexión

  • Despois de facer as diferentes operacións sobre a base de datos, teremos que pechar a conexión.
Para iso temos que chamar á función mysqil::close nas versión orientada a obxectos e procedimental. No caso de PDO chega con igual a null o obxecto $conex.
Nota: Neste exemplo partimos que estamos a empregar a verión PHP 8.1 ou superior. En todas as páxinas se opta por facer un único try-catch, pero dependendo de se se quere saber a sección onde foi o erro, se podería facer varios try-catch.


  • Versión PHP 8.1 ou superior


  • Versión PDO:
En PDO non existe un método close(). Para pechar explicitamente unha conexión podemos asignar null á variable que contén o obxecto PDO. Se non o facemos, PHP pechará automaticamente a conexión ao rematar a execución do script.
<?php

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

    // ..............
    // Operaciones contra a base de datos e amosar información ao usuario
    // ..............

} catch (PDOException $e) {
    die("Error PDO: " . $e->getMessage());
} catch (Throwable $e) {
    die("Error general: " . $e->getMessage());
} 

$conex = null;   // Poderíamos non poñer nada



  • Versión procedimental
<?php

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

try {  // O try se pecha na sección catch do final
    $conex = mysqli_connect(SERVIDOR, USUARIO, PASSWORD, BD);
    mysqli_set_charset($conex,'utf8mb4');
?>
  ..............
  // Operacións contra a base de datos e amosar información ao usuario
  ..............

<?php
}   // Fin do try do principio da páxina
catch (mysqli_sql_exception $e) {
    die("Erro mysql: " . $e->getMessage());
}
catch (Expcetion $e) {
    die("Erro xeral: " . $e->getMessage());
}
finally{
  if(!empty($conex)) {
     mysqli_close($conex);
  }
}
?>
  • Versión orientada a obxectos
<?php
require ('/var/www/confBD/bd.php');

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

  ..............
  // Operacións contra a base de datos e amosar información ao usuario
  ..............

<?php
}     // Fin do try do principio da páxina
catch (mysqli_sql_exception $e) {
    die("Erro mysql: " . $e->getMessage());
}
catch (Expcetion $e) {
    die("Erro xeral: " . $e->getMessage());
}
finally{
  if(!empty($conex)){
     $conex->close();
  }
}
?>



Cambiando de base de datos activa

  • Cando realizamos unha conexión con PDO podemos indicar no DSN a base de datos coa que queremos traballar:
$dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
Neste caso, a base de datos seleccionada inicialmente será php.
  • Se durante a execución do programa necesitamos traballar con outra base de datos temos, entre outras, dúas posibilidades:
 :* Crear unha nova conexión PDO indicando a nova base de datos no DSN.
 :* Cambiar a base de datos activa da conexión actual mediante a sentenza SQL USE.

Crear unha nova conexión PDO

  • Podemos crear un novo obxecto PDO indicando outra base de datos no DSN:
<?php

try {

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

    // Conexión inicial á base de datos php
    $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';

    $conex = new PDO(
        $dsn,
        'user_php',
        'user_php',
        $opciones
    );

    // Creamos unha nova conexión á base de datos outra_db
    $dsn = 'mysql:host=localhost;dbname=outra_db;charset=utf8mb4';

    $conex = new PDO(
        $dsn,
        'user_php',
        'user_php',
        $opciones
    );

} catch (PDOException $e) {

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

} catch (Throwable $e) {

    die('Erro xeral: ' . $e->getMessage());

}
?>
Ao crear o segundo obxecto PDO estamos realizando unha nova conexión na que a base de datos seleccionada inicialmente será outra_db.

Cambiar a base de datos activa con USE

  • Se queremos manter a mesma conexión e cambiar a base de datos activa podemos executar a sentenza SQL USE:
<?php

try {

    $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';

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

    $conex = new PDO(
        $dsn,
        'user_php',
        'user_php',
        $opciones
    );

    // A base de datos activa inicialmente é php

    $conex->exec('USE outra_db');

    // A partir deste momento a base de datos activa é outra_db

} catch (PDOException $e) {

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

} catch (Throwable $e) {

    die('Erro xeral: ' . $e->getMessage());

}
?>
A sentenza USE cambia a base de datos predeterminada da conexión actual. Esta seguirá sendo a base de datos activa ata que se execute outra sentenza USE ou se peche a conexión.
O usuario empregado para realizar a conexión deberá ter os permisos necesarios sobre a nova base de datos.
  • Podemos comprobar cal é a base de datos activa empregando:
$stmt = $conex->query('SELECT DATABASE()');

echo $stmt->fetchColumn();
Neste caso, despois de executar:
$conex->exec('USE outra_db');
o código anterior devolvería:
outra_db

IMPORTANTE: O nome da base de datos empregado na sentenza `USE` non debería obterse directamente de datos introducidos polo usuario. Neste módulo empregaremos nomes de bases de datos definidos previamente no código.


Optimización e seguridade: Sentenzas preparadas

  • NOTA: A explicación de como se fan consultas ven no seguinte punto, pero adianto agora a explicación de como en PDO se empregan sentenzas prepared.


  • Unha sentenza preparada permite separar a instrución SQL dos datos que se van empregar nela.
Primeiro prepárase a sentenza SQL indicando mediante marcadores os valores que se proporcionarán posteriormente. Despois asócianse os datos a eses marcadores e execútase a sentenza.
Por exemplo:
SELECT col1
FROM TABOA1
WHERE col2 = ?
Neste caso a sentenza ten un parámetro, representado polo carácter ?. O valor correspondente proporcionarase posteriormente no momento de executar a sentenza.
Unha mesma sentenza preparada pode executarse varias veces empregando valores diferentes.
  • PDO permite utilizar dous tipos de marcadores:
  • Marcadores posicionais, representados mediante `?`:
SELECT nome, apelido
FROM alumnos
WHERE idade > ?
  • Marcadores con nome, precedidos por `:`:
SELECT nome, apelido
FROM alumnos
WHERE idade > :idade
Nos exemplos dos apuntes empregaremos preferentemente marcadores con nome, xa que normalmente fan que o código sexa máis lexible.

Vantaxes das sentenzas preparadas

  • As sentenzas preparadas presentan, entre outras, as seguintes vantaxes:
  • Rendemento: cando unha mesma sentenza se executa repetidamente con distintos valores, a base de datos pode reutilizar parte do traballo realizado durante a preparación da sentenza, como a análise e optimización da consulta. Polo tanto, poden resultar especialmente útiles cando unha mesma operación se executa moitas veces.
  • Seguridade: os datos enviados mediante parámetros son tratados como valores e non como parte da instrución SQL. Isto axuda a evitar ataques de inxección SQL.
IMPORTANTE: As sentenzas preparadas protexen os valores enviados mediante parámetros, pero isto non significa que calquera consulta sexa automaticamente segura.
Por exemplo, non debemos construír unha consulta concatenando directamente datos recibidos do usuario:
$sql = "SELECT * FROM alumnos WHERE nome = '" . $_POST['nome'] . "'";
A forma correcta será empregar un parámetro:
$sql = 'SELECT * FROM alumnos WHERE nome = :nome';

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

$stmt->execute([
'nome' => $_POST['nome']
]);
Os parámetros dunha sentenza preparada representan valores. Non poden utilizarse directamente para substituír nomes de táboas, nomes de columnas, palabras reservadas SQL ou outras partes estruturais dunha consulta.
Máis información:
Sentenzas preparadas con PDO

PHP empregando PDO

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.

  • Para preparar unha sentenza empregamos o método:
PDO::prepare()
  • Este método devolve un obxecto da clase `PDOStatement`, sobre o que posteriormente chamaremos ao método:
PDOStatement::execute()
Máis información:

Exemplo

  • Supoñamos que queremos inserir datos na táboa `alumnos`:
INSERT INTO alumnos (nome, apelido)
VALUES (:nome, :apelido)
  • O código PHP podería ser o seguinte:
 1 <?php
 2 
 3 $conex = null;
 4 
 5 try {
 6 
 7     $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
 8 
 9     $opciones = [
10         PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
11         PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
12         PDO::ATTR_EMULATE_PREPARES   => false
13     ];
14 
15     $conex = new PDO(
16         $dsn,
17         'user_php',
18         'user_php',
19         $opciones
20     );
21 
22     // Preparamos a sentenza
23     $sql = 'INSERT INTO alumnos (nome, apelido)
24             VALUES (:nome, :apelido)';
25 
26     $stmt = $conex->prepare($sql);
27 
28     // Indicamos os datos e executamos a sentenza
29     $nome = 'Ana';
30     $apelido = 'López';
31 
32     $stmt->execute([
33         'nome'    => $nome,
34         'apelido' => $apelido
35     ]);
36 
37 } catch (PDOException $e) {
38 
39     // Durante o desenvolvemento podemos amosar a mensaxe de erro.
40     // Nunha aplicación en produción non se debería mostrar ao usuario.
41     printf('Erro PDO: %s', $e->getMessage());
42 
43 } catch (Throwable $e) {
44 
45     printf('Erro xeral: %s', $e->getMessage());
46 
47 } finally {
48 
49     $conex = null;
50 
51 }
52 ?>

Executar varias veces a mesma sentenza

  • Unha das vantaxes das sentenzas preparadas é que podemos preparar a instrución unha vez e executala varias veces con valores diferentes.
$sql = 'INSERT INTO alumnos (nome, apelido)
VALUES (:nome, :apelido)';

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

// Primeiro alumno
$stmt->execute([
'nome'    => 'Ana',
'apelido' => 'López'
]);

// Segundo alumno
$stmt->execute([
'nome'    => 'Brais',
'apelido' => 'Pérez'
]);

// Terceiro alumno
$stmt->execute([
'nome'    => 'Lucía',
'apelido' => 'García'
]);
A sentenza SQL prepárase unha vez, pero execútase tres veces con valores diferentes.


  • Exercicio proposto: Intenta crear unha páxina PHP que engada á táboa 'autores' os seguintes datos:
  • Benito Pérez Galdós
  • Julio Verne




Solución exercicio:
<?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
    );

    echo '<br />Conecta';

    $sql = 'INSERT INTO autores (nome, apelidos)
            VALUES (:nome, :apelidos)';

    $stmt = $conex->prepare($sql);
    $stmt->execute(['nome'  => 'Benito', 'apelidos' => 'Pérez Galdós']);
    $stmt->execute(['nome'  => 'Julio', 'apelidos' => 'Verne']);

    echo '<br />Datos engadidos';

} catch (PDOException $e) {
    die("Error PDO: " . $e->getMessage());
} catch (Throwable $e) {
    die("Error general: " . $e->getMessage());
} 

$conex = null;   // Poderíamos non poñer nada



Asociar explicitamente os parámetros

  • Tamén podemos asociar os valores aos parámetros antes de executar a sentenza empregando, por exemplo, o método `bindValue()`.
$sql = 'SELECT nome, apelido
FROM alumnos
WHERE idade >= :idade';

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

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

$stmt->execute();
Neste exemplo indicamos explicitamente que o valor asociado ao parámetro `:idade` é de tipo enteiro mediante:
PDO::PARAM_INT
Para datos de tipo cadea podemos empregar:
PDO::PARAM_STR
Nos exemplos máis sinxelos tamén podemos enviar directamente os valores ao método `execute()`:
$stmt->execute([
'idade' => 18
]);


  • IMPORTANTE: Se enviamos os parámetros directamente mediante o array de execute(), PDO trátaos por defecto como cadeas (PDO::PARAM_STR). O xestor de base de datos poderá realizar as conversións de tipo necesarias segundo o contexto da consulta e o tipo das columnas.
Se queremos indicar explicitamente o tipo dun parámetro, podemos empregar bindValue() ou bindParam() xunto cunha constante como PDO::PARAM_INT ou PDO::PARAM_STR.
Pode darse o caso de que teñamos unha columna na base de datos definida como numérica o o valor enviado sexa cadea e o interprete de forma erronéa (por exemplo con Limit de SQL que espara un número)





  • Exercicio Proposto: Empregando esta forma de paso de parámetros (indicando o tipo) agrega dos novos libros tendo en conta que a columna id é autonumérica e non tes que enviar o dato.





MySQL

NOTA: O seguinte apartado explica como crear sentenzas preparadas directamente desde MySQL. Inclúese a nivel informativo. Nas operacións realizadas desde PHP nos exames empregaremos PDO.

Máis información:
Prepared Statements en MySQL
  • Desde o propio xestor MySQL tamén podemos crear e executar sentenzas preparadas mediante:
  • `PREPARE`
  • `EXECUTE`
  • `DEALLOCATE PREPARE`
  • Esta funcionalidade pode resultar útil, por exemplo, para construír SQL dinámico dentro dun procedemento almacenado.
As sentenzas `PREPARE`, `EXECUTE` e `DEALLOCATE PREPARE` poden empregarse dentro de procedementos almacenados, pero non dentro de funcións almacenadas ou triggers.

Exemplo de SQL dinámico

  • Supoñamos que queremos buscar autores empregando dous criterios opcionais:
  • Apelido.
  • Nome.
Dependendo dos datos recibidos, a condición podería ser:
WHERE apelido = ?
ou:
WHERE nome = ?
ou:
WHERE apelido = ?
AND nome = ?
Se non se indica ningún criterio, seleccionaranse todas as filas.
  • Podemos construír dinamicamente a sentenza mantendo os valores separados da instrución SQL:
DELIMITER $$

CREATE PROCEDURE proba(
IN _apelido VARCHAR(100),
IN _nome VARCHAR(50)
)
BEGIN

```
SET @cadea = 'SELECT nome, apelido
              FROM AUTORES
              WHERE 1 = 1';

-- Gardamos os valores en variables de usuario,
-- xa que EXECUTE ... USING traballa con este tipo de variables
SET @p_apelido = _apelido;
SET @p_nome = _nome;

IF _apelido IS NOT NULL THEN
    SET @cadea = CONCAT(
        @cadea,
        ' AND apelido = ?'
    );
END IF;

IF _nome IS NOT NULL THEN
    SET @cadea = CONCAT(
        @cadea,
        ' AND nome = ?'
    );
END IF;

PREPARE stmt FROM @cadea;

IF _apelido IS NOT NULL AND _nome IS NOT NULL THEN

    EXECUTE stmt USING @p_apelido, @p_nome;

ELSEIF _apelido IS NOT NULL THEN

    EXECUTE stmt USING @p_apelido;

ELSEIF _nome IS NOT NULL THEN

    EXECUTE stmt USING @p_nome;

ELSE

    EXECUTE stmt;

END IF;

DEALLOCATE PREPARE stmt;
```

END$$

DELIMITER ;
  • Neste exemplo:
  • `@cadea` almacena a sentenza SQL que se vai construíndo.
  • Os valores de `_apelido` e `_nome` non se concatenan directamente dentro da instrución SQL.
  • Cada valor que se necesita engádese mediante un marcador `?`.
  • `PREPARE` prepara a sentenza.
  • `EXECUTE ... USING` asocia os valores aos marcadores e executa a sentenza.
  • `DEALLOCATE PREPARE` libera a sentenza preparada cando xa non se necesita.
IMPORTANTE: É necesario ter en conta os espazos en branco ao construír dinamicamente a sentenza mediante `CONCAT()`. Por exemplo:
WHERE 1 = 1 AND apelido = ?
debe conter un espazo entre `1 = 1` e `AND`.



Operacións contra a base de datos




Permisos mínimos

  • Sempre debemos ter en mente a seguridade de acceso aos datos gardados nunha base de datos.
Ata o de agora, o usuario que usamos para acceder á base de datos tiña asinado todos os permisos sobre a mesma, pero esta práctica non é aconsellable.
O ideal é que teña asinado os permisos mínimos necesarios para que poida utilizar a aplicación.


  • Cando creamos unha aplicación WEB podemos ter varios supostos no que se refire ao acceso á base de datos.
  • Temos creado en MYSQL un usuario diferente por cada usuario, de tal forma que a cadea de conexión á base de datos a crearemos en base ao usuario/password introducido dende unha páxina PHP.
  • Utilizamos o mesmo usuario MYSQL para acceder á base de datos e temos unha táboa de usuario/password. En función do usuario validado, este terá acceso a diferentes páxinas PHP con diferentes funcionalidades.
  • Utilizamos o mesmo usuario MYSQL para acceder á base de datos e todos os usuarios do sitio web poden ter acceso ás mesmas páxinas.
  • En calquera destes supostos, o usuario que empreguemos para acceder a MYSQL debe ter os permisos mínimos para poder funcionar no noso sitio web.



  • Partimos de que temos creado un usuario en Mysql, que vai ser o usuario que imos utilizar no noso sitio web para conectarnos a MYSQL e queremos darlle os permisos mínimos.


  • CASO 1: A nosa aplicación fai as operacións contra a base de datos empregando sentenzas SQL (SELECT/INSERT/UPDATE/DELETE) deberemos outorgar os permisos e so os permisos necesarios para facer as operacións sobre as táboas da nosa base de datos.
A primeira (grant) serve para outorgar un permiso a un usuario.
A segunda (revoke) serve para quitar un permiso a un usuario.
  • No caso de SELECT e UPDATE, podemos outorgar permisos a nivel de columnas, de tal forma que podemos especificar que columnas dunha táboa vai ter permiso para poder ser seleccionadas ou actualizadas.


Se estamos a utilizar o WorkBench, podemos asociar os permisos graficamente.
Php Perm Minimos 1.jpg
  • Prememos na sección 'User and privileges' (1).
  • Prememos o botón 'Add entry' e escollemos o schema (base de datos) sobre a que queremos outorgar os permisos (2).
  • Aparecerá o schema na lista (3)^.
  • Agora na parte baixa aparecerán os permisos que podemos outorgar. Fixarse que os marcados con (4) son os permisos de SELECT/INSERT/UPDATE/DELETE
  • Se eliximos este 'tipo de permisos' estaremos dando o permiso sobre todas as táboas creadas na base de datos.


Se estamos a utilizar a ferramenta Phpmyadmin poderemos facer o mesmo seleccionado a base de datos e indo a opción Privilegios. Aparecerá o usuario creado e poderemos editar os seus permisos.
Drupal instalacion mysql 7.jpg



  • Se queremos dar un permiso individual a unha táboa usaremos a orde grant. Esta será a opción a escoller se queremos otorgar un permisos mínimos a nosa base de datos:
Nota: A orde grant, cando fai referencia a un obxecto (coma unha táboa) pode indicarse o nome da base de datos e o nome do obxecto desta forma: bd.obxecto
Nos exemplos seguintes omitimos o nome da base de datos e supoñemos que xa está seleccionada (activa) a base de datos sobre a que imos a executar as ordes.
Permiso 'select' sobre unha táboa.
1 grant select on TABOA to 'novo_usuario';
Permiso 'select' sobre unha columna dunha táboa.
1 grant select (columna) on TABOA to 'novo_usuario';
Permiso 'update' sobre unha táboa.
1 grant update on TABOA to 'novo_usuario';
Permiso 'update' sobre unha columna dunha táboa.
1 grant update (columna) on TABOA to 'novo_usuario';
Para os permisos DELETE e INSERT soamente é necesario poñer na orde grant a operación igual que nos casos anteriores.



  • CASO 2: A nosa aplicación fai as operacións contra a base de datos empregando procedementos almacenados.
  • No caso anterior, aínda que melloramos a seguridade, o usuario aínda ten acceso ás táboas directamente e pode ver a estrutura das mesmas.
Se queremos aumentar a seguridade podemos utilizar procedementos almacenados.


  • Xa vimos anteriormente que empregar ditos procedementos leva consigo:
  • Mellor rendemento xa que as sentenzas xa están compiladas e analizadas polo xestor.
  • Mellor seguridade xa que é imposible facer ataques de inxección de código.
  • Mellor seguridade xa que o usuario non pode ver a estrutura de táboas.
  • Mantemento máis doado, xa que podo cambiar a estrutura de táboas sempre que o procedemento continúe devolvendo o mesmo ao usuario.


  • IMPORTANTE: Se utilizamos este tipo de 'seguridade' xa non necesitamos outorgar ningún tipo de permiso sobre as táboas.
  • O permiso que debemos outorgar para que poida executar un procedemento almacenado é 'EXECUTE'.
  • Podemos facelo como no punto anterior facendo uso do WORKBENCH (neste caso daremos permiso de execución sobre todos os procedementos almacenados).
  • Usar a orde grant:
Dando permiso de execución a todos os procedementos almacenados.
1 grant execute on *.* to 'novo_usuario'
Dando permiso de execución a un procedemento almacenado.
1 grant execute on procedure NOME_PROCEDEMENTO to 'novo_usuario'

Gardando contrasinais de forma segura

  • En moitas aplicacións necesitamos gardar na base de datos información relacionada coa autenticación dos usuarios, como por exemplo os seus contrasinais.
  • Os contrasinais nunca deben gardarse directamente na base de datos en texto plano.
Se un atacante consegue acceder á base de datos podería coñecer inmediatamente os contrasinais de todos os usuarios.
  • Tampouco debemos empregar algoritmos de hash xenéricos como `MD5`, `SHA1` ou `SHA256` directamente para almacenar contrasinais.
Estes algoritmos están deseñados para calcular hashes de forma moi rápida, característica que facilita que un atacante poida realizar grandes cantidades de intentos mediante ataques de forza bruta.
  • Para gardar contrasinais PHP proporciona funcións específicas que empregan algoritmos deseñados para este propósito:

Recomendacións para os contrasinais

  • Actualmente considérase que a lonxitude do contrasinal é un dos factores máis importantes para aumentar a súa seguridade.
  • Como recomendación xeral:
  • Se o contrasinal é o único factor de autenticación, debería ter como mínimo 15 caracteres.
  • Se se emprega como parte dun sistema de autenticación multifactor (MFA), poderá admitirse unha lonxitude mínima de 8 caracteres.
  • Débense permitir contrasinais longos ou frases de paso (passphrases).
  • Recoméndase permitir unha lonxitude máxima de polo menos 64 caracteres.
  • Non é recomendable obrigar ao usuario a incluír necesariamente maiúsculas, minúsculas, números e caracteres especiais.
  • Débese evitar o emprego de contrasinais moi coñecidos, previsibles ou que apareceran en filtracións de datos.
Por exemplo, unha frase de paso longa pode ser máis fácil de lembrar e proporcionar maior seguridade que un contrasinal curto construído unicamente para cumprir regras de complexidade.
Máis información:

Función password_hash()

  • Para gardar un contrasinal na base de datos empregaremos a función:
password_hash()
  • Por exemplo:
 1 <?php
 2 
 3 $password = 'contrasinal_introducido_polo_usuario';
 4 
 5 $hash = password_hash(
 6     $password,
 7     PASSWORD_DEFAULT
 8 );
 9 
10 echo $hash;
11 
12 ?>
  • A constante:
PASSWORD_DEFAULT
indica que PHP empregará o algoritmo considerado predeterminado para almacenar contrasinais.
En PHP 8.5, `PASSWORD_DEFAULT` emprega actualmente bcrypt. Non obstante, esta constante está deseñada para que PHP poida cambiar o algoritmo empregado en futuras versións se aparece unha alternativa máis adecuada.
Por este motivo, a lonxitude do hash podería cambiar no futuro e recoméndase gardalo nun campo suficientemente grande da base de datos, por exemplo:
password VARCHAR(255)

O salt

  • Os algoritmos de almacenamento de contrasinais empregan un valor aleatorio denominado salt.
O salt fai, entre outras cousas, que dous usuarios que teñan o mesmo contrasinal obteñan normalmente hashes diferentes.
Tamén dificulta o emprego de táboas precalculadas de hashes, como as chamadas Rainbow Tables.
  • Non debemos crear nin proporcionar manualmente o salt.
A función `password_hash()` xera automaticamente un salt seguro e diferente para cada contrasinal.
$hash = password_hash(
$password,
PASSWORD_DEFAULT
);
O algoritmo empregado, os seus parámetros e o salt necesario para comprobar posteriormente o contrasinal están incluídos dentro do propio hash devolto por `password_hash()`.
Polo tanto, non é necesario gardar o salt nun campo separado da base de datos.
IMPORTANTE: En versións antigas de PHP era posible proporcionar manualmente un salt a `password_hash()`. Esta opción está obsoleta desde PHP 7 e foi eliminada en PHP 8.0. Non debe empregarse.

Gardar o contrasinal na base de datos

  • Supoñamos que recibimos o contrasinal desde un formulario:
$password = $_POST['password'];
  • Primeiro calcularemos o hash:
$hash = password_hash(
$password,
PASSWORD_DEFAULT
);
  • O valor que gardaremos na base de datos será $hash e nunca o contrasinal orixinal.
  • Por exemplo, empregando PDO e unha sentenza preparada:
 1 $sql = 'INSERT INTO usuarios (nome, password)
 2 VALUES (:nome, :password)';
 3 
 4 $stmt = $conex->prepare($sql);
 5 
 6 $hash = password_hash(
 7 $_POST['password'],
 8 PASSWORD_DEFAULT
 9 );
10 
11 $stmt->execute([
12 'nome'     => $_POST['nome'],
13 'password' => $hash
14 ]);

Comprobar un contrasinal

  • Cando un usuario inicia sesión non se calcula manualmente o hash do contrasinal para comparalo co gardado.
  • Para comprobar se o contrasinal introducido corresponde co hash almacenado na base de datos empregaremos:
password_verify()
  • Por exemplo:
 1 <?php
 2 
 3 $passwordIntroducido = $_POST['password'];
 4 
 5 // Este valor obteríase previamente da base de datos
 6 $hashGardado = $usuario['password'];
 7 
 8 if (password_verify($passwordIntroducido, $hashGardado)) {
 9 
10     echo 'Contrasinal correcto';
11 
12 } else {
13 
14     echo 'Contrasinal incorrecto';
15 
16 }
17 
18 ?>
  • `password_verify()` recibe:
  1. O contrasinal introducido polo usuario.
  2. O hash que temos gardado na base de datos.
A función devolve `true` se o contrasinal se corresponde co hash almacenado ou `false` en caso contrario.
Toda a información necesaria para realizar a comprobación, incluído o algoritmo, os seus parámetros e o salt, está incluída no propio hash.

Actualización do hash

  • Os algoritmos ou os seus parámetros poden cambiar co paso do tempo.
PHP proporciona a función:
password_needs_rehash()
que permite comprobar se un hash almacenado debería volver calcularse utilizando a configuración actual.
  • Podemos facer esta comprobación despois de validar correctamente o contrasinal:
 1 if (password_verify($passwordIntroducido, $hashGardado)) {
 2 
 3 ```
 4 // O contrasinal é correcto
 5 
 6 if (password_needs_rehash(
 7     $hashGardado,
 8     PASSWORD_DEFAULT
 9 )) {
10 
11     $novoHash = password_hash(
12         $passwordIntroducido,
13         PASSWORD_DEFAULT
14     );
15 
16     // Actualizar $novoHash na base de datos
17 }
18 
19 // Continuar co proceso de autenticación
20 ```
21 
22 }
Desta forma podemos actualizar progresivamente os hashes almacenados cando cambie o algoritmo predeterminado de PHP ou os seus parámetros.

Hash e cifrado non son o mesmo

  • Un hash é unha transformación deseñada para non poder obter novamente o valor orixinal.
Por este motivo é adecuada para almacenar contrasinais: non necesitamos coñecer o contrasinal orixinal, unicamente comprobar se o introducido polo usuario é correcto.
  • Se necesitamos gardar un dato confidencial pero posteriormente temos que recuperar o seu valor orixinal, non debemos empregar `password_hash()`.
Nese caso necesitaríamos empregar técnicas de cifrado adecuadas para protexer os datos.

IMPORTANTE:

  • Para almacenar contrasinais empregaremos `password_hash()`.
  • Para comprobar contrasinais empregaremos `password_verify()`.
  • Non empregaremos `MD5`, `SHA1` nin hashes xenéricos para almacenar contrasinais.
  • Non xeraremos nin gardaremos manualmente o salt.
  • Na base de datos gardaremos unicamente o hash xerado por `password_hash()`.
  • Recoméndase utilizar un campo como `VARCHAR(255)` para almacenar o hash.


Subindo unha imaxe ao servidor

  • Lembrar que normalmente o que subimos á base de datos é a ruta 'relativa' onde se gardan as imaxes no servidor.






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