2019-02-26 19:46:33 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* FuzeWorks Component.
|
|
|
|
*
|
|
|
|
* The FuzeWorks PHP FrameWork
|
|
|
|
*
|
|
|
|
* Copyright (C) 2013-2019 TechFuze
|
|
|
|
*
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
*
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
* SOFTWARE.
|
|
|
|
*
|
|
|
|
* @author TechFuze
|
|
|
|
* @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net)
|
|
|
|
* @license https://opensource.org/licenses/MIT MIT License
|
|
|
|
*
|
|
|
|
* @link http://techfuze.net/fuzeworks
|
|
|
|
* @since Version 1.2.0
|
|
|
|
*
|
|
|
|
* @version Version 1.2.0
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace FuzeWorks\DatabaseEngine;
|
|
|
|
use FuzeWorks\Exception\DatabaseException;
|
2019-07-22 09:43:01 +00:00
|
|
|
use FuzeWorks\Exception\TransactionException;
|
2019-02-26 19:46:33 +00:00
|
|
|
use FuzeWorks\Logger;
|
|
|
|
use PDO;
|
|
|
|
use PDOException;
|
|
|
|
use PDOStatement;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class PDOEngine Class
|
|
|
|
*
|
|
|
|
* The following additional methods can be accessed through the __call method
|
|
|
|
* @method bool exec(string $statement)
|
|
|
|
* @method mixed getAttribute(int $attribute)
|
|
|
|
* @method string lastInsertId(string $name = null)
|
|
|
|
* @method string quote(string $string, int $parameter_type = PDO::PARAM_STR)
|
|
|
|
* @method bool setAttribute(int $attribute, mixed $value)
|
|
|
|
*/
|
|
|
|
class PDOEngine extends DatabaseDriver implements iDatabaseEngine
|
|
|
|
{
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether the Engine has been set up
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected $setUp = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The PDO object connected with the database
|
|
|
|
*
|
|
|
|
* @var PDO
|
|
|
|
*/
|
|
|
|
protected $pdoConnection;
|
|
|
|
|
2019-03-01 10:15:30 +00:00
|
|
|
/**
|
|
|
|
* Connection string with the database
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $dsn;
|
|
|
|
|
2019-02-26 19:46:33 +00:00
|
|
|
/**
|
|
|
|
* Whether a transaction has failed and should be reverted in the future
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected $transactionFailed = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether a transaction should be automatically committed if not manually aborted by the user.
|
|
|
|
*
|
|
|
|
* If enabled, will automatically commit or revert upon shutdown
|
|
|
|
* If disabled, will not do anything
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected $transactionAutocommit = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the name of this engine
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getName(): string
|
|
|
|
{
|
|
|
|
return 'pdo';
|
|
|
|
}
|
|
|
|
|
2019-03-01 10:15:30 +00:00
|
|
|
public function getConnectionDescription(): string
|
|
|
|
{
|
|
|
|
return is_null($this->dsn) ? 'none' : $this->dsn;
|
|
|
|
}
|
|
|
|
|
2019-02-26 19:46:33 +00:00
|
|
|
/**
|
|
|
|
* Whether the database connection has been set up yet
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function isSetup(): bool
|
|
|
|
{
|
|
|
|
return $this->setUp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Method called by \FuzeWorks\Database to setUp the database connection
|
|
|
|
*
|
|
|
|
* @param array $parameters
|
|
|
|
* @return bool
|
|
|
|
* @throws DatabaseException
|
|
|
|
*/
|
|
|
|
public function setUp(array $parameters): bool
|
|
|
|
{
|
|
|
|
// Prepare variables for connection
|
2019-03-01 10:15:30 +00:00
|
|
|
$this->dsn = isset($parameters['dsn']) ? $parameters['dsn'] : null;
|
2019-02-26 19:46:33 +00:00
|
|
|
$username = isset($parameters['username']) ? $parameters['username'] : '';
|
|
|
|
$password = isset($parameters['password']) ? $parameters['password'] : '';
|
|
|
|
|
|
|
|
// Don't attempt connection without DSN
|
2019-03-01 10:15:30 +00:00
|
|
|
if (is_null($this->dsn))
|
2019-02-26 19:46:33 +00:00
|
|
|
throw new DatabaseException("Could not setUp PDOEngine. No DSN provided");
|
|
|
|
|
2019-03-01 10:15:30 +00:00
|
|
|
// Set some base parameters which are required for FuzeWorks
|
|
|
|
$parameters[PDO::ATTR_ERRMODE] = PDO::ERRMODE_SILENT;
|
|
|
|
|
2019-02-26 19:46:33 +00:00
|
|
|
// Attempt to connect. Throw exception on failure
|
|
|
|
try {
|
2019-03-01 10:15:30 +00:00
|
|
|
$this->pdoConnection = new PDO($this->dsn, $username, $password, $parameters);
|
2019-02-26 19:46:33 +00:00
|
|
|
} catch (PDOException $e) {
|
|
|
|
throw new DatabaseException("Could not setUp PDOEngine. PDO threw PDOException: '" . $e->getMessage() . "'");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set this engine as set up
|
|
|
|
$this->setUp = true;
|
|
|
|
|
|
|
|
// And return true upon success
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Method called by \FuzeWorks\Database to tear down the database connection upon shutdown
|
|
|
|
*
|
|
|
|
* @return bool
|
2019-07-22 09:43:01 +00:00
|
|
|
* @throws TransactionException
|
2019-02-26 19:46:33 +00:00
|
|
|
*/
|
|
|
|
public function tearDown(): bool
|
|
|
|
{
|
|
|
|
// Commit or rollback all changes to the database
|
|
|
|
$this->transactionEnd();
|
|
|
|
|
|
|
|
//
|
|
|
|
$this->pdoConnection = null;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Call methods on the PDO Connection
|
|
|
|
*
|
|
|
|
* @param $name
|
|
|
|
* @param $arguments
|
|
|
|
* @return mixed
|
|
|
|
*/
|
|
|
|
public function __call($name, $arguments)
|
|
|
|
{
|
|
|
|
return $this->pdoConnection->{$name}(...$arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get properties from the PDO Connection
|
|
|
|
*
|
|
|
|
* @param $name
|
|
|
|
* @return mixed
|
|
|
|
*/
|
|
|
|
public function __get($name)
|
|
|
|
{
|
|
|
|
return $this->pdoConnection->$name;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set properties on the PDO Connection
|
|
|
|
*
|
|
|
|
* @param $name
|
|
|
|
* @param $value
|
|
|
|
* @return mixed
|
|
|
|
*/
|
|
|
|
public function __set($name, $value)
|
|
|
|
{
|
|
|
|
return $this->pdoConnection->$name = $value;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method used to report queries by the \FuzeWorks\DatabaseEngine\PDOStatement
|
|
|
|
*
|
|
|
|
* @internal
|
|
|
|
* @param string $queryString
|
2019-03-01 10:15:30 +00:00
|
|
|
* @param int $queryData
|
2019-02-26 19:46:33 +00:00
|
|
|
* @param float $queryTimings
|
2019-07-22 09:43:01 +00:00
|
|
|
* @param array $errorInfo
|
2019-02-26 19:46:33 +00:00
|
|
|
*/
|
2019-07-22 09:43:01 +00:00
|
|
|
public function logPDOQuery(string $queryString, int $queryData, float $queryTimings, array $errorInfo = [])
|
2019-02-26 19:46:33 +00:00
|
|
|
{
|
2019-03-01 10:15:30 +00:00
|
|
|
$errorInfo = empty($errorInfo) ? $this->error() : $errorInfo;
|
|
|
|
$this->logQuery($queryString, $queryData, $queryTimings, $errorInfo);
|
2019-02-26 19:46:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform a query with the database. Only supports queries.
|
|
|
|
*
|
|
|
|
* Should only be used for reading data without dynamic statements.
|
|
|
|
*
|
|
|
|
* @param string $sql
|
|
|
|
* @return PDOStatement
|
|
|
|
* @throws DatabaseException
|
|
|
|
*/
|
|
|
|
public function query(string $sql): PDOStatement
|
|
|
|
{
|
|
|
|
if (empty($sql))
|
|
|
|
throw new DatabaseException("Could not run query. Provided query is empty.");
|
|
|
|
|
|
|
|
// Run the query and benchmark the time
|
|
|
|
$benchmarkStart = microtime(true);
|
|
|
|
$result = $this->pdoConnection->query($sql);
|
|
|
|
$benchmarkEnd = microtime(true) - $benchmarkStart;
|
|
|
|
|
|
|
|
// Log the query
|
2019-07-22 09:43:01 +00:00
|
|
|
$this->logPDOQuery($sql, 0, $benchmarkEnd);
|
2019-02-26 19:46:33 +00:00
|
|
|
|
|
|
|
// If the query failed, handle the error
|
|
|
|
if ($result === false)
|
|
|
|
{
|
|
|
|
// Mark the transaction as failed
|
|
|
|
$this->transactionFailed = true;
|
|
|
|
|
|
|
|
// And throw an exception
|
|
|
|
throw new DatabaseException("Could not run query. Database returned an error. Error code: " . $this->error()['code']);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a PDOStatement to alter data on the database.
|
|
|
|
*
|
|
|
|
* @param string $statement
|
|
|
|
* @param array $driver_options
|
|
|
|
* @return PDOStatementWrapper
|
|
|
|
*/
|
|
|
|
public function prepare(string $statement, array $driver_options = []): PDOStatementWrapper
|
|
|
|
{
|
|
|
|
return new PDOStatementWrapper(
|
|
|
|
$this->pdoConnection->prepare($statement, $driver_options),
|
2019-07-22 09:43:01 +00:00
|
|
|
array($this, 'logPDOQuery'),
|
|
|
|
$this
|
2019-02-26 19:46:33 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates an error message for the last failure in PDO
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
protected function error(): array
|
|
|
|
{
|
2019-03-01 10:15:30 +00:00
|
|
|
$error = [];
|
2019-02-26 19:46:33 +00:00
|
|
|
$pdoError = $this->pdoConnection->errorInfo();
|
2019-03-01 10:15:30 +00:00
|
|
|
if (empty($pdoError[0]) || $pdoError[0] == '00000')
|
2019-02-26 19:46:33 +00:00
|
|
|
return $error;
|
|
|
|
|
|
|
|
$error['code'] = isset($pdoError[1]) ? $pdoError[0] . '/' . $pdoError[1] : $pdoError[0];
|
|
|
|
if (isset($pdoError[2]))
|
|
|
|
$error['message'] = $pdoError[2];
|
|
|
|
|
|
|
|
return $error;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start a transaction
|
|
|
|
*
|
|
|
|
* @return bool
|
2019-07-22 09:43:01 +00:00
|
|
|
* @throws TransactionException
|
2019-02-26 19:46:33 +00:00
|
|
|
*/
|
|
|
|
public function transactionStart(): bool
|
|
|
|
{
|
2019-07-22 09:43:01 +00:00
|
|
|
try {
|
|
|
|
return $this->pdoConnection->beginTransaction();
|
|
|
|
} catch (PDOException $e) {
|
|
|
|
throw new TransactionException("Could not start transaction. PDO threw PDOException: '" . $e->getMessage() . "'");
|
|
|
|
}
|
2019-02-26 19:46:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* End a transaction.
|
|
|
|
*
|
|
|
|
* Only runs of autocommit is enabled; and a transaction is running.
|
|
|
|
* Automatically rolls back changes if an error occurs with a query
|
|
|
|
*
|
|
|
|
* @return bool
|
2019-07-22 09:43:01 +00:00
|
|
|
* @throws TransactionException
|
2019-02-26 19:46:33 +00:00
|
|
|
*/
|
|
|
|
public function transactionEnd(): bool
|
|
|
|
{
|
|
|
|
// If autocommit is disabled, don't do anything
|
|
|
|
if (!$this->transactionAutocommit)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// If there is no transaction, there is nothing to rollback
|
|
|
|
if (!$this->pdoConnection->inTransaction())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// If a transaction has failed, it should be rolled back
|
|
|
|
if ($this->transactionFailed === true)
|
|
|
|
{
|
|
|
|
$this->transactionRollback();
|
|
|
|
Logger::logError("PDOEngine transaction failed. Transaction has been rolled back.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->transactionCommit();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Commit a transaction
|
|
|
|
*
|
|
|
|
* @return bool
|
2019-07-22 09:43:01 +00:00
|
|
|
* @throws TransactionException
|
2019-02-26 19:46:33 +00:00
|
|
|
*/
|
|
|
|
public function transactionCommit(): bool
|
|
|
|
{
|
2019-07-22 09:43:01 +00:00
|
|
|
try {
|
|
|
|
return $this->pdoConnection->commit();
|
|
|
|
} catch (PDOException $e) {
|
|
|
|
throw new TransactionException("Could not commit transaction. PDO threw PDOException: '" . $e->getMessage() . "'");
|
|
|
|
}
|
2019-02-26 19:46:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Roll back a transaction
|
|
|
|
*
|
|
|
|
* @return bool
|
2019-07-22 09:43:01 +00:00
|
|
|
* @throws TransactionException
|
2019-02-26 19:46:33 +00:00
|
|
|
*/
|
|
|
|
public function transactionRollback(): bool
|
|
|
|
{
|
2019-07-22 09:43:01 +00:00
|
|
|
try {
|
|
|
|
return $this->pdoConnection->rollBack();
|
|
|
|
} catch (PDOException $e) {
|
|
|
|
throw new TransactionException("Could not rollback transaction. PDO threw PDOException: '" . $e->getMessage() . "'");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
public function transactionFail()
|
|
|
|
{
|
|
|
|
$this->transactionFailed = true;
|
2019-02-26 19:46:33 +00:00
|
|
|
}
|
|
|
|
}
|