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