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