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

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 39 ediciones intermedias del mismo usuario)
Línea 1: Línea 1:
 
== Introdución ==
 
== Introdución ==
  
−
* '''<u>Aclaración importante:</u>''' A partires da versión 8.1 de PHP, as ordes que imos ver a continuación lanzan excepcións. Os exercicios resoltos están feitos con dita versión. Cando [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Operaci%C3%B3ns_BD_Mysql#Visualizaci.C3.B3n_de_errores cheguedes a sección de Visualización de Errores] se indicarán as dúas formas de xestionar os erros.
+
'''<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.
  
  
Línea 23: 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 37: 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:
 
−
 
 
−
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:
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
mysqli::close();
 
−
</syntaxhighlight>
 
−
 
 
−
 
 
−
* Aquí tedes un cadro comparativo obtido de php.net onde se ve o comentado anteriormente:
 
−
 
 
−
[[Imagen:Php_BD_2.jpg|600px|center]]
 
−
 
 
−
 
 
−
 
 
−
<u>Nota:</u> Máis información [http://php.net/manual/es/mysqli.overview.php neste enlace].
 
−
 
 
−
== Activar a extensión mysqli==
 
  
 
* Se instalamos XAMPP ou calquera das versións para outros S.O. normalmente a extensión xa ven habilitada por defecto.
 
* 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.
 
: 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.
  
 
<br />
 
<br />
Línea 93: Línea 82:
 
extension_dir = "ext"
 
extension_dir = "ext"
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
:: 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.
  
  
Línea 244: Línea 234:
 
<br />
 
<br />
  
−
== Visualización de errores ==
+
== Visualización de erros ==
  
−
* Ao principio ides cometer moitos erros durante as probas de programación.
+
* Ao principio é habitual cometer erros durante as probas de programación.
−
: PHP non vai a amosar nada e simplemente vai amosar unha páxina en branco.
+
  : 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''.
  
−
* Para saber onde está o erro, pódense cambiar as directivas '''display_errors''' e '''error_reporting''' do php.ini, pero se non queredes cambiar ditos arquivos, podedes poñer ao principio de cada páxina PHP o seguinte:
+
: 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:
−
:<syntaxhighlight lang="java" enclose="div" highlight="" >
+
 
 +
:<syntaxhighlight lang="php" enclose="div">
 
ini_set('display_errors', 1);
 
ini_set('display_errors', 1);
 
ini_set('display_startup_errors', 1);
 
ini_set('display_startup_errors', 1);
−
error_reporting(E_ALL);
+
error_reporting(E_ALL); </syntaxhighlight>
−
</syntaxhighlight>
+
 
 +
: Desta forma PHP mostrará os erros, avisos e advertencias que se produzan durante a execución do programa.
 +
 
 +
: '''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>
 +
 
 +
: 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.
 +
 
 +
[[Imagen:Mysql_visualuzar_erros_1.png|400px|center]]
 +
 
 +
 
  
  
−
: '''IMPORTANTE:'''<u>Lembrar quitar ditas liñas cando rematedes de programar a páxina.</u>
+
* <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].
  
  
  
 +
* 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)
  
 
<br />
 
<br />
Línea 270: Línea 282:
 
:* O xestor de bases de datos Mysql
 
:* 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 [http://blog.unreal4u.com/2012/08/sobre-collation-y-charset-en-mysql/ neste enlace].
 
:* 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].
−
:* 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'''.
+
:* 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 [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Requirimentos_previos_para_acceso_a_datos punto anterior da wiki].
 
: 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:'''
 +
 +
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'''.
 +
 +
 +
 +
<br />
 
=== Apertura da conexión ===
 
=== Apertura da conexión ===
 +
 +
::* '''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.
 +
 +
 +
 +
: Un exemplo:
 +
:::<syntaxhighlight lang="php enclose="div" highlight="" >
 +
$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);
 +
 +
</syntaxhighlight>
 +
 +
:: 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].
 +
:* Neste caso modificamos o atributo ATTR_ERRMODE e damos o valor ERRMODE_EXCEPTION.
 +
:: O que indica dito atributo é o seguinte:
 +
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 +
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.
 +
</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.
 +
 +
 +
<br />
 +
:: 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>
 +
 +
 +
 +
 +
<br />
 +
'''NOTA:''' O indicado a continuación inclúese a nivel informativo. Nos exames empregarase PDO.
 +
 
* Utilizando procedementos: [https://secure.php.net/manual/es/function.mysqli-connect.php mysqli_connect]
 
* 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]
 
