nur-sery/nur_src/m/pdo/PdoConn.php

229 lines
6.7 KiB
PHP

<?php
namespace nur\m\pdo;
use nur\A;
use nur\config;
use nur\debug;
use nur\m\base\AbstractConn;
use nur\m\base\QueryException;
use nur\m\IQuery;
use nur\m\IRowIncarnation;
use nur\md;
use PDO;
use PDOException;
use PDOStatement;
class PdoConn extends AbstractConn {
protected $dbname, $dbuser, $dbpass, $options;
/** @var PDO */
protected $pdo;
/** @var bool sommes-nous dans une transaction? */
protected $inTransaction = false;
/** cette base de données supporte-t-elle la fonction lastInsertId() ? */
protected function HAVE_LAST_INSERT_ID(): bool {
return static::HAVE_LAST_INSERT_ID;
} const HAVE_LAST_INSERT_ID = false;
/** cette base de données supporte-t-elle la clause returning ? */
protected function HAVE_RETURNING_CLAUSE(): bool {
return static::HAVE_RETURNING_CLAUSE;
} const HAVE_RETURNING_CLAUSE = false;
/** initialiser les options pour la création de l'instance de PDO */
protected function beforeInitPdo(?array &$options): void {
$options[PDO::ATTR_PERSISTENT] = true;
}
/** initialiser l'instance de PDO après sa création */
protected function afterInitPdo(PDO $pdo): void {
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER);
}
function __construct($dbname, ?string $dbuser=null, ?string $dbpass=null) {
if (is_array($dbname)) {
if ($dbuser === null) $dbuser = $dbname["user"];
if ($dbpass === null) $dbpass = $dbname["pass"];
$dbname = $dbname["name"];
}
$this->beforeInitPdo($options);
$this->dbname = $dbname;
$this->dbuser = $dbuser;
$this->dbpass = $dbpass;
$this->options = $options;
try {
$pdo = new PDO($dbname, $dbuser, $dbpass, $options);
} catch (PDOException $e) {
throw new QueryException("unable to create PDO instance", $e);
}
$this->afterInitPdo($pdo);
$this->pdo = $pdo;
}
function __destruct() {
$this->rollback();
}
function getInfos(): array {
return [$this->dbname, $this->dbuser, $this->dbpass, $this->options];
}
function beginTransaction(): void {
if (!$this->inTransaction) {
try {
$this->pdo->beginTransaction();
} catch (PDOException $e) {
throw new QueryException("cannot begin transaction", $e);
}
$this->inTransaction = true;
}
}
function commit(): void {
if ($this->inTransaction) {
$this->inTransaction = false;
try {
$this->pdo->commit();
} catch (PDOException $e) {
throw new QueryException("cannot commit", $e);
}
}
}
function rollback(): void {
if ($this->inTransaction) {
$this->inTransaction = false;
try {
$this->pdo->rollBack();
} catch (PDOException $e) {
throw new QueryException("cannot rollback", $e);
}
}
}
private function bindParam(PDOStatement $stmt, string $name, &$value): void {
try {
if (is_array($value)) {
# tableau associatif: on peut spécifier la longueur et le type de la valeur
$maxlength = A::get($value, "maxlength", -1);
$type = A::get($value, "type", PDO::PARAM_STR);
$stmt->bindParam($name, $value["value"], $type, $maxlength);
} else {
# sinon, faire un bind simple
$stmt->bindParam($name, $value);
}
} catch (PDOException $e) {
throw new QueryException("error binding with $name", $e);
}
}
function _execute0(string $sql, ?array &$bindings, array $params): array {
if ($params["transaction"]) $this->beginTransaction();
try {
$stmt = $this->pdo->prepare($sql);
} catch (PDOException $e) {
throw new QueryException("preparation error", $e);
}
if ($bindings !== null) {
foreach ($bindings as $name => &$values) {
# IMPORTANT: il faut binder sur l'adresse de $values
if (A::is_seq($values)) {
# liste de valeurs: binder avec des noms générés incrémentalement
# de la forme $name_$index
$count = count($values);
for ($index = 0; $index < $count; $index++) {
$this->bindParam($stmt, "${name}_${index}", $values[$index]);
}
} else {
# une seule valeur
$this->bindParam($stmt, $name, $values);
}
}; unset($values);
}
try {
$stmt->execute();
} catch (PDOException $e) {
throw new QueryException("execute error", $e);
}
$r = [];
if ($params["num_rows"]) {
$r["num_rows"] = $stmt->rowCount();
}
if ($params["last_insert_id"]) {
$r["insert_id"] = $this->HAVE_LAST_INSERT_ID()? $this->pdo->lastInsertId(): null;
}
if ($params["stmt"]) {
$r["stmt"] = $stmt;
} else {
$stmt->closeCursor();
}
return $r;
}
function _prepareLogger(string $sql, ?array $bindings): array {
$queryLogger = $this->queryLogger;
$actualQuery = null;
$traceSql = config::k("trace_sql", false);
if ($traceSql || $queryLogger !== null) {
$actualQuery = PdoQuery::build_actual_query($sql, $bindings);
}
if ($traceSql) debug::log("SQL TRACE --", $actualQuery);
return [$queryLogger, $actualQuery];
}
function _execute1(string $sql, ?array &$bindings, array $params): array {
[$queryLogger, $actualQuery] = $this->_prepareLogger($sql, $bindings);
$r = $this->_execute0($sql, $bindings, $params);
if ($queryLogger !== null) $queryLogger->logQuery($actualQuery);
return $r;
}
function _execute(string $sql, ?array &$bindings=null, ?array $params=null): array {
md::ensure_schema($params, self::EXECUTE_PARAMS_SCHEMA);
return $this->_execute1($sql, $bindings, $params);
}
function _fetchAll(string $sql, ?array &$bindings=null): array {
["stmt" => $stmt
] = $this->_execute1($sql, $bindings, self::EXECUTE_PARAMS_DQL);
try {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
throw new QueryException("fetch error", $e);
} finally {
$stmt->closeCursor();
}
return $rows;
}
function _fetchFirst(string $sql, ?array &$bindings=null): ?array {
["stmt" => $stmt
] = $this->_execute1($sql, $bindings, self::EXECUTE_PARAMS_DQL);
try {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
throw new QueryException("fetch error", $e);
} finally {
$stmt->closeCursor();
}
return $row !== false? $row: null;
}
function _update(string $sql, ?array &$bindings=null): int {
["num_rows" => $numRows
] = $this->_execute1($sql, $bindings, self::EXECUTE_PARAMS_DML_UPDATE);
return $numRows;
}
function query(?string $sql=null, ?array $filter=null, ?IRowIncarnation $incarnation=null): IQuery {
return new PdoQuery($this, $sql, $filter, $incarnation);
}
}