229 lines
6.7 KiB
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);
|
||
|
}
|
||
|
}
|