Finesse/MicroDB

View on GitHub
src/Connection.php

Summary

Maintainability
A
25 mins
Test Coverage
<?php
 
namespace Finesse\MicroDB;
 
use Finesse\MicroDB\Exceptions\FileException;
use Finesse\MicroDB\Exceptions\InvalidArgumentException;
use Finesse\MicroDB\Exceptions\PDOException;
use PDOException as BasePDOException;
 
/**
* Wraps a PDO object for more convenient usage.
*
* Future features:
* * todo: transations
* * todo: PostgreSQL and SQL Server support
*
* @author Surgie
*/
class Connection
{
/**
* @var \PDO The PDO instance. Throws exceptions on errors. The default fetch mode is FETCH_ASSOC.
*/
protected $pdo;
 
/**
* Connection constructor.
*
* @param \PDO $pdo A PDO instance to work with. The given object WILL BE MODIFIED. You MUST NOT MODIFY the given
* object.
*/
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
}
 
/**
* Creates a self instance. All the arguments are the arguments for the PDO constructor.
*
* @param string $dsn
* @param string|null $username
* @param string|null $passwd
* @param array|null $options
* @return static
* @throws PDOException
* @see http://php.net/manual/en/pdo.construct.php Arguments reference
*/
public static function create(
string $dsn,
string $username = null,
string $passwd = null,
array $options = null
): self {
$defaultOptions = [
\PDO::ATTR_STRINGIFY_FETCHES => false
];
 
try {
return new static(new \PDO($dsn, $username, $passwd, array_replace($defaultOptions, $options ?? [])));
} catch (\Throwable $exception) {
throw static::wrapException($exception);
}
}
 
/**
* Performs a select query and returns the query results.
*
* @param string $query Full SQL query
* @param array $values Values to bind. The indexes are the names or numbers of the values.
* @return array[] Array of the result rows. Result row is an array indexed by columns.
* @throws InvalidArgumentException
* @throws PDOException
*/
public function select(string $query, array $values = []): array
{
try {
return $this->executeStatement($query, $values)->fetchAll();
} catch (\Throwable $exception) {
throw static::wrapException($exception, $query, $values);
}
}
 
/**
* Performs a select query and returns the first query result.
*
* @param string $query Full SQL query
* @param array $values Values to bind. The indexes are the names or numbers of the values.
* @return array|null An array indexed by columns. Null if nothing is found.
* @throws InvalidArgumentException
* @throws PDOException
*/
public function selectFirst(string $query, array $values = [])
{
try {
$row = $this->executeStatement($query, $values)->fetch();
return $row === false ? null : $row;
} catch (\Throwable $exception) {
throw static::wrapException($exception, $query, $values);
}
}
 
/**
* Performs an insert query and returns the number of inserted rows.
*
* @param string $query Full SQL query
* @param array $values Values to bind. The indexes are the names or numbers of the values.
* @return int
* @throws InvalidArgumentException
* @throws PDOException
*/
public function insert(string $query, array $values = []): int
{
try {
return $this->executeStatement($query, $values)->rowCount();
} catch (\Throwable $exception) {
throw static::wrapException($exception, $query, $values);
}
}
 
/**
* Performs an insert query and returns the identifier of the last inserted row.
*
* @param string $query Full SQL query
* @param array $values Values to bind. The indexes are the names or numbers of the values.
* @param string|null $sequence Name of the sequence object from which the ID should be returned
* @return int|string
* @throws InvalidArgumentException
* @throws PDOException
*/
public function insertGetId(string $query, array $values = [], string $sequence = null)
{
try {
$this->executeStatement($query, $values);
$id = $this->pdo->lastInsertId($sequence);
return is_numeric($id) ? (int)$id : $id;
} catch (\Throwable $exception) {
throw static::wrapException($exception, $query, $values);
}
}
 
/**
* Performs an update query.
*
* @param string $query Full SQL query
* @param array $values Values to bind. The indexes are the names or numbers of the values.
* @return int The number of updated rows
* @throws InvalidArgumentException
* @throws PDOException
*/
public function update(string $query, array $values = []): int
{
try {
return $this->executeStatement($query, $values)->rowCount();
} catch (\Throwable $exception) {
throw static::wrapException($exception, $query, $values);
}
}
 
/**
* Performs a delete query.
*
* @param string $query Full SQL query
* @param array $values Values to bind. The indexes are the names or numbers of the values.
* @return int The number of deleted rows
* @throws InvalidArgumentException
* @throws PDOException
*/
public function delete(string $query, array $values = []): int
{
try {
return $this->executeStatement($query, $values)->rowCount();
} catch (\Throwable $exception) {
throw static::wrapException($exception, $query, $values);
}
}
 
