mirror of
https://github.com/Icinga/icinga-php-library.git
synced 2025-10-23 16:23:59 +02:00
555 lines
14 KiB
PHP
555 lines
14 KiB
PHP
<?php
|
|
|
|
namespace ipl\Sql;
|
|
|
|
use BadMethodCallException;
|
|
use Exception;
|
|
use InvalidArgumentException;
|
|
use ipl\Sql\Contract\Adapter;
|
|
use ipl\Sql\Contract\Quoter;
|
|
use ipl\Stdlib\Plugins;
|
|
use PDO;
|
|
use PDOStatement;
|
|
|
|
/**
|
|
* Connection to a SQL database using the native PDO for database access
|
|
*/
|
|
class Connection implements Quoter
|
|
{
|
|
use Plugins;
|
|
|
|
/** @var Config */
|
|
protected $config;
|
|
|
|
/** @var ?PDO */
|
|
protected $pdo;
|
|
|
|
/** @var QueryBuilder */
|
|
protected $queryBuilder;
|
|
|
|
/** @var Adapter */
|
|
protected $adapter;
|
|
|
|
/**
|
|
* Create a new database connection using the given config for initialising the options for the connection
|
|
*
|
|
* {@link init()} is called after construction.
|
|
*
|
|
* @param Config|iterable $config
|
|
*
|
|
* @throws InvalidArgumentException If there's no adapter for the given database available
|
|
*/
|
|
public function __construct($config)
|
|
{
|
|
$config = $config instanceof Config ? $config : new Config($config);
|
|
|
|
$this->addPluginLoader('adapter', __NAMESPACE__ . '\\Adapter');
|
|
|
|
$adapter = $this->loadPlugin('adapter', $config->db);
|
|
|
|
if (! $adapter) {
|
|
throw new InvalidArgumentException("Can't load database adapter for '{$config->db}'.");
|
|
}
|
|
|
|
$this->adapter = new $adapter();
|
|
$this->config = $config;
|
|
|
|
$this->init();
|
|
}
|
|
|
|
/**
|
|
* Proxy PDO method calls
|
|
*
|
|
* @param string $name The name of the PDO method to call
|
|
* @param array $arguments Arguments for the method to call
|
|
*
|
|
* @return mixed
|
|
*
|
|
* @throws BadMethodCallException If the called method does not exist
|
|
*
|
|
*/
|
|
public function __call($name, array $arguments)
|
|
{
|
|
$this->connect();
|
|
|
|
if (! method_exists($this->pdo, $name)) {
|
|
$class = get_class($this);
|
|
$message = "Call to undefined method $class::$name";
|
|
|
|
throw new BadMethodCallException($message);
|
|
}
|
|
|
|
return call_user_func_array([$this->pdo, $name], $arguments);
|
|
}
|
|
|
|
/**
|
|
* Initialise the database connection
|
|
*
|
|
* If you have to adjust the connection after construction, override this method.
|
|
*/
|
|
public function init()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Get the database adapter
|
|
*
|
|
* @return Adapter
|
|
*/
|
|
public function getAdapter()
|
|
{
|
|
return $this->adapter;
|
|
}
|
|
|
|
/**
|
|
* Get the connection configuration
|
|
*
|
|
* @return Config
|
|
*/
|
|
public function getConfig()
|
|
{
|
|
return $this->config;
|
|
}
|
|
|
|
/**
|
|
* Get the query builder for the database connection
|
|
*
|
|
* @return QueryBuilder
|
|
*/
|
|
public function getQueryBuilder()
|
|
{
|
|
if ($this->queryBuilder === null) {
|
|
$this->queryBuilder = new QueryBuilder($this->adapter);
|
|
}
|
|
|
|
return $this->queryBuilder;
|
|
}
|
|
|
|
/**
|
|
* Create and return the PDO instance
|
|
*
|
|
* This method is called via {@link connect()} to establish a database connection.
|
|
* If the default PDO needs to be adjusted for a certain DBMS, override this method.
|
|
*
|
|
* @return PDO
|
|
*/
|
|
protected function createPdoAdapter()
|
|
{
|
|
$adapter = $this->getAdapter();
|
|
|
|
$config = $this->getConfig();
|
|
|
|
return new PDO(
|
|
$adapter->getDsn($config),
|
|
$config->username,
|
|
$config->password,
|
|
$adapter->getOptions($config)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Connect to the database, if not already connected
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function connect()
|
|
{
|
|
if ($this->pdo !== null) {
|
|
return $this;
|
|
}
|
|
|
|
$this->pdo = $this->createPdoAdapter();
|
|
|
|
if (! empty($this->config->charset)) {
|
|
$this->exec(sprintf('SET NAMES %s', $this->pdo->quote($this->config->charset)));
|
|
}
|
|
|
|
$this->adapter->setClientTimezone($this);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Disconnect from the database
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function disconnect()
|
|
{
|
|
$this->pdo = null;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Check whether the connection to the database is still available
|
|
*
|
|
* @param bool $reconnect Whether to automatically reconnect
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function ping($reconnect = true)
|
|
{
|
|
try {
|
|
$this->query('SELECT 1')->closeCursor();
|
|
} catch (Exception $e) {
|
|
if (! $reconnect) {
|
|
return false;
|
|
}
|
|
|
|
$this->disconnect();
|
|
|
|
return $this->ping(false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Fetch and return all result rows as sequential array
|
|
*
|
|
* @param Select|string $stmt The SQL statement to prepare and execute.
|
|
* @param array $values Values to bind to the statement
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchAll($stmt, array $values = null)
|
|
{
|
|
return $this->prepexec($stmt, $values)
|
|
->fetchAll();
|
|
}
|
|
|
|
/**
|
|
* Fetch and return the first column of all result rows as sequential array
|
|
*
|
|
* @param Select|string $stmt The SQL statement to prepare and execute.
|
|
* @param array $values Values to bind to the statement
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchCol($stmt, array $values = null)
|
|
{
|
|
return $this->prepexec($stmt, $values)
|
|
->fetchAll(PDO::FETCH_COLUMN, 0);
|
|
}
|
|
|
|
/**
|
|
* Fetch and return the first row of the result rows
|
|
*
|
|
* @param Select|string $stmt The SQL statement to prepare and execute.
|
|
* @param array $values Values to bind to the statement
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchOne($stmt, array $values = null)
|
|
{
|
|
return $this->prepexec($stmt, $values)
|
|
->fetch();
|
|
}
|
|
|
|
/**
|
|
* Alias of {@link fetchOne()}
|
|
*/
|
|
public function fetchRow($stmt, array $values = null)
|
|
{
|
|
return $this->prepexec($stmt, $values)
|
|
->fetch();
|
|
}
|
|
|
|
/**
|
|
* Fetch and return all result rows as an array of key-value pairs
|
|
*
|
|
* First column is the key and the second column is the value.
|
|
*
|
|
* @param Select|string $stmt The SQL statement to prepare and execute.
|
|
* @param array $values Values to bind to the statement
|
|
*
|
|
* @return array
|
|
*/
|
|
public function fetchPairs($stmt, array $values = null)
|
|
{
|
|
return $this->prepexec($stmt, $values)
|
|
->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
}
|
|
|
|
/**
|
|
* Fetch and return the first column of the first result row
|
|
*
|
|
* @param Select|string $stmt The SQL statement to prepare and execute.
|
|
* @param array $values Values to bind to the statement
|
|
*
|
|
* @return string
|
|
*/
|
|
public function fetchScalar($stmt, array $values = null)
|
|
{
|
|
return $this->prepexec($stmt, $values)
|
|
->fetchColumn(0);
|
|
}
|
|
|
|
/**
|
|
* Yield each result row
|
|
*
|
|
* `Connection::yieldAll(Select|string $stmt [[, array $values], int $fetchMode [, mixed ...$fetchModeOptions]])`
|
|
*
|
|
* @param Select|string $stmt The SQL statement to prepare and execute.
|
|
* @param mixed ...$args Values to bind to the statement, fetch mode for the statement, fetch mode options
|
|
*
|
|
* @return \Generator
|
|
*/
|
|
public function yieldAll($stmt, ...$args)
|
|
{
|
|
$values = null;
|
|
|
|
if (! empty($args)) {
|
|
if (is_array($args[0])) {
|
|
$values = array_shift($args);
|
|
}
|
|
}
|
|
|
|
$fetchMode = null;
|
|
|
|
if (! empty($args)) {
|
|
$fetchMode = array_shift($args);
|
|
|
|
switch ($fetchMode) {
|
|
case PDO::FETCH_KEY_PAIR:
|
|
foreach ($this->yieldPairs($stmt, $values) as $key => $value) {
|
|
yield $key => $value;
|
|
}
|
|
|
|
return;
|
|
case PDO::FETCH_COLUMN:
|
|
if (empty($args)) {
|
|
$args[] = 0;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$sth = $this->prepexec($stmt, $values);
|
|
|
|
if ($fetchMode !== null) {
|
|
$sth->setFetchMode($fetchMode, ...$args);
|
|
}
|
|
|
|
foreach ($sth as $key => $row) {
|
|
yield $key => $row;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Yield the first column of each result row
|
|
*
|
|
* @param Select|string $stmt The SQL statement to prepare and execute
|
|
* @param array $values Values to bind to the statement
|
|
*
|
|
* @return \Generator
|
|
*/
|
|
public function yieldCol($stmt, array $values = null)
|
|
{
|
|
$sth = $this->prepexec($stmt, $values);
|
|
|
|
$sth->setFetchMode(PDO::FETCH_COLUMN, 0);
|
|
|
|
foreach ($sth as $key => $row) {
|
|
yield $key => $row;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Yield key-value pairs with the first column as key and the second column as value for each result row
|
|
*
|
|
* @param Select|string $stmt The SQL statement to prepare and execute
|
|
* @param array $values Values to bind to the statement
|
|
*
|
|
* @return \Generator
|
|
*/
|
|
public function yieldPairs($stmt, array $values = null)
|
|
{
|
|
$sth = $this->prepexec($stmt, $values);
|
|
|
|
$sth->setFetchMode(PDO::FETCH_NUM);
|
|
|
|
foreach ($sth as $row) {
|
|
list($key, $value) = $row;
|
|
|
|
yield $key => $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare and execute the given statement
|
|
*
|
|
* @param Delete|Insert|Select|Update|string $stmt The SQL statement to prepare and execute
|
|
* @param string|array $values Values to bind to the statement, if any
|
|
*
|
|
* @return PDOStatement
|
|
*/
|
|
public function prepexec($stmt, $values = null)
|
|
{
|
|
if ($values !== null && ! is_array($values)) {
|
|
$values = [$values];
|
|
}
|
|
|
|
if (is_object($stmt)) {
|
|
list($stmt, $values) = $this->getQueryBuilder()->assemble($stmt);
|
|
}
|
|
|
|
$this->connect();
|
|
|
|
$sth = $this->pdo->prepare($stmt);
|
|
$sth->execute($values);
|
|
|
|
return $sth;
|
|
}
|
|
|
|
/**
|
|
* Prepare and execute the given Select query
|
|
*
|
|
* @param Select $select
|
|
*
|
|
* @return PDOStatement
|
|
*/
|
|
public function select(Select $select)
|
|
{
|
|
list($stmt, $values) = $this->getQueryBuilder()->assembleSelect($select);
|
|
|
|
return $this->prepexec($stmt, $values);
|
|
}
|
|
|
|
/**
|
|
* Insert a table row with the specified data
|
|
*
|
|
* @param string $table The table to insert data into. The table specification must be in
|
|
* one of the following formats: 'table' or 'schema.table'
|
|
* @param iterable $data Row data in terms of column-value pairs
|
|
*
|
|
* @return PDOStatement
|
|
*
|
|
* @throws InvalidArgumentException If data type is invalid
|
|
*/
|
|
public function insert($table, $data)
|
|
{
|
|
$insert = (new Insert())
|
|
->into($table)
|
|
->values($data);
|
|
|
|
return $this->prepexec($insert);
|
|
}
|
|
|
|
/**
|
|
* Update table rows with the specified data, optionally based on a given condition
|
|
*
|
|
* @param string|array $table The table to update. The table specification must be in one of
|
|
* the following formats:
|
|
* 'table', 'table alias', ['alias' => 'table']
|
|
* @param iterable $data The columns to update in terms of column-value pairs
|
|
* @param mixed $condition The WHERE condition
|
|
* @param string $operator The operator to combine multiple conditions with,
|
|
* if the condition is in the array format
|
|
*
|
|
* @return PDOStatement
|
|
*
|
|
* @throws InvalidArgumentException If data type is invalid
|
|
*/
|
|
public function update($table, $data, $condition = null, $operator = Sql::ALL)
|
|
{
|
|
$update = (new Update())
|
|
->table($table)
|
|
->set($data);
|
|
|
|
if ($condition !== null) {
|
|
$update->where($condition, $operator);
|
|
}
|
|
|
|
return $this->prepexec($update);
|
|
}
|
|
|
|
/**
|
|
* Delete table rows, optionally based on a given condition
|
|
*
|
|
* @param string|array $table The table to delete data from. The table specification must be in one of the
|
|
* following formats: 'table', 'table alias', ['alias' => 'table']
|
|
* @param mixed $condition The WHERE condition
|
|
* @param string $operator The operator to combine multiple conditions with,
|
|
* if the condition is in the array format
|
|
*
|
|
* @return PDOStatement
|
|
*/
|
|
public function delete($table, $condition = null, $operator = Sql::ALL)
|
|
{
|
|
$delete = (new Delete())
|
|
->from($table);
|
|
|
|
if ($condition !== null) {
|
|
$delete->where($condition, $operator);
|
|
}
|
|
|
|
return $this->prepexec($delete);
|
|
}
|
|
|
|
/**
|
|
* Begin a transaction
|
|
*
|
|
* @return bool Whether the transaction was started successfully
|
|
*/
|
|
public function beginTransaction()
|
|
{
|
|
$this->connect();
|
|
|
|
return $this->pdo->beginTransaction();
|
|
}
|
|
|
|
/**
|
|
* Commit a transaction
|
|
*
|
|
* @return bool Whether the transaction was committed successfully
|
|
*/
|
|
public function commitTransaction()
|
|
{
|
|
return $this->pdo->commit();
|
|
}
|
|
|
|
/**
|
|
* Roll back a transaction
|
|
*
|
|
* @return bool Whether the transaction was rolled back successfully
|
|
*/
|
|
public function rollBackTransaction()
|
|
{
|
|
return $this->pdo->rollBack();
|
|
}
|
|
|
|
/**
|
|
* Run the given callback in a transaction
|
|
*
|
|
* @param callable $callback The callback to run in a transaction.
|
|
* This connection instance is passed as parameter to the callback
|
|
*
|
|
* @return mixed The return value of the callback
|
|
*
|
|
* @throws Exception If an error occurs when running the callback
|
|
*/
|
|
public function transaction(callable $callback)
|
|
{
|
|
$this->beginTransaction();
|
|
|
|
try {
|
|
$result = call_user_func($callback, $this);
|
|
$this->commitTransaction();
|
|
} catch (Exception $e) {
|
|
$this->rollBackTransaction();
|
|
|
|
throw $e;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function quoteIdentifier($identifier)
|
|
{
|
|
return $this->getAdapter()->quoteIdentifier($identifier);
|
|
}
|
|
}
|