* Utilizando obxectos: [https://secure.php.net/manual/es/mysqli.construct.php mysql::_construct]
Línea 281: Línea 372:
 
* Sintaxe (se indica a sintaxe do construtor e polo tanto será equivalente a facer un 'new' da clase mysqli):  
 
* Sintaxe (se indica a sintaxe do construtor e polo tanto será equivalente a facer un 'new' da clase mysqli):  
  
−
::* Orientada a obxectos:
+
::* '''Orientada a obxectos''':
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
:::<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") ]]]]]] )
 
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>  
  
−
::* Por procedementos:
+
::* '''Por procedementos''':
−
:::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
:::<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") ]]]]]] )
 
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>  
 
</syntaxhighlight>  
Línea 300: Línea 391:
 
:* Porto (numérico): Porto no que está escoitando o xestor de base de datos. No caso de mysql é o 3306.
 
:* 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.
 
:* 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.
 +
 +
 +
<br />
  
  
 
* Aplicado ao noso caso, teremos a seguinte orde:
 
* Aplicado ao noso caso, teremos a seguinte orde:
 
:* Por procedementos:
 
:* Por procedementos:
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
:::<syntaxhighlight lang="php" line enclose="div" highlight="" >
 
$conex = mysqli_connect('localhost','user_php','user_php','php');  
 
$conex = mysqli_connect('localhost','user_php','user_php','php');  
 
</syntaxhighlight>  
 
</syntaxhighlight>  
Línea 310: Línea 404:
  
 
:* Orientado a obxectos:
 
:* Orientado a obxectos:
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
:::<syntaxhighlight lang="php" line enclose="div" highlight="" >
 
$conex = new mysqli('localhost','user_php','user_php','php');  
 
$conex = new mysqli('localhost','user_php','user_php','php');  
 
</syntaxhighlight>  
 
</syntaxhighlight>  
 +
 +
:: $conex é un obxecto que represente a conexión á base de datos.
  
  
−
* En caso de erro, $conex é un obxecto que represente a conexión á base de datos.
 
  
  
Línea 321: Línea 416:
 
==== Control de errores ====
 
==== 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:
+
* '''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 por procedemento teremos que chamar á función [http://www.w3schools.com/php/func_mysqli_connect_error.asp mysqli_connect_error()] desta forma:
+
:* Versión con chamada empregando pdo:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
:::<syntaxhighlight lang="php" enclose="div" highlight="" >
 
try {
 
try {
−
    $conex = mysqli_connect('localhost', 'usuario_inexistente', 'contrasena_incorrecta', 'base_de_datos');
+
  $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
−
} catch (mysqli_sql_exception $e) {
+
 
−
     die("Erro de conexión: " . $e->getMessage());
+
  $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());
 
}
 
}
−
</syntaxhighlight>
 
  
−
:* Versión con chamada empregando obxectos:
+
</syntaxhighlight>
−
::<syntaxhighlight lang="java" line 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());
 
−
}
 
−
</syntaxhighlight>
 
  
 
:<u>Nota:</u>  
 
:<u>Nota:</u>  
−
:* 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 para o administrador e que poida ver cal foi o erro.  
+
:* 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
+
:* 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.
−
:: 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
 +
    ];
  
−
<br />
+
     $conex = new PDO($dsn, $usuario, $password, $opciones);
−
* Por exemplo, imos modificar o password do usuario por un que non exista e veremos o resultado:
+
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
+
} catch (PDOException $e) {
−
try {
 
−
     $conex = mysqli_connect('localhost','user_php','password_incorrecta','php');
 
−
} catch (mysqli_sql_exception $e) {
 
 
     die("Erro de conexión: (" . $e->getCode() . ') ' . $e->getMessage());
 
     die("Erro de conexión: (" . $e->getCode() . ') ' . $e->getMessage());
 
}
 
}
−
</syntaxhighlight>  
+
} 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.
  
 
: Dará como resultado:
 
: Dará como resultado:
 
[[Imagen:Php_BD_3.jpg|400px|center]]
 
[[Imagen:Php_BD_3.jpg|400px|center]]
 +
  
  
  
 
<br />
 
<br />
−
* '''En versións de PHP inferior á 8.1'''
+
'''NOTA:''' O indicado a continuación inclúese a nivel informativo. Nos exames empregarase PDO.
−
:* En caso de ter unha versión PHP anterior á PHP 5.2.9 e 5.3.0 ou estamos a utilizar a versión de chamadas por procedementos, teremos que chamar á función [http://www.w3schools.com/php/func_mysqli_connect_error.asp mysqli_connect_error()] desta forma:
+
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
+
:* Por procedementos:
−
$conex = mysqli_connect('localhost','user_php','user_php','php');  
+
::<syntaxhighlight lang="php" enclose="div" highlight="" >
−
if (mysqli_connect_error()){
+
try {
−
  die('Erro de Conexión (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
+
    $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());
 
}
 
}
 +
 
</syntaxhighlight>  
 
</syntaxhighlight>  
  
  
−
:* En caso de ter unha versión PHP posterior á PHP 5.2.9 e 5.3.0 e estar utilizando a forma orientada a obxectos:
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
+
:* Versión con chamada empregando obxectos:
−
$conex = new mysqli('localhost','user_php','user_php','php');  
+
::<syntaxhighlight lang="php" enclose="div" highlight="" >
−
if ($conex->connect_error) {
+
try {
−
  die('Erro de Conexión (' . $conex->connect_errno . ') ' . $conex->connect_error);
+
    $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>  
 
</syntaxhighlight>  
−
 
  
 
<br />
 
<br />
−
: Como vemos mostramos 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.
+
* Por exemplo, imos modificar o password do usuario por un que non exista e veremos o resultado:
−
: 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].
+
::<syntaxhighlight lang="php" line enclose="div" highlight="2" >
−
:<u>Nota:</u> 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 para o administrador e que poida ver cal foi o erro.
+
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());
 +
}
  
 +
</syntaxhighlight>
  
  
Línea 405: Línea 546:
 
* O primeiro punto, definir en forma de constante e nun arquivo de include os datos da conexión:
 
* O primeiro punto, definir en forma de constante e nun arquivo de include os datos da conexión:
  
−
'''Arquivo: bd.inc.php''':
+
'''Arquivo: bd.php''':
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
 
<?php
 
<?php
  
Línea 426: Línea 567:
  
 
:Se facemos un echo destas variables dentro dunha páxina do noso sitio web, obteremos unha ruta, coma por exemplo:
 
: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="" >
+
::<syntaxhighlight lang="php" line enclose="div" highlight="" >
 
/var/www/html/
 
/var/www/html/
 
</syntaxhighlight>  
 
</syntaxhighlight>  
Línea 448: Línea 589:
 
:* Agora, calquera páxina que faga uso destes datos terá que facer o seguinte require:
 
:* Agora, calquera páxina que faga uso destes datos terá que facer o seguinte require:
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="6" >
+
:* No caso de PDO:
 +
::<syntaxhighlight lang="php" line enclose="div" highlight="3,7,8" >
 +
<?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
 +
    );
 +
 
 +
} catch (PDOException $e) {
 +
    die('Erro de conexión coa base de datos.');
 +
}
 +
</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
 
<?php
  
Línea 454: Línea 633:
  
  
−
require ('/var/www/confBD/bd.inc.php');
+
require ('/var/www/confBD/bd.php');
 
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
 
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
  
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
  
  
 
* Se queremos facer o mesmo pero sen utilizar rutas absolutas (por exemplo se estamos nun aloxamento compartido):
 
* Se queremos facer o mesmo pero sen utilizar rutas absolutas (por exemplo se estamos nun aloxamento compartido):
−
::<syntaxhighlight lang="java" line enclose="div" highlight="6-7" >
+
::<syntaxhighlight lang="php" line enclose="div" highlight="6-7" >
 
<?php
 
<?php
−
 
−
  //error_reporting(0);  // Inabilita os erros de Apache se temos activada a directiva display_erros no php.ini
 
−
 
  
 
   $ruta = substr($_SERVER['DOCUMENT_ROOT'],0,strpos($_SERVER['DOCUMENT_ROOT'],'html')) . 'confBD'; // RUTA al sitio web sin /html pero con /confBD
 
   $ruta = substr($_SERVER['DOCUMENT_ROOT'],0,strpos($_SERVER['DOCUMENT_ROOT'],'html')) . 'confBD'; // RUTA al sitio web sin /html pero con /confBD
−
   require ($ruta.'/bd.inc.php');
+
   require ($ruta.'/bd.php');
  
−
  $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
</syntaxhighlight>
  
−
</syntaxhighlight>
 
  
 +
<br />
 
===O xogo de caracteres===
 
===O xogo de caracteres===
  
Línea 495: Línea 672:
  
 
::: No caso que nos ocupa teremos que poñer:
 
::: No caso que nos ocupa teremos que poñer:
−
:::* Versións inferiores a Html 5: <meta http-equiv="Content-Type" content="text/html; <b>charset=UTF-8"</b> />
+
:::* Html 5 e posteriores: <meta <b>charset="UTF-8"</b>>
−
:::* Html 5: <meta <b>charset="UTF-8"</b>>
 
  
 
::: Máis información en [http://www.w3schools.com/html/html_charset.asp w3schools].
 
::: Máis información en [http://www.w3schools.com/html/html_charset.asp w3schools].
  
 
:* No código PHP ao conectar coa base de datos.
 
:* 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
 +
    ];
 +
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        $opciones
 +
    );
 +
 +
</syntaxhighlight>
 +
 +
* '''É 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 [https://secure.php.net/manual/es/mysqli.set-charset.php función set_charset].
 
:: 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.
 
:: En caso de éxito devolve true ou false en caso de existir algún problema.
  
−
::: Cando conectemos coa base de datos teremos que indicarlle que imos utilizar o xogo de caracteres utf8mb4 con esta liña:
+
::::<syntaxhighlight lang="php" line enclose="div" highlight="4" >
−
:::* Versión procedimental
 
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="3" >
 
 
try {
 
try {
−
     $conex = mysqli_connect('localhost', 'user_php', 'user_php', 'php');
+
    require ('/var/www/confBD/bd.php');
 +
     $conex = mysqli_connect(SERVIDOR, USUARIO, PASSWORD, BD);
 
     mysqli_set_charset($conex,'utf8mb4');
 
     mysqli_set_charset($conex,'utf8mb4');
 
} catch (mysqli_sql_exception $e) {
 
} catch (mysqli_sql_exception $e) {
Línea 516: Línea 725:
  
  
−
:::* Versión orientada a obxectos
+
:::* Versión orientada a obxectos:
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="3" >
+
::::<syntaxhighlight lang="php" line enclose="div" highlight="4" >
 
try {
 
try {
−
     $conex = new mysqli('localhost', 'user_php', 'user_php', 'php');
+
    require ('/var/www/confBD/bd.php');
 +
     $conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
 
     $conex->set_charset('utf8mb4');
 
     $conex->set_charset('utf8mb4');
 
} catch (mysqli_sql_exception $e) {
 
} catch (mysqli_sql_exception $e) {
Línea 530: Línea 740:
  
  
−
* '''E FUNDAMENTAL QUE NOS TRES SITIOS ESTEA POSTO O MESMO TIPO DO XOGO DE CARACTERES'''.
+
<br />
−
 
 
 
===Peche da conexión===
 
===Peche da conexión===
  
 
* Despois de facer as diferentes operacións sobre a base de datos, teremos que pechar a 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].
+
: 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.
 +
 
 +
 
 +
:* '''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.
 +
 
 +
::::<syntaxhighlight lang="php" enclose="div" highlight="36" >
 +
<?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
 +
 
 +
 
 +
</syntaxhighlight>
 +
 
 +
 
  
−
<u>Nota:</u> 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 procedimental
 
:::* Versión procedimental
−
::::<syntaxhighlight lang="java" enclose="div" highlight="19-21" >
+
::::<syntaxhighlight lang="php" enclose="div" highlight="21-25" >
 
<?php
 
<?php
 +
 +
require ('/var/www/confBD/bd.php');
 +
 
try {  // O try se pecha na sección catch do final
 
try {  // O try se pecha na sección catch do final
−
     $conex = mysqli_connect('localhost', 'user_php', 'user_php', 'php');
+
     $conex = mysqli_connect(SERVIDOR, USUARIO, PASSWORD, BD);
 
     mysqli_set_charset($conex,'utf8mb4');
 
     mysqli_set_charset($conex,'utf8mb4');
 
?>
 
?>
Línea 567: Línea 830:
  
 
:::* Versión orientada a obxectos
 
:::* Versión orientada a obxectos
−
::::<syntaxhighlight lang="java" enclose="div" highlight="19-21" >
+
::::<syntaxhighlight lang="php" enclose="div" highlight="21-25" >
 
<?php
 
<?php
 +
require ('/var/www/confBD/bd.php');
 +
 
try {
 
try {
−
     $conex = new mysqli('localhost', 'user_php', 'user_php', 'php');
+
     $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD, BD);
 
     $conex->set_charset('utf8mb4');
 
     $conex->set_charset('utf8mb4');
 
?>
 
?>
Línea 594: Línea 859:
  
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
 +
 +
<br />
  
 
== Cambiando de base de datos activa ==
 
== Cambiando de base de datos activa ==
  
−
* Cando creamos a cadea de conexión, un dos parámetros que enviamos foi a base de datos na que iamos traballar.
+
* Cando realizamos unha conexión con PDO podemos indicar no '''DSN''' a base de datos coa que queremos traballar:
 +
 
 +
:<syntaxhighlight lang="php">
 +
$dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4'; </syntaxhighlight>
 +
 
 +
: 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 non enviar ese parámetro ou querer cambiar de base de datos dentro dunha páxina php.
+
* Podemos crear un novo obxecto PDO indicando outra base de datos no DSN:
  
−
: Se queremos escoller outra diferente teremos que facer uso da [http://php.net/manual/es/mysqli.select-db.php función mysqli::select_db].
+
:<syntaxhighlight lang="php">
  
−
: Por exemplo:
 
−
:* Versión procedimental:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="3,5" >
 
 
<?php
 
<?php
 +
 
try {
 
try {
−
    $conex = mysqli_connect('localhost', 'user_php', 'user_php');  // Non indicamos o nome da bd
 
−
    mysqli_set_charset($conex,'utf8mb4');
 
−
    mysqli_select_db($conex, "php");  // O segundo parámetro indica o nome da base de datos
 
  
−
?>
+
    $opciones = [
−
   ..............
+
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
  // Operacións contra a base de datos e amosar información ao usuario
+
        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());
  
−
<?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);
 
−
  }
 
 
}
 
}
 
?>
 
?>
  
 +
</syntaxhighlight>
 +
 +
: 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 ===
  
−
</syntaxhighlight>
+
* Se queremos manter a mesma conexión e cambiar a base de datos activa podemos executar a sentenza SQL '''USE''':
  
 +
:<syntaxhighlight lang="php">
  
−
<br />
 
−
:* Versión orientada a obxectos:
 
−
::<syntaxhighlight lang="java" enclose="div" highlight="3,5" >
 
 
<?php
 
<?php
 +
 
try {
 
try {
−
    $conex = new mysqli('localhost', 'user_php', 'user_php');  // Non indicamos o nome da bd
 
−
    $conex->set_charset('utf8mb4');
 
−
    $conex->select_db('php'); // O parámetro indica o nome da base de datos
 
  
−
?>
+
    $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());
−
  // 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();
 
−
  }
 
 
}
 
}
 
?>
 
?>
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
: 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.
  
−
<br />
+
: O usuario empregado para realizar a conexión deberá ter os permisos necesarios sobre a nova base de datos.
−
== Operacións contra a base de datos ==
 
  
−
* [[PHP_UD3_OP_CONSULTA | Operacións de consulta]]
+
* Podemos comprobar cal é a base de datos activa empregando:
−
* [[PHP_UD3_OP_ALTA | Operacións de alta]]
 
−
* [[PHP_UD3_OP_BAIXA | Operacións de baixa]]
 
−
* [[PHP_UD3_OP_MODIFICAR | Operacións de modificación]]
 
−
* [[PHP_UD3_EXERCICIOS_PROPOSTOS | Exercicios propostos]]
 
  
 +
:<syntaxhighlight lang="php">
 +
$stmt = $conex->query('SELECT DATABASE()');
  
−
<br />
+
echo $stmt->fetchColumn(); </syntaxhighlight>
  
−
== Transaccións ==
+
: Neste caso, despois de executar:
  
−
* En certas ocasións imos necesitar facer varias operacións que conleven modificacións nas táboas da base de datos (operacións de INSERT, UPDATE, DELETE). Se queremos que nunha páxina php se <u>fagan varias modificacións</u> e queremos que se hai calquera problema nunha delas, deixe o estado da base de datos xusto a como estaba antes de facer a primeira operación, necesitamos facer uso das '''transaccións'''.
+
:<syntaxhighlight lang="php">
 +
$conex->exec('USE outra_db'); </syntaxhighlight>
  
 +
: o código anterior devolvería:
  
−
<br />
+
:<syntaxhighlight lang="text">
−
* Por exemplo, no exemplo que estamos a desenvolver:
+
outra_db </syntaxhighlight>
−
:* Podemos crear unha páxina PHP cun formulario para dar de alta un libro xunto cos exemplares.
 
−
:* Podemos crear unha páxina PHP que relacione libros xunto cos autores que o escribiron.
 
−
:* Podemos dar de baixa un libro. Isto leva consigo dar de baixa o libro máis os exemplares máis os datos da táboas LIBROS_AUTORES.
 
  
−
* Outro caso típico é o carrito da compra. Cando o usuario remata o proceso de compra e procede a realizar o pedido, necesitaremos gardar os produtos que conforman o pedido na nosa base de datos.
+
'''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.
−
: Queremos que se garden todos os produtos.
 
  
 +
<br />
  
−
: Normalmente imos queres que ditas operacións se fagan todas e en caso de erro, necesitaremos volver ao estado anterior ao comezo do inicio das operacións.
+
== Optimización e seguridade: Sentenzas preparadas ==
  
−
* Isto o podemos facer grazas as '''TRANSACCIÓNS'''.
+
* '''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 transacción nos garante que o conxunto de operacións que están dentro dunha transacción se farán todas ou non se fará ningunha.
 
  
−
* Normalmente cada operación individual é 'considerada' coma unha transacción en si mesma, de tal forma o ou ten éxito ou os cambios non se gardan na base de datos.
 
  
−
* Máis información sobre Transaccións:
 
−
:* [https://es.wikipedia.org/wiki/Transacci%C3%B3n_(base_de_datos) Definición Wikipedia].
 
−
:* [https://es.wikipedia.org/wiki/ACID Características dunha transacción].
 
−
:* [https://dev.mysql.com/doc/refman/5.7/en/commit.html Transaccións en Mysql].
 
  
 +
* 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:
  
−
* Cando falamos de transaccións outro factor a ter en conta é o chamado '''Nivel de illamento'''.
+
::<syntaxhighlight lang="sql" enclose="div">
−
: Imaxinemos que estamos dentro dunha transacción e estou lendo o número de filas dunha táboa. Se outro usuario engade unha nova fila despois de que eu lera o número, e eu dentro da transacción volvo a ler o número de filas. Terei dúas cantidades diferentes dentro da mesma transacción.
+
SELECT col1
−
: Para evitalo podemos facer que a táboa que estou a utilizar dentro da transacción quede 'bloqueada' e non se poida modificar ata que eu remate.
+
FROM TABOA1
−
: Este e outros problemas que poden darse no uso de transaccións se intentan evitar cun nivel de illamento determinado.
+
WHERE col2 = ? </syntaxhighlight>
−
: Cada nivel de illamento soluciona algún dos problemas que se poden dar.
 
  
−
* Máis información sobre Niveis de illamento en Mysql:
+
: Neste caso a sentenza ten un parámetro, representado polo carácter '''?'''. O valor correspondente proporcionarase posteriormente no momento de executar a sentenza.
−
:* [http://download.nust.na/pub6/mysql/doc/refman/5.0/es/innodb-transaction-isolation.html Niveis de illamento en Mysql].
 
−
:* [http://recetasinformaticas.blogspot.com.es/2007/09/mysql-niveles-de-aislamiento-isolations.html Exemplos de problemas no uso de transaccións].
 
  
 +
: Unha mesma sentenza preparada pode executarse varias veces empregando valores diferentes.
  
 +
* PDO permite utilizar dous tipos de marcadores:
  
 +
:* '''Marcadores posicionais''', representados mediante `?`:
  
−
* Dos niveis de illamento (que se basean en bloquear rexistros de táboas) se pode provocar outro problema, que é o '''Interbloqueo''' ou '''Deadlock'''.
+
::<syntaxhighlight lang="sql" enclose="div">
−
: Isto prodúcese cando unha transacción_1 está esperando o acceso a un recurso que está sendo bloqueado por outra transacción_2 e esta outra transacción_2 está esperando por un recurso que está sendo bloqueado pola transacción_1.
+
SELECT nome, apelido
 +
FROM alumnos
 +
WHERE idade > ? </syntaxhighlight>
  
−
* Máis información sobre os DeadLock [http://mysql.localhost.net.ar/doc/refman/5.0/es/innodb-deadlocks.html neste enlace].
+
:* '''Marcadores con nome''', precedidos por `:`:
  
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
SELECT nome, apelido
 +
FROM alumnos
 +
WHERE idade > :idade </syntaxhighlight>
  
 +
: Nos exemplos dos apuntes empregaremos preferentemente '''marcadores con nome''', xa que normalmente fan que o código sexa máis lexible.
  
−
* En PHP, para poder utilizar transaccións necesitamos:
+
=== Vantaxes das sentenzas preparadas ===
−
:* MySQL 5.6 ou superior
 
−
:* Un motor de base de datos que soporte transaccións, coma [https://es.wikipedia.org/wiki/InnoDB motor InnoDB].
 
−
 
  
 +
* As sentenzas preparadas presentan, entre outras, as seguintes vantaxes:
  
−
* Unha transacción a podemos facer dende a páxina PHP ou dende o xestor Mysql dentro dun procedemento almacenado.
+
:* '''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.
  
−
* Para realizar unha transacción o proceso sempre é o mesmo en todas as linguaxes e xestores.
+
:* '''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'''.
−
:# Iniciar a transacción. Normalmente farase coa orde 'START TRANSACTION' (outros xestores teñen coma orde BEGIN TRANSACTION)
 
−
:# Realizar as operacións que conforman as transaccións.
 
−
:# En caso de erro en algunha das operacións anteriores executar a orde 'ROLLBACK' e saír
 
−
:# En caso de que todo se execute correctamente, entón confirmamos a transacción e os cambios se gardan definitivamente na base de datos, coa orde: 'COMMIT'
 
  
 +
: '''IMPORTANTE:''' As sentenzas preparadas protexen os '''valores enviados mediante parámetros''', pero isto non significa que calquera consulta sexa automaticamente segura.
  
−
=== Transaccións en PHP ===
+
: Por exemplo, non debemos construír unha consulta concatenando directamente datos recibidos do usuario:
  
−
* Para iniciar unha transacción temos dúas opcións:
+
::<syntaxhighlight lang="php" enclose="div">
 +
$sql = "SELECT * FROM alumnos WHERE nome = '" . $_POST['nome'] . "'"; </syntaxhighlight>
  
−
:* Inhabilitar as transaccións implícitas, coa [http://php.net/manual/es/mysqli.autocommit.php función autocommit]:
+
: A forma correcta será empregar un parámetro:
−
::* Versión procedimental:
 
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
 
−
$conex = mysqli_connect(HOST, USER, PASSWORD, BD);
 
−
mysqli_autocommit($conex,false);
 
−
</syntaxhighlight>
 
  
−
::* Versión obxectual:
+
::<syntaxhighlight lang="php" enclose="div">
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
+
$sql = 'SELECT * FROM alumnos WHERE nome = :nome';
−
$conex= new mysqli(HOST, USER, PASSWORD, BD);
 
−
$conex->autocommit(false);
 
−
</syntaxhighlight>
 
  
−
:: Desta forma as operacións que vaian a continuación desta orde non se farán efectivas ata atopar un commit.
+
$stmt = $conex->prepare($sql);
  
−
:* Iniciar unha transacción coa [http://php.net/manual/es/mysqli.begin-transaction.php función mysqli::begin_transaction].
+
$stmt->execute([
−
:: '''NECESITAMOS QUE A VERSIÓN PHP SEXA 5.5 OU SUPERIOR'''.
+
'nome' => $_POST['nome']
 +
]); </syntaxhighlight>
  
−
::* Versión procedimental:
+
: 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.
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
 
−
$conex = mysqli_connect(HOST, USER, PASSWORD, BD);
 
−
mysqli_begin_transaction($conex,MYSQLI_TRANS_START_READ_ONLY);
 
−
</syntaxhighlight>
 
  
−
::* Versión obxectual:
+
: Máis información:
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="2" >
+
: [https://www.php.net/manual/es/pdo.prepared-statements.php Sentenzas preparadas con PDO]
−
$conex= new mysqli(HOST, USER, PASSWORD, BD);
 
−
$conex->begin_transaction(MYSQLI_TRANS_START_READ_ONLY);
 
−
</syntaxhighlight>
 
  
 +
=== PHP empregando PDO ===
  
−
:: Esta segunda forma leva como parámetro unha bandeira que vai indicar que dentro da transacción non vai poderse facer operacións que modifiquen os contidos das táboas da base de datos.
+
'''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.
−
:: Se a transacción necesita facer operacións contra a base de datos, levaría o flag MYSQLI_TRANS_START_READ_WRITE.
 
−
:: Máis información [https://dev.mysql.com/doc/refman/5.6/en/commit.html neste enlace].
 
  
 +
* Para preparar unha sentenza empregamos o método:
  
−
* Unha vez iniciada a transacción faremos as operacións necesarias sobre a base de datos. En caso de erro nalgunha delas, teremos que facer un rollback coa [http://php.net/manual/es/mysqli.rollback.php función rollback].
+
::<syntaxhighlight lang="php" enclose="div">
 +
PDO::prepare() </syntaxhighlight>
  
−
::* Versión procedimental:
+
* Este método devolve un obxecto da clase `PDOStatement`, sobre o que posteriormente chamaremos ao método:
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="7" >
 
−
$conex = mysqli_connect(HOST, USER, PASSWORD, BD);
 
−
mysqli_begin_transaction($conex,MYSQLI_TRANS_START_READ_ONLY);
 
  
−
.........
+
::<syntaxhighlight lang="php" enclose="div">
−
OPERACIÓN
+
PDOStatement::execute() </syntaxhighlight>
−
Si erro
 
−
  mysqli_rollback($conex);
 
−
  die('Houbo un erro...');
 
  
−
</syntaxhighlight>
+
: 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()]
  
−
::* Versión obxectual:
+
==== Exemplo ====
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="7" >
 
−
$conex= new mysqli(HOST, USER, PASSWORD, BD);
 
−
$conex->begin_transaction(MYSQLI_TRANS_START_READ_ONLY);
 
  
−
.........
+
* Supoñamos que queremos inserir datos na táboa `alumnos`:
−
OPERACIÓN
 
−
Si erro
 
−
  $conex->rollback();
 
−
  die('Houbo un erro...');
 
  
−
</syntaxhighlight>  
+
::<syntaxhighlight lang="sql" enclose="div">
 +
INSERT INTO alumnos (nome, apelido)
 +
VALUES (:nome, :apelido) </syntaxhighlight>
  
 +
* O código PHP podería ser o seguinte:
  
−
* Exemplo (obxectual):
+
::<syntaxhighlight lang="php" line enclose="div">
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="5,6,11,,19,23" >
 
−
try {
 
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,DB);  // Constantes definidas nun arquivo fora do sitio web
 
−
    $conex->set_charset('utf8mb4');
 
−
   
 
−
    $conex->begin_transaction(MYSQLI_TRANS_START_READ_WRITE);
 
−
    $result=$conex->query("INSERT INTO TABLE_1 (col2) VALUES ('valor')");
 
−
    if ($result){
 
−
      // Operación correcta
 
−
      printf ("Novo rexistro có id %d.\n", $conex->insert_id);
 
−
    }
 
−
    $result=$conex->query("INSERT INTO TABLE_2 (col2) VALUES ('valor')");
 
−
    if ($result){
 
−
      // Operación correcta
 
−
      printf ("Novo rexistro có id %d.\n", $conex->insert_id);
 
−
    }
 
  
−
}  // Pecha o try da parte superior da páxina
+
<?php
−
catch (mysqli_sql_exception $e) {
 
−
    if(!empty($conex)) $conex->rollback();
 
−
    die("Erro mysql: " . $e->getMessage());
 
−
}
 
−
catch (Expcetion $e) {
 
−
    if(!empty($conex)) $conex->rollback();
 
−
    die("Erro xeral: " . $e->getMessage());
 
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
}
 
  
−
</syntaxhighlight>
+
$conex = null;
  
 +
try {
  
 +
    $dsn = 'mysql:host=localhost;dbname=php;charset=utf8mb4';
  
−
* Unha vez rematadas as operacións facemos o '''commit''' coa [http://php.net/manual/es/mysqli.commit.php función commit()]:
+
    $opciones = [
−
::* Versión procedimental:
+
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
−
mysqli_commit($conex);
+
        PDO::ATTR_EMULATE_PREPARES  => false
−
</syntaxhighlight>
+
    ];
  
−
::* Versión obxectual:
+
    $conex = new PDO(
−
::::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
        $dsn,
−
$conex->commit();
+
        'user_php',
−
</syntaxhighlight>
+
        'user_php',
 +
        $opciones
 +
    );
  
 +
    // Preparamos a sentenza
 +
    $sql = 'INSERT INTO alumnos (nome, apelido)
 +
            VALUES (:nome, :apelido)';
  
 +
    $stmt = $conex->prepare($sql);
  
 +
    // Indicamos os datos e executamos a sentenza
 +
    $nome = 'Ana';
 +
    $apelido = 'López';
  
−
* Se establecimos autocommit(false) deberemos volvelo a establecer en True.
+
    $stmt->execute([
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
        'nome'    => $nome,
−
$conex->autocommit(true);
+
        'apelido' => $apelido
−
</syntaxhighlight>
+
    ]);
  
 +
} catch (PDOException $e) {
  
−
* Ao rematar todas as operacións sobre a base de datos pecharemos a conexión.
+
    // Durante o desenvolvemento podemos amosar a mensaxe de erro.
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
    // Nunha aplicación en produción non se debería mostrar ao usuario.
−
$conex->close();
+
    printf('Erro PDO: %s', $e->getMessage());
−
</syntaxhighlight>
 
  
 +
} catch (Throwable $e) {
  
 +
    printf('Erro xeral: %s', $e->getMessage());
  
−
<br />
+
} finally {
−
O exemplo anterior de forma completa:
 
−
:::<syntaxhighlight lang="java" line enclose="div" highlight="16,21,26,32" >
 
−
try {
 
−
    $conex = new mysqli(SERVIDOR, USUARIO, PASSWORD,DB);  // Constantes definidas nun arquivo fora do sitio web
 
−
    $conex->set_charset('utf8mb4');
 
−
   
 
−
    $conex->begin_transaction(MYSQLI_TRANS_START_READ_WRITE);
 
−
    $result=$conex->query("INSERT INTO TABLE_1 (col2) VALUES ('valor')");
 
−
    if ($result){
 
−
      // Operación correcta
 
−
      printf ("Novo rexistro có id %d.\n", $conex->insert_id);
 
−
    }
 
−
    $result=$conex->query("INSERT INTO TABLE_2 (col2) VALUES ('valor')");
 
−
    if ($result){
 
−
      // Operación correcta
 
−
      printf ("Novo rexistro có id %d.\n", $conex->insert_id);
 
−
    }
 
−
    $conex->commit();
 
  
−
}  // Pecha o try da parte superior da páxina
+
     $conex = null;
−
catch (mysqli_sql_exception $e) {
 
−
     if(!empty($conex)) $conex->rollback();
 
−
    printf("Erro mysql: " . $e->getMessage()); // Se non poñemos die executa o finally. Se non habería que poñer o autocommit aquí tamén
 
  
 
}
 
}
−
catch (Expcetion $e) {
+
?>
−
    if(!empty($conex)) $conex->rollback();
+
 
−
    printf("Erro xeral: " . $e->getMessage());  // Se non poñemos die executa o finally. Se non habería que poñer o autocommit aquí tamén
+
</syntaxhighlight>
−
}
 
−
finally{
 
−
  if(!empty($conex)){
 
−
      $conex->close();
 
−
  }
 
−
  $conex->autocommit(true);
 
  
−
}
+
==== Executar varias veces a mesma sentenza ====
  
−
</syntaxhighlight>
+
* Unha das vantaxes das sentenzas preparadas é que podemos preparar a instrución unha vez e executala varias veces con valores diferentes.
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
$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'
 +
]);
  
−
<br />
+
// Terceiro alumno
−
=== Transaccións en MYSQL dentro de procedementos almacenados ===
+
$stmt->execute([
 +
'nome'    => 'Lucía',
 +
'apelido' => 'García'
 +
]);
 +
</syntaxhighlight>
  
 +
: A sentenza SQL prepárase unha vez, pero execútase tres veces con valores diferentes.
  
−
* Outra forma de realizar as transaccións é que poidamos enviar os datos a un procedemento almacenado de Mysql e que dende o procedemento se faga a transacción.
 
  
−
* O proceso é moi parecido ao feito dende PHP.
+
* '''Exercicio proposto:''' Intenta crear unha páxina PHP que engada á táboa 'autores' os seguintes datos:
−
: Lembrar que se o facemos dende o propio procedemento <u>NON TEMOS QUE FACELO</u> dende PHP.
+
:* Benito Pérez Galdós
 +
:* Julio Verne
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="5-9,11,20" >
 
−
DELIMITER $$
 
  
−
CREATE PROCEDURE `PROCEDEMENTO_CON_TRANSACCION`(_param1 int,_param2 varchar(50),_param3 varchar(200))
 
−
BEGIN
 
−
DECLARE EXIT HANDLER FOR SQLEXCEPTION
 
−
BEGIN
 
−
ROLLBACK;
 
−
SELECT -1 as error;
 
−
END;
 
  
−
START TRANSACTION;
 
  
−
INSERT INTO TABLE_1(col1,col2)
+
<br />
−
VALUES (_param2,_param3); -- Supoñemos que esta táboa devolve un valor clave autonumérico.
+
: '''Solución exercicio:'''
 +
::<syntaxhighlight lang="php" enclose="div">
 +
<?php
 +
ini_set('display_errors', 1);
 +
ini_set('display_startup_errors', 1);
 +
error_reporting(E_ALL);
  
−
INSERT INTO TABLE_2(col1,col2)
+
require ('/var/www/confBD/bd.php');
−
VALUES (_param1,last_insert_id());
 
  
−
+
try {
−
COMMIT;
 
−
SELECT 0 AS error;
 
  
−
END
+
    $dsn = sprintf(
−
$$
+
        'mysql:host=%s;dbname=%s;charset=utf8mb4',
−
</syntaxhighlight>
+
        SERVIDOR,
 +
        BD
 +
    );
  
−
* Liñas 5-9: Capturamos calquera erro que se produza e nese caso enviamos a PHP un código -1 no select facendo un rollback da transacción.
+
    $opciones = [
−
* Liña 11: Iniciamos a transacción.
+
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
−
* Liñas 20 e 21: Confirmamos a transacción se todo foi ben e enviamos un 0 como código ao PHP.
+
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 +
        PDO::ATTR_EMULATE_PREPARES  => false
 +
    ];
  
 +
    $conex = new PDO(
 +
        $dsn,
 +
        USUARIO,
 +
        PASSWORD,
 +
        $opciones
 +
    );
  
−
* Comentar que se facemos as transaccións dende procedementos almacenados teremos que capturar as posibles excepcións que se produzan para facer o rollback.
+
    echo '<br />Conecta';
−
: Máis información: [http://www.mysqltutorial.org/mysql-error-handling-in-stored-procedures/ Captura de erros en Mysql].
 
  
−
* A partires de Mysql 5.6 temos outra forma de capturar as excepcións: [https://dev.mysql.com/doc/refman/5.6/en/get-diagnostics.html Mensaxes e códigos de erro en Mysql '''a partires da versión 5.6''']
+
    $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';
  
−
==== Caso práctico: Transaccións en MYSQL dentro de procedementos almacenados ====
+
} catch (PDOException $e) {
 +
    die("Error PDO: " . $e->getMessage());
 +
} catch (Throwable $e) {
 +
    die("Error general: " . $e->getMessage());
 +
}
  
−
* Imos crear un procedemento almacenado que dea de alta a un único autor xunto co libro.
+
$conex = null;  // Poderíamos non poñer nada
−
: Partimos da base que xa coñecemos o id do libro e enviamos o nome e apelidos do autor.
 
−
: Isto leva consigo que o procedemento vai ter que dar de alta datos en dúas táboas: AUTORES e LIBROS_AUTORES.
 
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
</syntaxhighlight>
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `INSERIR_AUTORES_LIBRO`(_id_libro int,_nome varchar(50),_apelido varchar(200))
 
−
BEGIN
 
−
DECLARE EXIT HANDLER FOR SQLEXCEPTION
 
−
BEGIN
 
−
ROLLBACK;
 
−
SELECT -1 as error;
 
−
END;
 
  
−
START TRANSACTION;
 
  
−
INSERT INTO AUTORES(nome,apelido)
+
<br />
−
VALUES (_nome,_apelido);
+
==== Asociar explicitamente os parámetros ====
  
−
INSERT INTO LIBROS_AUTORES (libro_id,autor_id)
+
* Tamén podemos asociar os valores aos parámetros antes de executar a sentenza empregando, por exemplo, o método `bindValue()`.
−
VALUES (_id_libro,last_insert_id());
 
−
 
−
COMMIT;
 
−
SELECT 0 AS error;
 
  
−
END
+
::<syntaxhighlight lang="php" enclose="div">
−
$$
+
$sql = 'SELECT nome, apelido
−
</syntaxhighlight>
+
FROM alumnos
 +
WHERE idade >= :idade';
  
−
=== Caso práctico: Transaccións ===
+
$stmt = $conex->prepare($sql);
  
−
'''Exercicio Proposto:''' Asociar Libros a Autores.
+
$stmt->bindValue(':idade', 18, PDO::PARAM_INT);
−
: A idea é escoller dunha lista (<select>) un libro.
 
−
: Unha vez escollido e premido o botón 'AUTORES' desaparecerá a lista.
 
−
: Se indicará nun header o nome do libro e aparecerá nunha lista na que se van poder seleccionar varios autores, os autores da base de datos.
 
−
: Seleccionaremos un ou varios autores e premeremos o botón de ALTA.
 
−
: Isto levará consigo as operacións de alta sobre a táboa LIBROS_AUTORES de cada un dos autores escollidos.
 
  
 +
$stmt->execute(); </syntaxhighlight>
  
−
* Melloras:
+
: Neste exemplo indicamos explicitamente que o valor asociado ao parámetro `:idade` é de tipo enteiro mediante:
−
:* Posibilidade de dar de alta a novos autores. Estes autores non aparecerán la lista ata que se dean de alta.
 
−
:* Polo tanto aparecerán nunha checkboxlist con opción para ser eliminados (non da base de datos xa que aínda non foron engadidos). Poderiades gardar os datos nun array dentro dunha variable de sesión.
 
−
:* Ao premer o botón de ALTA, engadiranse á base de datos e se asociarán ao libro seleccionado.
 
−
:* Isto levará consigo as operacións de alta sobre a táboa AUTORES e LIBROS_AUTORES de cada un dos autores.
 
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
PDO::PARAM_INT </syntaxhighlight>
  
 +
: Para datos de tipo cadea podemos empregar:
  
 +
::<syntaxhighlight lang="php" enclose="div">
 +
PDO::PARAM_STR </syntaxhighlight>
  
−
'''Exercicio proposto/resolto'''
+
: Nos exemplos máis sinxelos tamén podemos enviar directamente os valores ao método `execute()`:
−
* 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 (o podedes implementar cunha táboa) un formulario (unha por cada exemplar) cos datos: imaxe-prezo-estado.
 
−
: Os datos anteriores (título e número de exemplares) non se poderán cambiar.
 
  
−
* Os campos do formulario serán dos seguintes tipo:
+
::<syntaxhighlight lang="php" enclose="div">
−
:* Estado: Será un combobox (select en html) cos estados da táboa estados.
+
$stmt->execute([
−
:* Imaxe: cambiaremos isto máis adiante. Por agora deixade un campo de texto.
+
'idade' => 18
−
:* Prezo: Prezo da imaxe.
+
]); </syntaxhighlight>
  
−
* Como sempre deberedes validar os datos e que teñan o tipo/formato correcto.
 
−
* 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.
 
  
 +
* '''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)
  
−
* '''POSIBLE SOLUCIÓN''' (algúns dos procedementos xa foron creados en solucións anteriores)
 
  
−
'''MYSQL'''
 
  
−
'''Procedemento: CONSULTAR_ESTADOS'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `CONSULTAR_ESTADOS`()
 
−
BEGIN
 
−
SELECT id_estado,descripcion
 
−
FROM ESTADOS
 
−
ORDER BY descripcion;
 
  
−
END
 
−
</syntaxhighlight>
 
  
  
  
 +
<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: INSERIR_LIBRO'''
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `INSERIR_LIBRO`(_titulo varchar(300))
 
−
etiqueta:BEGIN  -- Asociamos unha etiqueta ao código do procedemento para que poidamos facer algo parecido a un exit
 
−
    declare existe int;
 
−
     
 
−
SELECT count(*)
 
−
INTO existe
 
−
FROM LIBROS
 
−
WHERE upper(titulo)=upper(_titulo);
 
−
 
−
IF existe=1 THEN
 
−
begin
 
−
  SELECT -1 as valor_devolto;
 
−
  leave etiqueta; -- Para que saia do procedemento
 
−
end;
 
−
    END IF;  -- Fin do IF
 
  
−
INSERT INTO LIBROS (titulo)
 
−
VALUES (_titulo);
 
  
−
SELECT last_insert_id() as valor_devolto;
 
−
END
 
−
</syntaxhighlight>
 
  
  
 +
<br />
  
 +
=== MySQL ===
  
−
'''Procedemento: INSERIR_EXEMPLAR'''
+
'''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'''.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `INSERIR_EXEMPLAR`(_libro_id int,_id_exemplar int,_imaxe varchar(100),_prezo decimal(6,2),_estado_id int)
 
−
BEGIN
 
−
INSERT INTO EXEMPLARES (libro_id,id_exemplar,imaxe,prezo,estado_id)
 
−
VALUES (_libro_id,_id_exemplar,_imaxe,_prezo,_estado_id);
 
  
−
END
+
: Máis información:
−
$$
+
: [https://dev.mysql.com/doc/refman/8.4/en/sql-prepared-statements.html Prepared Statements en MySQL]
−
</syntaxhighlight>
 
  
 +
* 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'''.
  
−
'''Arquivo bd.inc.php''':
+
: As sentenzas `PREPARE`, `EXECUTE` e `DEALLOCATE PREPARE` poden empregarse dentro de procedementos almacenados, pero non dentro de funcións almacenadas ou triggers.
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
define ('PROCEDEMENTO_INSERIR_LIBRO',"INSERIR_LIBRO('%s')");
 
−
define ('PROCEDEMENTO_INSERIR_EXEMPLAR',"INSERIR_EXEMPLAR(%d,%d,'%s',%f,%d)");
 
−
</syntaxhighlight>
 
  
 +
==== 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:
  
−
'''Arquivo Alta_Libro_Exemplares.php'''
+
::<syntaxhighlight lang="sql" enclose="div">
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
WHERE apelido = ? </syntaxhighlight>
−
<?php
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
: ou:
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
::<syntaxhighlight lang="sql" enclose="div">
−
$conex->set_charset('utf8');
+
WHERE nome = ? </syntaxhighlight>
  
−
if ($conex->connect_error){
+
: ou:
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
  
−
$error='';  // Usado para amosar as mensaxes de erro
+
::<syntaxhighlight lang="sql" enclose="div">
−
// *********************************ALTA ************************************************************************//
+
WHERE apelido = ?
−
if (!empty($_POST['btnAlta'])){ // DAMOS DE ALTA A EXAMPLARES E O LIBRO
+
AND nome = ? </syntaxhighlight>
−
    foreach ($_POST['lstEstado'] as $indice => $estado){    // Usamos o índice para buscar os datos no resto dos campos
 
−
        $prezo = $_POST['txtPrezo'][$indice];
 
−
        $imaxe = $_POST['txtImaxe'][$indice];
 
  
−
        // Xa temos os datos de cada un dos exemplares
+
: Se non se indica ningún criterio, seleccionaranse todas as filas.
−
        // Verifacamos que cumpran os criterios de tipo e formato => PRIMEIRO VALIDAMOS. SE SON CORRECTOS TODOS INICIAMOS A OPERACION DE ALTA
 
−
        if (!filter_var($estado,FILTER_VALIDATE_INT) || $estado<0){
 
−
            $error .= "O estado é incorrecto...";
 
−
            break;
 
−
        }
 
−
        if (filter_var(str_replace(',', '.', $prezo),FILTER_VALIDATE_FLOAT)==false || $prezo<=0 || !preg_match("/[0-9]{1,6}(,[0-9]{1,2})?/",$prezo)){
 
−
            $error .= "O prezo é incorrecto...";
 
−
            break;
 
−
        }
 
−
        $imaxe=filter_var($imaxe,FILTER_SANITIZE_STRING);
 
−
        echo strlen($imaxe);
 
−
        if(((strlen($imaxe))>100) || (strlen($imaxe)==0)) {    // O número de caracteres está establecido na táboa da base de datos
 
−
            $error .= "O nome da imaxe é demasiado grande ou non foi posto...";
 
−
            break;
 
−
        }
 
−
        // Gardamos os datos validados para dalos de alta todos xuntos
 
−
        $dato['prezo'] = $prezo;
 
−
        $dato['imaxe'] = $imaxe;
 
−
        $dato['estado'] = $estado;
 
−
        $datos[] = $dato;
 
−
    }  // Fin do for
 
−
   
 
−
   
 
−
    if(empty($error)){  // Non houbo error, empezamos a operación de alta con transaccións
 
−
        $conex->autocommit(false);  // Iniciamos a transcción
 
−
       
 
−
        // Damos de alta ao libro e obtemos o id xerado
 
−
        $titulo = $conex->real_escape_string($_POST['txtTitulo']);
 
−
        $proc=sprintf(PROCEDEMENTO_INSERIR_LIBRO,$titulo);
 
−
        $proc = sprintf("CALL %s",$proc);
 
−
        if ($conex->multi_query($proc)){
 
−
            $result = $conex->store_result();
 
−
            if ($result && ($result->num_rows>0)){
 
−
                $fila=$result->fetch_assoc();
 
−
                switch($fila['valor_devolto']){
 
−
                    case -1:
 
−
                        $error = "Ese libro xa est&aacute; dado de alta na base de datos...<br />";
 
−
                        break;
 
−
                    default:
 
−
                        $id_libro = $fila['valor_devolto'];    // XA TEMOS O ID AUTONUMERICO DO LIBRO
 
−
                }
 
−
                $result->free();
 
−
                while($conex->next_result() && $conex->store_result());
 
−
            }
 
−
            else{  // Sempre ten que devolver algo como temos implementado o proc.
 
−
                $error = 'Houbo un erro na chamada á base de datos<br />';
 
−
            }
 
−
        }
 
−
        else{
 
−
            $error = 'Houbo un erro na chamada á base de datos <br/>';
 
−
        }       
 
−
       
 
−
       
 
−
        if(empty($error)){  // Non houbo erro e temos o id do libro xerado
 
−
            // Temos que dar de alta aos exemplares
 
−
            $cont_num_exemplar=1;  // Número de exemplar
 
−
            foreach ($datos as $dato){
 
−
                $prezo = $conex->real_escape_string($dato['prezo']);
 
−
                $estado = $conex->real_escape_string($dato['estado']);
 
−
                $imaxe = $conex->real_escape_string($dato['imaxe']);
 
−
                $proc=sprintf(PROCEDEMENTO_INSERIR_EXEMPLAR,$id_libro,$cont_num_exemplar,$imaxe,$prezo,$estado);
 
−
                $proc = sprintf("CALL %s",$proc);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();  // Neste caso o procedemento non devolve nada => vale 'false'
 
−
                    if ($conex->errno>0){
 
−
                        $error .= 'Houbo un erro na alta dos exemplares';
 
−
                        break;  //Saimos do for
 
−
                    }
 
−
                    while($conex->next_result() && $conex->store_result());
 
−
}                 
 
−
                else{
 
−
                    $error = 'Houbo un erro na chamada á base de datos:' . $conex->error;
 
−
                    break;  //Saimos do for
 
−
                }       
 
−
                $cont_num_exemplar+=1;
 
−
            }  // Fin do For por cada exemplar
 
−
        }
 
−
        if (!empty($error)){    // Fallos nas operacións
 
−
            $conex->rollback();
 
−
        }
 
−
        else {  // Todo correcto
 
−
            $conex->commit();
 
−
        }
 
−
       
 
−
    }  // Fin de (empty($error)
 
−
}
 
−
// *********************************FIN DE ALTA ************************************************************************//
 
  
−
?>
+
* Podemos construír dinamicamente a sentenza mantendo os valores separados da instrución SQL:
  
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
DELIMITER $$
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
CREATE PROCEDURE proba(
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
IN _apelido VARCHAR(100),
−
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
+
IN _nome VARCHAR(50)
−
    <head>
+
)
−
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
BEGIN
−
        <title>Transaccións</title>
 
−
    </head>
 
  
−
    <body>
+
```
−
        <?php
+
SET @cadea = 'SELECT nome, apelido
−
            $titulo='';
+
              FROM AUTORES
−
            $num_exemplares='';
+
              WHERE 1 = 1';
−
            if (!empty($_POST['btnEnviar'])) {  // Prememos o botón que indica o título e o número de exemplares
 
−
                $titulo = filter_var($_POST['txtTitulo'],FILTER_SANITIZE_STRING);
 
−
                if (strlen($titulo)>200){  // Non pode ter unha lonxitude maior que o campo da base de datos
 
−
                    $error='A lonxitude do título non pode ser maior que 200 caracteres...<br />';
 
−
                }
 
−
                $num_exemplares = $_POST['txtNumExemplares'];
 
−
                if (!filter_var($num_exemplares,FILTER_VALIDATE_INT) || $num_exemplares > 9 || $num_exemplares < 1){
 
−
                    $error.="O número de exemplares ten que ser numérico e non pode ser maior que 9<br />";
 
−
                }
 
−
                if (strlen($titulo)==0) { 
 
−
                    $error.='O título é obrigatorio...<br />';
 
−
                }
 
−
            }
 
−
        ?>
 
−
       
 
−
        <?php
 
−
            $inhabilitar='';
 
−
            if (!empty($_POST['btnEnviar']) && (empty($error))) {  // Facemos que a caixa de texto e todo e demais quede inhabilitado
 
−
                $inhabilitar="disabled='disabled'";                  // Unha vez enviamos os datos do titulo e num exemplares
 
−
            }
 
−
        ?>
 
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
 
−
            <div>Titulo Libro: <input <?php echo $inhabilitar ?> <?php printf(" value='%s'",$titulo); ?> type='text' size='100' maxlength="300" name='txtTitulo' /></div>
 
−
            <div>Num. Exemplares: <input <?php echo $inhabilitar ?>  <?php printf(" value='%s'",$num_exemplares); ?> type='number' min='1' max='9' size="1" maxlength="1" name='txtNumExemplares' /> (&lt;10) </div>
 
−
            <input <?php echo $inhabilitar ?> type="submit" value='Enviar' name='btnEnviar' />   
 
−
        </form>
 
  
−
        <?php
+
-- Gardamos os valores en variables de usuario,
−
            if (!empty($error)) {  // En caso de atopar erros non amosa nada máis que a mensaxe
+
-- xa que EXECUTE ... USING traballa con este tipo de variables
−
                die("<h4>$error</h4>");
+
SET @p_apelido = _apelido;
−
            }
+
SET @p_nome = _nome;
−
        ?>
 
−
       
 
−
       
 
−
        <?php
 
−
        if ($titulo && $num_exemplares){    // Enviamos datos
 
−
            // NON IMOS A FACER UNHA CONSULTA A TABOA ESTADOS POR CADA UN DOS EXEMPLARES. NON TEN SENTIDO.
 
−
            // PODERIAMOS PERCORRER O ARRAY DE RESULTADOS E AO REMATAR VOLVER AO PRINCIPIO: http://php.net/manual/es/mysqli-result.data-seek.php
 
−
            $proc = sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_ESTADOS);
 
−
            if ($conex->multi_query($proc)){
 
−
                $result = $conex->store_result();
 
−
                if ($result && ($result->num_rows>0)){
 
−
                    while($valor = $result->fetch_assoc()){
 
−
                        $estados[]=$valor;
 
−
                    }
 
−
                    $result->free();
 
−
                    while($conex->next_result() && $conex->store_result());
 
−
                }
 
−
            }
 
−
            printf("<form action=\"%s\" method='post'>",htmlentities($_SERVER['PHP_SELF']));
 
−
            printf("<input type='hidden' value='%s' name='txtTitulo' />",$_POST['txtTitulo']); // Como temos dous formularios, temos que gardar o texto do título nun campo oculto
 
−
            printf ("<table>");
 
−
            for($cont=0;$cont<$num_exemplares;$cont++){
 
−
                echo "<th>Estado</th><th>Imaxe</th><th>Prezo</th>";
 
−
                echo "<tr>";
 
−
                echo "<td>";    // ESTADO
 
−
                    printf ("<select name='lstEstado[]>");  // Poñemos o nome en forma de array xa que imos recibir varios exemplares con datos.
 
−
                    foreach ($estados as $estado){
 
−
                        printf("<option value='%d'>%s</option>",$estado['id_estado'],$estado['descripcion']);
 
−
                    }
 
−
                    printf ("</select>");
 
−
                echo "</td>";
 
−
                echo "<td>";    // IMAXE
 
−
                    printf("<input type='text' size='50' maxlength='70' name='txtImaxe[]' />");
 
−
                echo "</td>";
 
−
                echo "<td>";    // PREZO
 
−
                    printf("<input type='text' size='9' maxlength='9' name='txtPrezo[]' /> Euros (XXXXXX,YY)");
 
−
                echo "</td>";
 
−
                echo "</tr>";
 
−
            }
 
−
           
 
−
            printf ("</table>");
 
−
            printf("<input type='submit' value='ALTA' name='btnAlta' />");  
 
−
            printf("<input type='reset' value='CANCELAR' name='btnCancelar' />");   
 
−
            printf("</form>");
 
−
           
 
−
        }
 
−
        ?>
 
−
    </body>   
 
−
</html>
 
−
</syntaxhighlight>
 
  
 +
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
  
−
* '''Exercicio proposto'''
+
    EXECUTE stmt USING @p_apelido, @p_nome;
−
: Tedes [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Gardando_imaxes_no_servidor neste enlace] da WIKI a forma de subir unha imaxe ao servidor. Modificade o código anterior para facelo.
 
  
 +
ELSEIF _apelido IS NOT NULL THEN
  
 +
    EXECUTE stmt USING @p_apelido;
  
−
* '''Exercicio proposto'''
+
ELSEIF _nome IS NOT NULL THEN
−
: Amplía a funcionalidade da páxina anterior para poder asociar a os autores cos libros na mesma operación de alta.
 
−
: Para iso fai que os autores se carguen nunha lista (<select con size en HTML>) ordenados alfabeticamente e que se poidan seleccionar varios.
 
−
: En caso de que os autores non estean dados de alta, poderás premer nun enlace que abrirá unha xanela nova (sen o toolbar, botóns,...) e que permitirá dar de alta a un novo autor. Ao pechar a xanela deberán refrescarse os datos da lista para amosar o novo autor, mantendo os datos previamente introducidos.
 
  
 +
    EXECUTE stmt USING @p_nome;
  
 +
ELSE
  
 +
    EXECUTE stmt;
  
 +
END IF;
  
−
==== Variante Caso práctico: Transaccións con control de excepcións ====
+
DEALLOCATE PREPARE stmt;
 +
```
  
−
* É o mesmo exercicio que no punto anterior, pero utilizando [http://php.net/manual/es/language.exceptions.php Excepcións] dentro do bloque de instrucións que conforman a transacción.
+
END$$
  
−
'''Arquivo Alta_Libro_Exemplares.php'''
+
DELIMITER ; </syntaxhighlight>
−
::<syntaxhighlight lang="java" line enclose="div" highlight="45,56,64,68,84,89,97-103" >
 
−
<?php
 
  
−
require ('/var/www/confBD/bd.inc.php');
+
* Neste exemplo:
−
//error_reporting(0);
 
  
−
$conex = new mysqli(SERVIDOR,USUARIO,PASSWORD,BD);
+
:* `@cadea` almacena a sentenza SQL que se vai construíndo.
−
$conex->set_charset('utf8');
+
:* 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.
  
−
if ($conex->connect_error){
+
: '''IMPORTANTE:''' É necesario ter en conta os espazos en branco ao construír dinamicamente a sentenza mediante `CONCAT()`. Por exemplo:
−
    die('Erro de Conexi&oacute;n (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
 
−
}
 
  
−
$error='';  // Usado para amosar as mensaxes de erro
+
::<syntaxhighlight lang="sql" enclose="div">
−
// *********************************ALTA ************************************************************************//
+
WHERE 1 = 1 AND apelido = ? </syntaxhighlight>
−
if (!empty($_POST['btnAlta'])){ // DAMOS DE ALTA A EXAMPLARES E O LIBRO
 
−
    foreach ($_POST['lstEstado'] as $indice => $estado){    // Usamos o índice para buscar os datos no resto dos campos
 
−
        $prezo = $_POST['txtPrezo'][$indice];
 
−
        $imaxe = $_POST['txtImaxe'][$indice];
 
  
−
        // Xa temos os datos de cada un dos exemplares
+
: debe conter un espazo entre `1 = 1` e `AND`.
−
        // Verifacamos que cumpran os criterios de tipo e formato => PRIMEIRO VALIDAMOS. SE SON CORRECTOS TODOS INICIAMOS A OPERACION DE ALTA
 
−
        if (!filter_var($estado,FILTER_VALIDATE_INT) || $estado<0){
 
−
            $error .= "O estado é incorrecto...";
 
−
            break;
 
−
        }
 
−
        if (filter_var(str_replace(',', '.', $prezo),FILTER_VALIDATE_FLOAT)==false || $prezo<=0 || !preg_match("/[0-9]{1,6}(,[0-9]{1,2})?/",$prezo)){
 
−
            $error .= "O prezo é incorrecto...";
 
−
            break;
 
−
        }
 
−
        $imaxe=filter_var($imaxe,FILTER_SANITIZE_STRING);
 
−
        if(((strlen($imaxe))>100) || (strlen($imaxe)==0)) {    // O número de caracteres está establecido na táboa da base de datos
 
−
            $error .= "O nome da imaxe é demasiado grande ou non foi posto...";
 
−
            break;
 
−
        }
 
−
        // Gardamos os datos validados para dalos de alta todos xuntos
 
−
        $dato['prezo'] = $prezo;
 
−
        $dato['imaxe'] = $imaxe;
 
−
        $dato['estado'] = $estado;
 
−
        $datos[] = $dato;
 
−
    }  // Fin do for
 
−
   
 
−
   
 
−
    if(empty($error)){  // Non houbo error, empezamos a operación de alta con transaccións
 
−
        $conex->autocommit(false);  // Iniciamos a transcción
 
−
        try{
 
−
                // Damos de alta ao libro e obtemos o id xerado
 
−
          $titulo = $conex->real_escape_string($_POST['txtTitulo']);
 
−
          $proc=sprintf(PROCEDEMENTO_INSERIR_LIBRO,$titulo);
 
−
          $proc = sprintf("CALL %s",$proc);
 
−
          if ($conex->multi_query($proc)){
 
−
              $result = $conex->store_result();
 
−
              if ($result && ($result->num_rows>0)){
 
−
                    $fila=$result->fetch_assoc();
 
−
                    switch($fila['valor_devolto']){
 
−
                        case -1:
 
−
                            throw new Exception("Ese libro xa est&aacute; dado de alta na base de datos...<br />", 1);  // Co 1 (true) indicamos que debemos facer rollback
 
−
                        default:
 
−
                            $id_libro = $fila['valor_devolto'];    // XA TEMOS O ID AUTONUMERICO DO LIBRO
 
−
                    }
 
−
                    $result->free();
 
−
                    while($conex->next_result() && $conex->store_result());
 
−
              }
 
−
              else{  // Sempre ten que devolver algo como temos implementado o proc.
 
−
                  throw new Exception('Houbo un erro na chamada á base de datos<br />',1);
 
−
              }
 
−
          }
 
−
          else{
 
−
              throw new Exception('Houbo un erro na chamada á base de datos <br/>',1);
 
−
          }       
 
  
  
−
            // Temos que dar de alta aos exemplares
 
−
            $cont_num_exemplar=1;  // Número de exemplar
 
−
            foreach ($datos as $dato){
 
−
                $prezo = $conex->real_escape_string($dato['prezo']);
 
−
                $estado = $conex->real_escape_string($dato['estado']);
 
−
                $imaxe = $conex->real_escape_string($dato['imaxe']);
 
  
−
                $proc=sprintf(PROCEDEMENTO_INSERIR_EXEMPLAR,$id_libro,$cont_num_exemplar,$imaxe,$prezo,$estado);
+
<br />
−
                $proc = sprintf("CALL %s",$proc);
 
−
                if ($conex->multi_query($proc)){
 
−
                    $result = $conex->store_result();  // Neste caso o procedemento non devolve nada => vale 'false'
 
−
                    if ($conex->errno>0){
 
−
                        throw new Exception('Houbo un erro na alta dos exemplares',1);
 
−
                    }
 
−
                    while($conex->next_result() && $conex->store_result());
 
−
                }                 
 
−
                else{
 
−
                    throw new Exception('Houbo un erro na chamada á base de datos:' . $conex->error,1);  // Ao usuario non debemos amosarlle as mensaxes do xestor. Estamos na fase de desenvolvemento
 
−
                }       
 
−
                $cont_num_exemplar+=1;
 
−
            }  // Fin do For por cada exemplar
 
−
           
 
−
           
 
−
            $conex->commit();      // Todo correcto, confirmamos a transacción
 
−
            $error="Libro e examplares dados de alta correctamente...";
 
−
        } catch (Exception $ex) {
 
−
            if ($ex->getCode()==1){
 
−
                $conex->rollback();
 
−
            }
 
−
            $error = $ex->getMessage();
 
−
           
 
−
        }   
 
−
       
 
−
       
 
−
    }  // Fin de (empty($error)
 
−
}
 
−
// *********************************FIN DE ALTA ************************************************************************//
 
  
−
?>
+
== Operacións contra a base de datos ==
  
 +
* [[PHP_UD3_OP_CONSULTA | Operacións de consulta]]
 +
* [[PHP_UD3_OP_ALTA | Operacións de alta]]
 +
* [[PHP_UD3_OP_BAIXA | Operacións de baixa]]
 +
* [[PHP_UD3_OP_MODIFICAR | Operacións de modificación]]
 +
* [[PHP_UD3_EXERCICIOS_PROPOSTOS | Exercicios propostos]]
  
−
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+
<br />
−
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
* [[PHP_UD3_TRANSACCIONS | Transaccións]]
−
<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>Transaccións</title>
 
−
    </head>
 
  
−
    <body>
 
−
        <?php
 
−
            $titulo='';
 
−
            $num_exemplares='';
 
−
            if (!empty($_POST['btnEnviar'])) {  // Prememos o botón que indica o título e o número de exemplares
 
−
                $titulo = filter_var($_POST['txtTitulo'],FILTER_SANITIZE_STRING);
 
−
                if (strlen($titulo)>200){  // Non pode ter unha lonxitude maior que o campo da base de datos
 
−
                    $error='A lonxitude do título non pode ser maior que 200 caracteres...<br />';
 
−
                }
 
−
                $num_exemplares = $_POST['txtNumExemplares'];
 
−
                if (!filter_var($num_exemplares,FILTER_VALIDATE_INT) || $num_exemplares > 9 || $num_exemplares < 1){
 
−
                    $error.="O número de exemplares ten que ser numérico e non pode ser maior que 9<br />";
 
−
                }
 
−
                if (strlen($titulo)==0) { 
 
−
                    $error.='O título é obrigatorio...<br />';
 
−
                }
 
−
            }
 
−
        ?>
 
−
       
 
−
        <?php
 
−
            $inhabilitar='';
 
−
            if (!empty($_POST['btnEnviar']) && (empty($error))) {  // Facemos que a caixa de texto e todo e demais quede inhabilitado
 
−
                $inhabilitar="disabled='disabled'";                  // Unha vez enviamos os datos do titulo e num exemplares
 
−
            }
 
−
        ?>
 
−
        <form action="<?php echo htmlentities($_SERVER['PHP_SELF']) ?>" method='post'>
 
−
            <div>Titulo Libro: <input <?php echo $inhabilitar ?> <?php printf(" value='%s'",$titulo); ?> type='text' size='100' maxlength="300" name='txtTitulo' /></div>
 
−
            <div>Num. Exemplares: <input <?php echo $inhabilitar ?>  <?php printf(" value='%s'",$num_exemplares); ?> type='number' min='1' max='9' size="1" maxlength="1" name='txtNumExemplares' /> (&lt;10) </div>
 
−
            <input <?php echo $inhabilitar ?> type="submit" value='Enviar' name='btnEnviar' />   
 
−
        </form>
 
  
−
        <?php
+
<br />
−
            if (!empty($error)) {  // En caso de atopar erros non amosa nada máis que a mensaxe
 
−
                die("<h4>$error</h4>");
 
−
            }
 
−
        ?>
 
−
       
 
−
       
 
−
        <?php
 
−
        if ($titulo && $num_exemplares){    // Enviamos datos
 
−
            // NON IMOS A FACER UNHA CONSULTA A TABOA ESTADOS POR CADA UN DOS EXEMPLARES. NON TEN SENTIDO.
 
−
            // PODERIAMOS PERCORRER O ARRAY DE RESULTADOS E AO REMATAR VOLVER AO PRINCIPIO: http://php.net/manual/es/mysqli-result.data-seek.php
 
−
            $proc = sprintf("CALL %s",PROCEDEMENTO_CONSULTAR_ESTADOS);
 
−
            if ($conex->multi_query($proc)){
 
−
                $result = $conex->store_result();
 
−
                if ($result && ($result->num_rows>0)){
 
−
                    while($valor = $result->fetch_assoc()){
 
−
                        $estados[]=$valor;
 
−
                    }
 
−
                    $result->free();
 
−
                    while($conex->next_result() && $conex->store_result());
 
−
                }
 
−
            }
 
−
            printf("<form action=\"%s\" method='post'>",htmlentities($_SERVER['PHP_SELF']));
 
−
            printf("<input type='hidden' value='%s' name='txtTitulo' />",$_POST['txtTitulo']); // Como temos dous formularios, temos que gardar o texto do título nun campo oculto
 
−
            printf ("<table>");
 
−
            for($cont=0;$cont<$num_exemplares;$cont++){
 
−
                echo "<th>Estado</th><th>Imaxe</th><th>Prezo</th>";
 
−
                echo "<tr>";
 
−
                echo "<td>";    // ESTADO
 
−
                    printf ("<select name='lstEstado[]>");  // Poñemos o nome en forma de array xa que imos recibir varios exemplares con datos.
 
−
                    foreach ($estados as $estado){
 
−
                        printf("<option value='%d'>%s</option>",$estado['id_estado'],$estado['descripcion']);
 
−
                    }
 
−
                    printf ("</select>");
 
−
                echo "</td>";
 
−
                echo "<td>";    // IMAXE
 
−
                    printf("<input type='text' size='50' maxlength='70' name='txtImaxe[]' />");
 
−
                echo "</td>";
 
−
                echo "<td>";    // PREZO
 
−
                    printf("<input type='text' size='9' maxlength='9' name='txtPrezo[]' /> Euros (XXXXXX,YY)");
 
−
                echo "</td>";
 
−
                echo "</tr>";
 
−
            }
 
−
           
 
−
            printf ("</table>");
 
−
            printf("<input type='submit' value='ALTA' name='btnAlta' />");   
 
−
            printf("<input type='reset' value='CANCELAR' name='btnCancelar' />");   
 
−
            printf("</form>");
 
−
           
 
−
        }
 
−
        ?>
 
−
    </body>   
 
−
</html>
 
−
</syntaxhighlight>
 
  
 
== Permisos mínimos ==
 
== Permisos mínimos ==
Línea 1656: Línea 1544:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
== Optimización: Sentenzas Prepared ==
+
== 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:
 +
 
 +
:* [https://www.php.net/manual/es/function.password-hash.php password_hash()]
 +
:* [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()]
 +
 
 +
=== 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:
 +
:* [https://pages.nist.gov/800-63-4/sp800-63b.html NIST SP 800-63B]
 +
:* [https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html OWASP - Authentication Cheat Sheet]
 +
 
 +
=== 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:
  
−
* A idea das sentenzas 'prepared' é a de mandar previamente ao Mysql a sentenza que imos executar pero sen datos.
+
::<syntaxhighlight lang="php" line enclose="div">
−
: Despois asociaremos a dita sentenza 'prepared' os datos e a executaremos.
 
  
−
: Por exemplo:
+
<?php
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
 
−
select col1 from TABOA1
+
$password = 'contrasinal_introducido_polo_usuario';
−
where col2=?
+
 
−
</syntaxhighlight>  
+
$hash = password_hash(
 +
    $password,
 +
    PASSWORD_DEFAULT
 +
);
 +
 
 +
echo $hash;
 +
 
 +
?>
 +
 
 +
</syntaxhighlight>
 +
 
 +
* A constante:
 +
 
 +
::<syntaxhighlight lang="php" enclose="div">
 +
PASSWORD_DEFAULT </syntaxhighlight>
  
−
: Neste caso, a sentenza a executar leva un parámetro (o indicado pola interrogación).
+
: indica que PHP empregará o algoritmo considerado predeterminado para almacenar contrasinais.
−
: Este parámetro se asociará posteriormente e se executará a sentenza. Podemos executar a sentenza moitas veces cambiando o valor do parámetro.
 
  
 +
: 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.
  
−
* Preparar as sentenzas ten as vantaxes:
+
: 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:
−
:* É máis rápido que enviar as sentenzas da forma 'tradicional' sobre todo cando repetimos a mesma sentenza (con valores diferentes) varias veces. Isto é debido a que cando 'preparamos' a sentenza a estamos a enviar ao xestor MYSQL o cal xa fai as operacións de análise da sentenza e outras optimizacións. Isto soamente o terá que facer unha vez.
 
−
:* Maior seguridade, xa que fai imposible a inxección de código (o mesmo que pasa cos procedementos almacenados) como [http://wiki.cifprodolfoucha.es/index.php?title=PHP_Paso_de_datos_con_formularios#Inxecci.C3.B3n_de_CSS vimos anteriormente nesta wiki].
 
  
 +
::<syntaxhighlight lang="sql" enclose="div">
 +
password VARCHAR(255) </syntaxhighlight>
  
 +
=== O salt ===
  
−
=== PHP ===
+
* Os algoritmos de almacenamento de contrasinais empregan un valor aleatorio denominado '''salt'''.
−
[http://php.net/manual/es/mysqli.prepare.php Enlace].
 
  
 +
: 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'''.
  
−
=== MySQL ===
+
* '''Non debemos crear nin proporcionar manualmente o salt'''.
−
[http://download.nust.na/pub6/mysql/doc/refman/5.4/en/sql-syntax-prepared-statements.html Enlace].
 
  
−
* Dende o propio xestor tamén se poden 'executar' sentenzas 'prepared'.  
+
: A función `password_hash()` xera automaticamente un salt seguro e diferente para cada contrasinal.
  
−
* Esta forma pode sernos moi útil se queremos executar sentenzas SQL que se cren dinamicamente dentro dun procedemento almacenado.
+
::<syntaxhighlight lang="php" enclose="div">
 +
$hash = password_hash(
 +
$password,
 +
PASSWORD_DEFAULT
 +
); </syntaxhighlight>
  
−
* Por exemplo, se temos unha páxina de busca en base a varios criterios (por exemplo, nome e idade). A sentenza SQL terá unha parte WHERE que pode ser:
+
: 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()`.
−
:* nome=valor1
 
−
:* idade=valor2
 
−
:* nome=valor1 and idade=valor2
 
  
−
: Vai depender se enviamos ou non o dato teremos que executar unha instrución ou outra.
+
: Polo tanto, '''non é necesario gardar o salt nun campo separado da base de datos'''.
−
: En vez de ter 3 instrucións SQL dentro do procedemento almacenado en base a se enviamos ou non o dato, podemos 'construír' a sentenza SQL coma unha cadea e cando a teñamos executala con 'PREPARED'
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
DELIMITER $$
 
  
−
CREATE DEFINER=`root`@`localhost` PROCEDURE `proba`(_apelido varchar(100),_nome varchar(50))
+
: '''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.
−
BEGIN
 
−
 
−
set @cadea = 'select nome,apelido from AUTORES where 1=1';
 
−
if (_apelido is not null) then
 
−
set @cadea=concat(@cadea,' and apelido=','\'', _apelido,'\'');
 
−
end if;
 
−
if (_nome is not null) then
 
−
set @cadea=concat(@cadea,' and nome=','\'', _nome,'\'');
 
−
end if;
 
  
−
prepare stmt from @cadea;
+
=== Gardar o contrasinal na base de datos ===
  
−
execute stmt;
+
* Supoñamos que recibimos o contrasinal desde un formulario:
  
−
deallocate prepare stmt;
+
::<syntaxhighlight lang="php" enclose="div">
 +
$password = $_POST['password']; </syntaxhighlight>
  
 +
* Primeiro calcularemos o hash:
  
−
END
+
::<syntaxhighlight lang="php" enclose="div">
−
</syntaxhighlight>  
+
$hash = password_hash(
 +
$password,
 +
PASSWORD_DEFAULT
 +
); </syntaxhighlight>
  
−
:* Neste caso o procedemento buscará nunha táboa AUTORES por un nome, apelido ou nome e apelido concreto. En caso de enviar null nos dous parámetros, seleccionará todas as filas.
+
* O valor que gardaremos na base de datos será '''$hash''' e nunca o contrasinal orixinal.
−
:: Fixarse nos espazos en branco cando concatenedes xa que tedes que formar a frase: WHERE 1=1 and cond=valor. Entre o 1=1 e a condición ten que ir un espazo en branco e o mesmo se engadides máis condicións.
 
  
−
== Gardando campos a protexer na base de datos ==
+
* Por exemplo, empregando PDO e unha sentenza preparada:
  
−
* En moitas ocasións imos necesitar gardar nunha base de datos información que non queremos que sexa 'visible' en caso de que alguén poida acceder a nosa base de datos.
+
::<syntaxhighlight lang="php" line enclose="div">
 +
$sql = 'INSERT INTO usuarios (nome, password)
 +
VALUES (:nome, :password)';
  
−
* Un exemplo típico é o das contrasinais dos usuarios do noso sitio web, no caso de implantar un sitio web cunha validación de usuarios.
+
$stmt = $conex->prepare($sql);
  
−
* Información sobre a [https://www.schneier.com/blog/archives/2006/12/realworld_passw.html seguridade das passwords].
+
$hash = password_hash(
−
: Como normas, nunca debe estar baseada nun dicionario.
+
$_POST['password'],
−
: Debe levar caracteres alfanuméricos, maiúsculas e minúsculas e algún carácter non alfanumérico.
+
PASSWORD_DEFAULT
−
: Debe ser de a lo menos 10 ou máis caracteres
+
);
  
 +
$stmt->execute([
 +
'nome'    => $_POST['nome'],
 +
'password' => $hash
 +
]); </syntaxhighlight>
  
 +
=== Comprobar un contrasinal ===
  
−
* Para gardar a información dunha forma segura temos que aplicar un [https://es.wikipedia.org/wiki/Funci%C3%B3n_hash algoritmo de hash].
+
* Cando un usuario inicia sesión non se calcula manualmente o hash do contrasinal para comparalo co gardado.
  
−
: En PHP temos dispoñible a partires da versión 5, [http://php.net/manual/es/function.hash.php a función de hash].
+
* Para comprobar se o contrasinal introducido corresponde co hash almacenado na base de datos empregaremos:
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
::<syntaxhighlight lang="php" enclose="div">
−
<?php
+
password_verify() </syntaxhighlight>
−
echo hash('ripemd160', 'The quick brown fox jumped over the lazy dog.');
 
−
?>
 
−
</syntaxhighlight>
 
  
−
:: Dará como resultado:
+
* Por exemplo:
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
ec457d0a974c48d5685a7efa03d137dc8bbde7e3
 
−
</syntaxhighlight>
 
  
 +
::<syntaxhighlight lang="php" line enclose="div">
  
−
* En PHP temos funcións de hash específicas dun tipo de algoritmo. Por exemplo a [http://php.net/manual/es/function.md5.php función md5].
 
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
 
<?php
 
<?php
−
echo md5('The quick brown fox jumped over the lazy dog.')          // As dúas funcións devolven o mesmo
 
−
echo hash('md5', 'The quick brown fox jumped over the lazy dog.');  // As dúas funcións devolven o mesmo
 
−
?>
 
−
</syntaxhighlight>
 
  
 +
$passwordIntroducido = $_POST['password'];
  
 +
// Este valor obteríase previamente da base de datos
 +
$hashGardado = $usuario['password'];
  
−
* Pero esta forma de 'tratar' a información non chega, xa que se podería descubrir o valor orixinal pola forza bruta.
+
if (password_verify($passwordIntroducido, $hashGardado)) {
  
−
: Para complicalo un pouco máis debemos facer uso de '''SALT'.
+
    echo 'Contrasinal correcto';
  
−
* Salt é unha cadea que se aplica no proceso de hash e fai que sexa practicamente imposible utilizar mecanismos de forza bruta ou [https://es.wikipedia.org/wiki/Tabla_arco%C3%ADris Táboas Rainbow] para obter a cadea orixinal.
+
} else {
  
 +
    echo 'Contrasinal incorrecto';
  
−
* Exemplo de como utilizar salt na función md5.
+
}
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
 
−
<?php
 
−
$password='q121ahgsha..'; // Password introducida polo usuario. Viría dun formulario con $_POST['txtPassword'].
 
−
$cadeaSalt='unha cadea ao chou';
 
  
−
echo md5($cadeaSalt,$password)
 
 
?>
 
?>
 +
 
</syntaxhighlight>
 
</syntaxhighlight>
−
: Cabe sinalar que se a contrasinal é débil, é posible chegar a rompela, polo que é moi importante que a contrasinal sexa 'forte'.
 
−
: A cadea SALT pode ser calquera cadea e mellor que non estea baseada en ningún dicionario.
 
  
 +
* `password_verify()` recibe:
 +
 +
:# O contrasinal introducido polo usuario.
 +
:# 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 ===
  
−
'''Opción recomendada'''
+
* Os algoritmos ou os seus parámetros poden cambiar co paso do tempo.
  
−
* A cadea SALT a podemos proporcionar nos ou podemos xerar unha por medio da [http://php.net/manual/es/function.password-hash.php función password_hash].
+
: PHP proporciona a función:
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
::<syntaxhighlight lang="php" enclose="div">
−
$password = password_hash('contrasinal_a_codificar', PASSWORD_DEFAULT);
+
password_needs_rehash() </syntaxhighlight>
−
</syntaxhighlight>
+
 
 +
: que permite comprobar se un hash almacenado debería volver calcularse utilizando a configuración actual.
  
−
: Codifica a contrasinal utilizando o algoritmo bcrypt e o resultado é unha cadea de 60 caracteres de lonxitude. Seguramente en versións posteriores, o algoritmo vai devolve lonxitudes máis grandes, polo que é <u>recomendable gardar a contrasinal nun campo de 256 caracteres de lonxitede</u>.
+
* Podemos facer esta comprobación despois de validar correctamente o contrasinal:
  
−
: Esta forma de codificar fai que o valor SALT cambie de cada vez.
+
::<syntaxhighlight lang="php" line enclose="div">
 +
if (password_verify($passwordIntroducido, $hashGardado)) {
  
 +
```
 +
// O contrasinal é correcto
  
−
: O resultado será o valor que teremos que gardar na base de datos.
+
if (password_needs_rehash(
 +
    $hashGardado,
 +
    PASSWORD_DEFAULT
 +
)) {
  
 +
    $novoHash = password_hash(
 +
        $passwordIntroducido,
 +
        PASSWORD_DEFAULT
 +
    );
  
−
* Para realizar o proceso contrario, e comprobar se o password introducido por un usuario se corresponde con o gardado na base de datos, teremos que facer uso da [http://php.net/manual/es/function.password-verify.php función password_verify()].
+
    // Actualizar $novoHash na base de datos
 +
}
  
−
::<syntaxhighlight lang="java" line enclose="div" highlight="" >
+
// Continuar co proceso de autenticación
−
$orixinal = 'password_introdicada_polo_usuario';
+
```
−
$codificado = '2y$10$ETpCoo11Gd3w2kx4x1me1OQtx.p67c6eRzg6OSRgPxUsBGaQnff4W';
 
−
 
−
$iguais = password_verify($orixinal, $codificado);
 
−
 
−
if ($iguais) {
 
−
    echo 'Password correcto';
 
−
} else {
 
−
    echo 'Password incorrecto';
 
−
}</syntaxhighlight>
 
  
 +
} </syntaxhighlight>
  
 +
: 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.
  
−
* Máis información [http://php.net/manual/en/faq.passwords.php neste enlace].
+
: 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.
  
 
<br />
 
<br />
 +
 
== Subindo unha imaxe ao servidor ==
 
== Subindo unha imaxe ao servidor ==
  

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