/**
* Performs a general query. If the query contains multiple statements separated by a semicolon, only the first
* statement will be executed.
*
* @param string $query Full SQL query
* @param array $values Values to bind. The indexes are the names or numbers of the values.
* @throws InvalidArgumentException
* @throws PDOException
*/
public function statement(string $query, array $values = [])
{
try {
$this->executeStatement($query, $values);
} catch (\Throwable $exception) {
throw static::wrapException($exception, $query, $values);
}
}
 
/**
* Performs a general query. It executes all the statements separated by a semicolon.
*
* @param string $query Full SQL query
* @throws PDOException
*/
public function statements(string $query)
{
try {
$this->pdo->exec($query);
} catch (\Throwable $exception) {
throw static::wrapException($exception, $query);
}
}
 
/**
* Executes statements from a file.
*
* @param string|resource $file A file path or a read resource. If a resource is given, it will be read to the end
* end closed.
* @throws PDOException
* @throws InvalidArgumentException
* @throws FileException
*/
public function import($file)
{
$resource = $this->makeReadResource($file);
 
// Maybe it will read and execute the resource statement by statement instead of reading all the resource at
// once in the future, but it is not required yet.
$sqlText = @stream_get_contents($resource);
 
if ($sqlText === false) {
$errorInfo = error_get_last();
throw new FileException(sprintf(
'Failed to read from the resource%s',
$errorInfo ? ': '.$errorInfo['message'] : ''
));
}
 
@fclose($resource);
$this->statements($sqlText);
}
 
/**
* Returns the used PDO instance.
*
* @return \PDO You MUST NOT MODIFY it
*/
public function getPDO(): \PDO
{
return $this->pdo;
}
 
/**
* Executes a single SQL statement and returns the corresponding PDO statement.
*
* @param string $query Full SQL query
* @param array $values Values to bind. The indexes are the names or numbers of the values.
* @return \PDOStatement
* @throws InvalidArgumentException
* @throws BasePDOException
*/
protected function executeStatement(string $query, array $values = []): \PDOStatement
{
$statement = $this->pdo->prepare($query);
$this->bindValues($statement, $values);
$statement->execute();
return $statement;
}
 
/**
* Binds parameters to a PDO statement.
*
* @param \PDOStatement $statement PDO statement
* @param array $values Parameters. The indexes are the names or numbers of the values.
* @throws InvalidArgumentException
* @throws BasePDOException
*/
protected function bindValues(\PDOStatement $statement, array $values)
{
$number = 1;
 
foreach ($values as $name => $value) {
$this->bindValue($statement, is_string($name) ? $name : $number, $value);
$number += 1;
}
}
 
/**
* Binds a value to a PDO statement.
*
* @param \PDOStatement $statement PDO statement
* @param string|int $name Value placeholder name or index (if the placeholder is not named)
* @param string|int|float|boolean|null $value Value to bind
* @throws InvalidArgumentException
* @throws BasePDOException
*/
Function `bindValue` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.
protected function bindValue(\PDOStatement $statement, $name, $value)
{
if ($value !== null && !is_scalar($value)) {
throw new InvalidArgumentException(sprintf(
'Bound value %s expected to be scalar or null, a %s given',
is_int($name) ? '#'.$name : '`'.$name.'`',
gettype($value)
));
}
 
if ($value === null) {
$type = \PDO::PARAM_NULL;
} elseif (is_bool($value)) {
$type = \PDO::PARAM_BOOL;
} elseif (is_integer($value)) {
$type = \PDO::PARAM_INT;
} else {
$type = \PDO::PARAM_STR;
}
 
$statement->bindValue($name, $value, $type);
}
 
/**
* Makes a resource for reading data.
*
* @param string|resource $source A file path or a read resource
* @return resource
* @throws FileException
* @throws InvalidArgumentException
*/
protected function makeReadResource($source)
{
if (is_resource($source)) {
return $source;
}
 
if (is_string($source)) {
$resource = @fopen($source, 'r');
 
if ($resource) {
return $resource;
}
 
$errorInfo = error_get_last();
throw new FileException(sprintf(
'Unable to open the file `%s` for reading%s',
$source,
$errorInfo ? ': '.$errorInfo['message'] : ''
));
}
 
throw new InvalidArgumentException(sprintf(
'The given source expected to be a file path of a resource, a %s given',
is_object($source) ? get_class($source).' instance' : gettype($source)
));
}
 
/**
* Creates a library exception from a PHP exception if possible.
*
* @param \Throwable $exception
* @param string|null $query SQL query which caused the error (if caused by a query)
* @param array|null $values Bound values (if caused by a query)
* @return IException|\Throwable
*/
protected static function wrapException(
\Throwable $exception,
string $query = null,
array $values = null
): \Throwable {
if ($exception instanceof BasePDOException) {
return PDOException::wrapBaseException($exception, $query, $values);
}
 
return $exception;
}
}