Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Adapter;
|
||||
|
||||
use Generator;
|
||||
|
||||
interface AdapterInterface
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function connect();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect();
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function inTransaction();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function beginTransaction();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function rollback();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function commit();
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function lastInsertId();
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
* @param string $statement
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function perform($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function fetchAffected($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAll($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAssoc($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
* @param bool $includeGroupColumn
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchGroup($statement, array $values = [], $includeGroupColumn = false);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return int|float|string|false false on empty result
|
||||
*/
|
||||
public function fetchValue($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchRow($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchCol($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchPairs($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldCol($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAssoc($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAll($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldPairs($statement, array $values = []);
|
||||
|
||||
/**
|
||||
* Escapes values for "BOOLEAN" and (TINY)INT columns
|
||||
*
|
||||
* @param bool|null $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeBool($value, $isNullable = false);
|
||||
|
||||
/**
|
||||
* Escapes values for INT columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeInt($value, $isNullable = false);
|
||||
|
||||
/**
|
||||
* Escapes values for FLOAT, DOUBLE and REAL columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeDecimal($value, $isNullable = false);
|
||||
|
||||
/**
|
||||
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeString($value, $isNullable = false);
|
||||
|
||||
/**
|
||||
* Escapes and quotes an identifier (column or table name)
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeIdentifier($value);
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Adapter;
|
||||
|
||||
use Generator;
|
||||
use mysqli;
|
||||
use mysqli_result;
|
||||
use mysqli_stmt;
|
||||
use Xentral\Components\Database\DatabaseConfig;
|
||||
use Xentral\Components\Database\Exception\BindParameterException;
|
||||
use Xentral\Components\Database\Exception\ConnectionException;
|
||||
use Xentral\Components\Database\Exception\EscapingException;
|
||||
use Xentral\Components\Database\Exception\QueryFailureException;
|
||||
use Xentral\Components\Database\Exception\TransactionException;
|
||||
use Xentral\Components\Database\Parser\MysqliArrayValueParser;
|
||||
use Xentral\Components\Database\Parser\MysqliNamedParameterParser;
|
||||
use Xentral\Components\Database\Profiler\ProfilerInterface;
|
||||
|
||||
final class MysqliAdapter implements AdapterInterface
|
||||
{
|
||||
/** @var mysqli|null $connection */
|
||||
private $connection;
|
||||
|
||||
/** @var DatabaseConfig $config */
|
||||
private $config;
|
||||
|
||||
/** @var bool $transactionActive */
|
||||
private $transactionActive = false;
|
||||
|
||||
/** @var int|null $reconnectCounter */
|
||||
private $reconnectCounter;
|
||||
|
||||
/** @var int $reconnectLimit */
|
||||
private $reconnectMaxCount = 5;
|
||||
|
||||
/** @var ProfilerInterface|null $profiler */
|
||||
private $profiler;
|
||||
|
||||
/**
|
||||
* @param DatabaseConfig $config
|
||||
* @param ProfilerInterface|null $profiler
|
||||
*/
|
||||
public function __construct(DatabaseConfig $config, ProfilerInterface $profiler = null)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->profiler = $profiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConnectionException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if ($this->connection !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->reconnectCounter === null) {
|
||||
$this->reconnectCounter = 0;
|
||||
} else {
|
||||
$this->reconnectCounter++;
|
||||
}
|
||||
|
||||
if ($this->reconnectCounter >= $this->reconnectMaxCount) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Too many reconnects. Reconnect count: %d (Max allowed %d)',
|
||||
$this->reconnectCounter,
|
||||
$this->reconnectMaxCount
|
||||
));
|
||||
}
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
|
||||
$connection = new mysqli(
|
||||
$this->config->getHostname(),
|
||||
$this->config->getUsername(),
|
||||
$this->config->getPassword(),
|
||||
null,
|
||||
$this->config->getPort()
|
||||
);
|
||||
|
||||
if ($connection->connect_errno > 0) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Database connection to host "%s" failed. Error code #%s. Error message: %s',
|
||||
$this->config->getHostname(),
|
||||
$connection->connect_errno,
|
||||
$connection->connect_error
|
||||
));
|
||||
}
|
||||
|
||||
$connection->select_db($this->config->getDatabase());
|
||||
if ($connection->errno > 0) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Database selection failed for database "%s". Error code #%s. Error message: %s',
|
||||
$this->config->getDatabase(),
|
||||
$connection->errno,
|
||||
$connection->error
|
||||
));
|
||||
}
|
||||
|
||||
// @see https://www.php.net/manual/de/mysqlinfo.concepts.charset.php
|
||||
if (!$connection->set_charset($this->config->getCharset())) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Failed to set character set "%s". Error: %s',
|
||||
$this->config->getCharset(),
|
||||
$connection->error
|
||||
));
|
||||
}
|
||||
|
||||
if (!$connection->autocommit(true)) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Failed to activate auto commit. Error: %s',
|
||||
$connection->error
|
||||
));
|
||||
}
|
||||
|
||||
$this->finishProfiler(null, [
|
||||
'dbname' => $this->config->getDatabase(),
|
||||
'host' => $this->config->getHostname(),
|
||||
'port' => $this->config->getPort(),
|
||||
]);
|
||||
|
||||
$this->connection = $connection;
|
||||
|
||||
foreach ($this->config->getQueries() as $query) {
|
||||
$this->perform($query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
if ($this->connection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$this->connection->close();
|
||||
$this->connection = null;
|
||||
$this->finishProfiler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function inTransaction()
|
||||
{
|
||||
return $this->transactionActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransactionException If transaction is already started
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function beginTransaction()
|
||||
{
|
||||
if ($this->inTransaction()) {
|
||||
throw new TransactionException('Transaction is already started.');
|
||||
}
|
||||
|
||||
$this->connect();
|
||||
|
||||
if ($this->connection->begin_transaction() === false) {
|
||||
throw new TransactionException(sprintf('Transaction start failed: %s', $this->connection->error));
|
||||
}
|
||||
$this->transactionActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransactionException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
if (!$this->inTransaction()) {
|
||||
throw new TransactionException('Transaction not started.');
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
$this->connection->autocommit(true);
|
||||
$this->transactionActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransactionException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rollback()
|
||||
{
|
||||
if (!$this->inTransaction()) {
|
||||
throw new TransactionException('Transaction not started.');
|
||||
}
|
||||
|
||||
$this->connection->rollback();
|
||||
$this->connection->autocommit(true);
|
||||
$this->transactionActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function lastInsertId()
|
||||
{
|
||||
return (int)$this->connection->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
* @param string $statement
|
||||
*
|
||||
* @throws QueryFailureException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function perform($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$query = $this->getMysqliStatement($statement, $values);
|
||||
if (!is_object($query)) {
|
||||
throw new QueryFailureException(sprintf('Database query failed: %s', $this->connection->error));
|
||||
}
|
||||
|
||||
$query->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @throws QueryFailureException
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function fetchAffected($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$query = $this->getMysqliStatement($statement, $values);
|
||||
if (!is_object($query)) {
|
||||
throw new QueryFailureException(sprintf('Database query failed: %s', $this->connection->error));
|
||||
}
|
||||
|
||||
$affectedRows = (int)$query->affected_rows;
|
||||
$query->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $affectedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAll($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAssoc($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$assocKey = reset($row); // Fetch first array value
|
||||
$data[$assocKey] = $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchRow($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
if ($result->num_rows === 0) {
|
||||
$result->close();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = $result->fetch_assoc();
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return int|float|string|false false on empty result
|
||||
*/
|
||||
public function fetchValue($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
if ($result->num_rows === 0) {
|
||||
$result->close();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $result->fetch_assoc();
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return reset($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchCol($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$firstValue = reset($row); // Fetch first array value
|
||||
$data[] = $firstValue;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @throws QueryFailureException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchPairs($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
if ($result->field_count !== 2) {
|
||||
throw new QueryFailureException('Field count does not match. fetchPairs() allows only two fields.');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$key = array_shift($row);
|
||||
$value = array_shift($row);
|
||||
$data[$key] = $value;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
* @param bool $includeGroupColumn
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchGroup($statement, array $values = [], $includeGroupColumn = false)
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
|
||||
$data = [];
|
||||
$includeGroupColumn = (bool)$includeGroupColumn;
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$group = $includeGroupColumn === true ? reset($row) : array_shift($row); // Fetch first array value
|
||||
if (!isset($data[$group])) {
|
||||
$data[$group] = [];
|
||||
}
|
||||
$data[$group][] = $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAll($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
yield $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAssoc($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$assocKey = reset($row); // Fetch first array value
|
||||
|
||||
yield $assocKey => $row;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldCol($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$firstValue = reset($row); // Fetch first array value
|
||||
|
||||
yield $firstValue;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @throws QueryFailureException
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldPairs($statement, array $values = [])
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->startProfiler(__FUNCTION__);
|
||||
$result = $this->getMysqliResult($statement, $values);
|
||||
$this->finishProfiler($statement, $values);
|
||||
|
||||
if ($result->field_count !== 2) {
|
||||
throw new QueryFailureException('Field count does not match. yieldPairs() allows only two fields.');
|
||||
}
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$key = array_shift($row);
|
||||
$value = array_shift($row);
|
||||
|
||||
yield $key => $value;
|
||||
}
|
||||
|
||||
$result->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for "BOOLEAN" and (TINY)INT columns
|
||||
*
|
||||
* @param bool|null $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeBool($value, $isNullable = false)
|
||||
{
|
||||
if ($isNullable === true && $value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (!is_bool($value)) {
|
||||
throw new EscapingException('Can not escape bool. Value is not a bool.');
|
||||
}
|
||||
|
||||
return $value === true ? '1' : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for INT columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeInt($value, $isNullable = false)
|
||||
{
|
||||
if ($isNullable === true && $value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (!is_int($value)) {
|
||||
throw new EscapingException('Can not escape integer. Value is not an integer.');
|
||||
}
|
||||
|
||||
return (string)(int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for FLOAT, DOUBLE and REAL columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeDecimal($value, $isNullable = false)
|
||||
{
|
||||
if ($isNullable === true && $value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
throw new EscapingException('Can not escape decimal. Value is not numeric.');
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeString($value, $isNullable = false)
|
||||
{
|
||||
if ($isNullable === true && $value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
throw new EscapingException('Can not escape string. Value is not a string.');
|
||||
}
|
||||
|
||||
$this->connect();
|
||||
|
||||
return "'" . $this->connection->real_escape_string($value) . "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and quotes an identifier (column or table name)
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeIdentifier($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new EscapingException('Can not escape identifier. Passed value is not a string.');
|
||||
}
|
||||
if (empty(trim($value))) {
|
||||
throw new EscapingException('Can not escape identifier. Passed value is empty.');
|
||||
}
|
||||
|
||||
$parts = explode('.', $value);
|
||||
if (count($parts) > 2) {
|
||||
throw new EscapingException('Can not escape identifier. Identifier contains more than one dots.');
|
||||
}
|
||||
|
||||
$partsCleaned = [];
|
||||
foreach ($parts as $part) {
|
||||
if (empty(trim($part))) {
|
||||
throw new EscapingException(
|
||||
'Can not escape identifier. Parts before and after the dot can not be empty.'
|
||||
);
|
||||
}
|
||||
if (strlen($part) > 64) {
|
||||
throw new EscapingException(
|
||||
'Can not escape identifier. Identifier is too long. Only 64 characters are allowed.'
|
||||
);
|
||||
}
|
||||
$partCleaned = preg_replace('/[^A-Za-z0-9_]+/', '', $part);
|
||||
if (strlen($partCleaned) !== strlen($part)) {
|
||||
throw new EscapingException(
|
||||
'Can not escape identifier. Passed value contains invalid characters. ' .
|
||||
'Valid characters: A-Z, a-z, 0-9, Underscore'
|
||||
);
|
||||
}
|
||||
|
||||
$partsCleaned[] = '`' . $partCleaned . '`';
|
||||
}
|
||||
|
||||
return implode('.', $partsCleaned);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->connection = null;
|
||||
$this->transactionActive = false;
|
||||
$this->config = clone $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return mysqli_stmt
|
||||
*/
|
||||
private function getMysqliStatement($statement, array $values = [])
|
||||
{
|
||||
list($statement, $values) = $this->replaceArrayValues($statement, $values);
|
||||
list($rebuildStatement, $bindValues, $parameterNames) = $this->replaceNamedParameters($statement, $values);
|
||||
|
||||
$query = $this->connection->prepare($rebuildStatement);
|
||||
if ($query === false && $this->connection->errno === 2006) {
|
||||
// Code 2006 = MySQL server has gone away
|
||||
// Falls Verbindung in einen Timeout gelaufen ist
|
||||
// => Verbindung wiederherstellen und Prepare erneut probieren
|
||||
$this->disconnect();
|
||||
$this->connect();
|
||||
$query = $this->connection->prepare($rebuildStatement);
|
||||
}
|
||||
if ($query === false || !is_object($query)) {
|
||||
throw new QueryFailureException(
|
||||
sprintf(
|
||||
'Database prepare failed. Error code #%s. Error message: %s',
|
||||
$this->connection->errno,
|
||||
$this->connection->error
|
||||
),
|
||||
(int)$this->connection->errno
|
||||
);
|
||||
}
|
||||
|
||||
$this->bindParametersToMysqliStatement($query, $bindValues, $parameterNames);
|
||||
$query->execute();
|
||||
|
||||
if ($query->errno > 0) {
|
||||
throw new QueryFailureException(
|
||||
sprintf('Database query failed. Error code #%s. Error message: %s', $query->errno, $query->error),
|
||||
(int)$query->errno
|
||||
);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replaceArrayValues($statement, array $values = [])
|
||||
{
|
||||
$parser = new MysqliArrayValueParser();
|
||||
$result = $parser->rebuild($statement, $values);
|
||||
|
||||
return [$result['statement'], $result['values']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replaceNamedParameters($statement, array $values = [])
|
||||
{
|
||||
$parser = new MysqliNamedParameterParser();
|
||||
$result = $parser->rebuild($statement, $values);
|
||||
|
||||
return [$result['statement'], $result['values'], $result['params']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli_stmt $statement
|
||||
* @param array $bindValues Values for binding
|
||||
* @param array $parameterNames Original parameter names (for debugging only)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function bindParametersToMysqliStatement($statement, $bindValues, $parameterNames)
|
||||
{
|
||||
if (empty($bindValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bindTypes = '';
|
||||
foreach ($bindValues as $index => &$bindValue) {
|
||||
if (is_bool($bindValue)) {
|
||||
$bindValue = (int)$bindValue;
|
||||
$bindTypes .= 'i'; // integer
|
||||
continue;
|
||||
}
|
||||
if (is_float($bindValue)) {
|
||||
$bindTypes .= 'd'; // double
|
||||
continue;
|
||||
}
|
||||
if (is_array($bindValue)) {
|
||||
throw new BindParameterException(sprintf(
|
||||
'Can not bind parameter of type "array" to placeholder "%s".',
|
||||
$parameterNames[$index]
|
||||
));
|
||||
}
|
||||
if (is_object($bindValue)) {
|
||||
throw new BindParameterException(sprintf(
|
||||
'Can not bind parameter of type "object" to placeholder "%s".',
|
||||
$parameterNames[$index]
|
||||
));
|
||||
}
|
||||
$bindTypes .= 's'; // string
|
||||
}
|
||||
unset($bindValue);
|
||||
|
||||
$statement->bind_param($bindTypes, ...$bindValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return mysqli_result
|
||||
*/
|
||||
private function getMysqliResult($statement, array $values = [])
|
||||
{
|
||||
$query = $this->getMysqliStatement($statement, $values);
|
||||
$result = $query->get_result();
|
||||
$query->close();
|
||||
|
||||
if ($result === false) {
|
||||
throw new QueryFailureException(
|
||||
sprintf('Database query failed. Error code #%s. Error message: %s', $query->errno, $query->error),
|
||||
(int)$query->errno
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $methodName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function startProfiler($methodName)
|
||||
{
|
||||
if ($this->profiler === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->profiler->start(__CLASS__, $methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function finishProfiler($statement = null, array $values = [])
|
||||
{
|
||||
if ($this->profiler === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->profiler->finish($statement, $values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database;
|
||||
|
||||
use Xentral\Components\Database\Adapter\MysqliAdapter;
|
||||
use Xentral\Components\Database\Exception\ConfigException;
|
||||
use Xentral\Components\Database\Profiler\Profiler;
|
||||
use Xentral\Components\Database\SqlQuery\QueryFactory;
|
||||
use Xentral\Components\Logger\Context\ContextHelper;
|
||||
use Xentral\Components\Logger\MemoryLogger;
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
use Xentral\Core\DependencyInjection\ServiceContainer;
|
||||
use Xentral\Core\LegacyConfig\ConfigLoader;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'Database' => 'onInitDatabase',
|
||||
'DatabaseProfiler' => 'onGetDatabaseProfiler',
|
||||
'MysqliAdapter' => 'onInitMysqliAdapter',
|
||||
'QueryFactory' => 'onInitQueryFactory',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServiceContainer $container
|
||||
*
|
||||
* @return Database
|
||||
*/
|
||||
public static function onInitDatabase(ServiceContainer $container)
|
||||
{
|
||||
return new Database($container->get('MysqliAdapter'), $container->get('QueryFactory'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return QueryFactory
|
||||
*/
|
||||
public static function onInitQueryFactory()
|
||||
{
|
||||
return new QueryFactory('mysql');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServiceContainer $container
|
||||
*
|
||||
* @return Profiler
|
||||
*/
|
||||
public static function onGetDatabaseProfiler(ServiceContainer $container)
|
||||
{
|
||||
$request = $container->get('Request');
|
||||
return new Profiler(new MemoryLogger(new ContextHelper($request)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return MysqliAdapter
|
||||
*/
|
||||
public static function onInitMysqliAdapter(ContainerInterface $container)
|
||||
{
|
||||
$conf = ConfigLoader::load();
|
||||
|
||||
$dbHost = property_exists($conf, 'WFdbhost') ? $conf->WFdbhost : 'localhost';
|
||||
$dbPort = property_exists($conf, 'WFdbport') ? $conf->WFdbport : 3306;
|
||||
$dbName = property_exists($conf, 'WFdbname') ? $conf->WFdbname : null;
|
||||
$dbUser = property_exists($conf, 'WFdbuser') ? $conf->WFdbuser : null;
|
||||
$dbPass = property_exists($conf, 'WFdbpass') ? $conf->WFdbpass : null;
|
||||
|
||||
if (empty($dbName)) {
|
||||
throw new ConfigException('Could not connect to database. Database name is missing or empty.');
|
||||
}
|
||||
if (empty($dbUser)) {
|
||||
throw new ConfigException('Could not connect to database. Database user is missing or empty.');
|
||||
}
|
||||
if (empty($dbPass)) {
|
||||
throw new ConfigException('Could not connect to database. Database password is missing or empty.');
|
||||
}
|
||||
|
||||
$startupQueries = [
|
||||
"SET NAMES 'utf8', " .
|
||||
"CHARACTER SET 'utf8', " .
|
||||
"lc_time_names = 'de_DE', " .
|
||||
"SESSION sql_mode = '', " .
|
||||
"SESSION sql_big_selects = 1;",
|
||||
];
|
||||
$config = new DatabaseConfig($dbHost, $dbUser, $dbPass, $dbName, 'utf8', $dbPort, $startupQueries);
|
||||
|
||||
// Profiler aktivieren
|
||||
// Kann mit $container->get('DatabaseProfiler')->getContexts() abgefragt werden
|
||||
$profiler = $container->get('DatabaseProfiler');
|
||||
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE === true) {
|
||||
$profiler->setActive(true);
|
||||
}
|
||||
|
||||
return new MysqliAdapter($config, $profiler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php /** @noinspection PhpInconsistentReturnPointsInspection */
|
||||
|
||||
namespace Xentral\Components\Database;
|
||||
|
||||
use Generator;
|
||||
use Xentral\Components\Database\Adapter\AdapterInterface;
|
||||
use Xentral\Components\Database\Exception\EscapingException;
|
||||
use Xentral\Components\Database\Exception\TransactionException;
|
||||
use Xentral\Components\Database\SqlQuery\DeleteQuery;
|
||||
use Xentral\Components\Database\SqlQuery\InsertQuery;
|
||||
use Xentral\Components\Database\SqlQuery\SelectQuery;
|
||||
use Xentral\Components\Database\SqlQuery\QueryFactory;
|
||||
use Xentral\Components\Database\SqlQuery\UpdateQuery;
|
||||
|
||||
final class Database
|
||||
{
|
||||
/** @var AdapterInterface $adapter */
|
||||
private $adapter;
|
||||
|
||||
/** @var QueryFactory $queryFactory */
|
||||
private $queryFactory;
|
||||
|
||||
/**
|
||||
* @param AdapterInterface $adapter
|
||||
* @param QueryFactory $queryFactory
|
||||
*/
|
||||
public function __construct(AdapterInterface $adapter, QueryFactory $queryFactory)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
$this->queryFactory = $queryFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aura.SqlQuery
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return SelectQuery
|
||||
*/
|
||||
public function select()
|
||||
{
|
||||
return $this->queryFactory->newSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InsertQuery
|
||||
*/
|
||||
public function insert()
|
||||
{
|
||||
return $this->queryFactory->newInsert();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UpdateQuery
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
return $this->queryFactory->newUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DeleteQuery
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
return $this->queryFactory->newDelete();
|
||||
}
|
||||
|
||||
/**
|
||||
* ENDE: Aura.SqlQuery
|
||||
*/
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$this->adapter->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes simple queries without named parameters
|
||||
*
|
||||
* Use self::perform() for queries with named parameters.
|
||||
*
|
||||
* @param string $query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function exec($query)
|
||||
{
|
||||
$this->adapter->perform($query, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function lastInsertId()
|
||||
{
|
||||
return (int)$this->adapter->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransactionException If transaction is already started
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function beginTransaction()
|
||||
{
|
||||
$this->adapter->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
$this->adapter->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function rollBack()
|
||||
{
|
||||
$this->adapter->rollBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function inTransaction()
|
||||
{
|
||||
return $this->adapter->inTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchAll($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchAll($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchAssoc($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchAssoc($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
* @param bool $includeGroupColumn
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchGroup($query, array $values = [], $includeGroupColumn = false)
|
||||
{
|
||||
return $this->adapter->fetchGroup($query, $values, (bool)$includeGroupColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchRow($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchRow($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchPairs($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchPairs($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return int|float|string|false false on empty result
|
||||
*/
|
||||
public function fetchValue($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchValue($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function fetchAffected($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchAffected($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return array Empty array on empty result
|
||||
*/
|
||||
public function fetchCol($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->fetchCol($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAll($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->yieldAll($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldAssoc($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->yieldAssoc($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldPairs($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->yieldPairs($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return Generator
|
||||
*/
|
||||
public function yieldCol($query, array $values = [])
|
||||
{
|
||||
return $this->adapter->yieldCol($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function perform($query, array $values = [])
|
||||
{
|
||||
$this->adapter->perform($query, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for "BOOLEAN" and (TINY)INT columns
|
||||
*
|
||||
* @param bool|null $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeBool($value, $isNullable = false)
|
||||
{
|
||||
return $this->adapter->escapeBool($value, $isNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for INT columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeInt($value, $isNullable = false)
|
||||
{
|
||||
return $this->adapter->escapeInt($value, $isNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for FLOAT, DOUBLE and REAL columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeDecimal($value, $isNullable = false)
|
||||
{
|
||||
return $this->adapter->escapeDecimal($value, $isNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes values for CHAR, VARCHAR, TEXT and BLOB columns
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $isNullable
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeString($value, $isNullable = false)
|
||||
{
|
||||
return $this->adapter->escapeString($value, $isNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and quotes an identifier (column or table name)
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @throws EscapingException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escapeIdentifier($value)
|
||||
{
|
||||
return $this->adapter->escapeIdentifier($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database;
|
||||
|
||||
final class DatabaseConfig
|
||||
{
|
||||
/** @var string $hostname */
|
||||
private $hostname;
|
||||
|
||||
/** @var string $username */
|
||||
private $username;
|
||||
|
||||
/** @var string $password */
|
||||
private $password;
|
||||
|
||||
/** @var string $database */
|
||||
private $database;
|
||||
|
||||
/** @var string $charset */
|
||||
private $charset;
|
||||
|
||||
/** @var int $port */
|
||||
private $port;
|
||||
|
||||
/** @var array $queries */
|
||||
private $queries;
|
||||
|
||||
/**
|
||||
* @param string $hostname
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $database
|
||||
* @param string|null $charset
|
||||
* @param int|null $port
|
||||
* @param array $queries
|
||||
*/
|
||||
public function __construct(
|
||||
$hostname,
|
||||
$username,
|
||||
$password,
|
||||
$database,
|
||||
$charset = null,
|
||||
$port = null,
|
||||
array $queries = []
|
||||
) {
|
||||
$this->hostname = (string)$hostname;
|
||||
$this->username = (string)$username;
|
||||
$this->password = (string)$password;
|
||||
$this->database = (string)$database;
|
||||
$this->charset = $charset !== null ? (string)$charset : 'utf8';
|
||||
$this->port = $port !== null ? (int)$port : 3306;
|
||||
$this->queries = $queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
*
|
||||
* @return DatabaseConfig
|
||||
*/
|
||||
public static function fromArray(array $config)
|
||||
{
|
||||
return new DatabaseConfig(
|
||||
$config['hostname'],
|
||||
$config['username'],
|
||||
$config['password'],
|
||||
$config['database'],
|
||||
isset($config['charset']) ? $config['charset'] : null,
|
||||
isset($config['port']) ? $config['port'] : null,
|
||||
isset($config['queries']) ? $config['queries'] : []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHostname()
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDatabase()
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCharset()
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getQueries()
|
||||
{
|
||||
return $this->queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'args' => [
|
||||
'hostname' => $this->hostname,
|
||||
'username' => '****',
|
||||
'password' => '****',
|
||||
'database' => $this->database,
|
||||
'charset' => $this->charset,
|
||||
'port' => $this->port,
|
||||
'queries' => $this->queries,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class BindParameterException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class ConfigException extends \InvalidArgumentException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class ConnectionException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in 19.4
|
||||
*/
|
||||
class DatabaseException extends RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface DatabaseExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class EscapingException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class MissingParameterException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class NotConnectedException extends \LogicException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class QueryFailureException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Exception;
|
||||
|
||||
class TransactionException extends \RuntimeException implements DatabaseExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Parser;
|
||||
|
||||
/**
|
||||
* @example
|
||||
* self::rebuild('SELECT * FROM foo WHERE id IN (:ids)', ['ids' => [1, 2, 3]])
|
||||
* Erzeugt:
|
||||
* [
|
||||
* 'statement' => 'SELECT * FROM foo WHERE id IN (:ids_expl_0_, :ids_expl_1_, :ids_expl_2_)',
|
||||
* 'values' => [
|
||||
* '_ids_expl_0_' => 1,
|
||||
* '_ids_expl_1_' => 2,
|
||||
* '_ids_expl_2_' => 3,
|
||||
* ]
|
||||
* ]
|
||||
*/
|
||||
final class MysqliArrayValueParser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
* - Array key 'statement' contains the rebuild statement
|
||||
* - Array key 'values' contains the rebuild bind parameters
|
||||
*/
|
||||
public function rebuild($statement, array $values = [])
|
||||
{
|
||||
return $this->replaceArrayValues($statement, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replaceArrayValues($statement, array $values = [])
|
||||
{
|
||||
foreach ($values as $paramName => $paramValue) {
|
||||
if (is_array($paramValue)) {
|
||||
$counter = 0;
|
||||
$additionalParams = [];
|
||||
foreach ($paramValue as $arrayValue) {
|
||||
$additionalParamName = '_' . $paramName . '_expl_' . $counter . '_';
|
||||
$additionalParams[] = ':' . $additionalParamName;
|
||||
$values[$additionalParamName] = $arrayValue;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
// Replace original named parameter by exploded parameters in statement
|
||||
$replaceString = implode(', ', $additionalParams);
|
||||
$statement = str_replace(':' . $paramName, $replaceString, $statement);
|
||||
|
||||
// Remove original parameter value
|
||||
unset($values[$paramName]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'statement' => $statement,
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Parser;
|
||||
|
||||
use Xentral\Components\Database\Exception\MissingParameterException;
|
||||
|
||||
/**
|
||||
* Responsibility of this class is to make sql statement and bind values compatible with mysqli
|
||||
*
|
||||
* (Mysqli does not support named parameters)
|
||||
*
|
||||
* It does this by:
|
||||
* - Replacing named parameters (:param) by ?-Placeholder (in statement)
|
||||
* - Rearranging bind values in order of appearance of named parameters
|
||||
*/
|
||||
final class MysqliNamedParameterParser implements ParserInterface
|
||||
{
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rebuild($statement, array $values = [])
|
||||
{
|
||||
return $this->replaceNamedParameters($statement, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @throws MissingParameterException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function replaceNamedParameters($statement, array $values = [])
|
||||
{
|
||||
$result = [
|
||||
'statement' => $statement,
|
||||
'values' => [],
|
||||
'params' => [],
|
||||
];
|
||||
|
||||
if (empty($values)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Split statement on named parameters
|
||||
$parts = preg_split('/(:[a-zA-Z0-9_]+)/um', $statement, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
foreach ($parts as &$part) {
|
||||
if (strpos($part, ':') !== 0) {
|
||||
continue; // SQL part does not contain named parameter
|
||||
}
|
||||
|
||||
$parameterName = substr_replace($part, '', 0, 1);
|
||||
if (!array_key_exists($parameterName, $values)) {
|
||||
throw new MissingParameterException(sprintf(
|
||||
'Parameter "%s" is missing from the bound values',
|
||||
$parameterName
|
||||
));
|
||||
}
|
||||
|
||||
// Push values in same order of parameters for binding
|
||||
$result['values'][] = $values[$parameterName];
|
||||
$result['params'][] = $parameterName; // For debugging only
|
||||
|
||||
// Replace named parameter by ?-Placeholder
|
||||
$part = '?';
|
||||
}
|
||||
unset($part);
|
||||
|
||||
// Rebuild statement from (changed) parts
|
||||
$result['statement'] = implode('', $parts);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Parser;
|
||||
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* @param string $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
* - Array key 'statement' contains the corrected statement
|
||||
* - Array key 'values' contains the corrected bind values
|
||||
*/
|
||||
public function rebuild($statement, array $values = []);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Profiler;
|
||||
|
||||
use Exception;
|
||||
use Xentral\Components\Logger\LoggerInterface;
|
||||
|
||||
final class Profiler implements ProfilerInterface
|
||||
{
|
||||
/** @var LoggerInterface $logger */
|
||||
private $logger;
|
||||
|
||||
/** @var bool $active */
|
||||
private $active = false;
|
||||
|
||||
/** @var string $logLevel */
|
||||
private $logLevel = 'debug';
|
||||
|
||||
/** @var string $logFormat */
|
||||
private $logFormat = "{method} ({duration}): {statement} \n{backtrace}";
|
||||
|
||||
/** @var array $context */
|
||||
private $context = [];
|
||||
|
||||
/** @var array $contexts */
|
||||
private $contexts = [];
|
||||
|
||||
/**
|
||||
* @param LoggerInterface $logger
|
||||
*/
|
||||
public function __construct(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
* @param string $methodName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function start($className, $methodName)
|
||||
{
|
||||
if (!$this->active) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->context = [
|
||||
'class' => $className,
|
||||
'method' => $methodName,
|
||||
'start' => microtime(true),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finish($statement = null, array $values = [])
|
||||
{
|
||||
if (!$this->active) {
|
||||
return;
|
||||
}
|
||||
|
||||
$finish = microtime(true);
|
||||
$exception = new Exception();
|
||||
|
||||
$this->context['finish'] = $finish;
|
||||
$this->context['duration_real'] = $finish - $this->context['start'];
|
||||
$this->context['duration'] = sprintf('%.6f', $this->context['duration_real']) . ' seconds';
|
||||
$this->context['statement'] = $statement;
|
||||
$this->context['bindings'] = $values;
|
||||
$this->context['backtrace'] = $exception->getTraceAsString();
|
||||
|
||||
$this->logger->log($this->logLevel, $this->logFormat, $this->context);
|
||||
|
||||
$this->contexts[] = $this->context;
|
||||
$this->context = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $active
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setActive($active)
|
||||
{
|
||||
$this->active = (bool)$active;
|
||||
}
|
||||
|
||||
/**
|
||||
* /**
|
||||
* @return string
|
||||
*/
|
||||
public function getLogLevel()
|
||||
{
|
||||
return $this->logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $logLevel
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLogLevel($logLevel)
|
||||
{
|
||||
$this->logLevel = (string)$logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LoggerInterface
|
||||
*/
|
||||
public function getLogger()
|
||||
{
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getContexts()
|
||||
{
|
||||
return $this->contexts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\Profiler;
|
||||
|
||||
use Xentral\Components\Logger\LoggerInterface;
|
||||
|
||||
interface ProfilerInterface
|
||||
{
|
||||
/**
|
||||
* @param string $className
|
||||
* @param string $methodName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function start($className, $methodName);
|
||||
|
||||
/**
|
||||
* @param string|null $statement
|
||||
* @param array $values
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finish($statement = null, array $values = []);
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive();
|
||||
|
||||
/**
|
||||
* @param bool $active
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setActive($active);
|
||||
|
||||
/**
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLogLevel();
|
||||
|
||||
/**
|
||||
* @param string $logLevel
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLogLevel($logLevel);
|
||||
|
||||
/**
|
||||
* @return LoggerInterface
|
||||
*/
|
||||
public function getLogger();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\Mysql\Delete;
|
||||
|
||||
final class DeleteQuery extends Delete
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\Mysql\Insert;
|
||||
|
||||
final class InsertQuery extends Insert
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\AbstractQuery;
|
||||
use Aura\SqlQuery\QueryFactory as AuraQueryFactory;
|
||||
|
||||
final class QueryFactory extends AuraQueryFactory
|
||||
{
|
||||
/**
|
||||
* @return SelectQuery
|
||||
*/
|
||||
public function newSelect()
|
||||
{
|
||||
return $this->newInstance('Select');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InsertQuery
|
||||
*/
|
||||
public function newInsert()
|
||||
{
|
||||
$insert = $this->newInstance('Insert');
|
||||
$insert->setLastInsertIdNames($this->last_insert_id_names);
|
||||
|
||||
return $insert;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UpdateQuery
|
||||
*/
|
||||
public function newUpdate()
|
||||
{
|
||||
return $this->newInstance('Update');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DeleteQuery
|
||||
*/
|
||||
public function newDelete()
|
||||
{
|
||||
return $this->newInstance('Delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query The query object type.
|
||||
*
|
||||
* @return AbstractQuery
|
||||
*/
|
||||
protected function newInstance($query)
|
||||
{
|
||||
$class = "Xentral\\Components\\Database\\SqlQuery\\{$query}Query";
|
||||
|
||||
return new $class(
|
||||
$this->getQuoter(),
|
||||
$this->newSeqBindPrefix()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\Mysql\Select;
|
||||
use Closure;
|
||||
|
||||
final class SelectQuery extends Select
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasOrderBy()
|
||||
{
|
||||
return !empty($this->order_by);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $andor
|
||||
* @param array|callable $args
|
||||
*
|
||||
* @return Select
|
||||
*/
|
||||
protected function addWhere($andor, $args)
|
||||
{
|
||||
if ($args[0] instanceof Closure) {
|
||||
$this->addClauseCondClosure('where', $andor, $args[0]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
return parent::addWhere($andor, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aura.SqlQuery 2.x unterstützt keine Klammersetzung in WHERE-Bedingungen
|
||||
*
|
||||
* @see https://github.com/auraphp/Aura.SqlQuery/issues/97
|
||||
*
|
||||
* Feature aus Version 3 importiert: https://github.com/auraphp/Aura.SqlQuery/pull/136/files
|
||||
*
|
||||
* @copyright Paul M. Jones
|
||||
* @license MIT
|
||||
*
|
||||
* @param string $clause
|
||||
* @param string $andor
|
||||
* @param callable $closure
|
||||
*/
|
||||
protected function addClauseCondClosure($clause, $andor, $closure)
|
||||
{
|
||||
// retain the prior set of conditions, and temporarily reset the clause
|
||||
// for the closure to work with (otherwise there will be an extraneous
|
||||
// opening AND/OR keyword)
|
||||
$set = $this->$clause;
|
||||
$this->$clause = [];
|
||||
|
||||
// invoke the closure, which will re-populate the $this->$clause
|
||||
$closure($this);
|
||||
|
||||
// are there new clause elements?
|
||||
if (!$this->$clause) {
|
||||
// no: restore the old ones, and done
|
||||
$this->$clause = $set;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// append an opening parenthesis to the prior set of conditions,
|
||||
// with AND/OR as needed ...
|
||||
if ($set) {
|
||||
$set[] = "{$andor} (";
|
||||
} else {
|
||||
$set[] = "(";
|
||||
}
|
||||
|
||||
// append the new conditions to the set, with indenting
|
||||
foreach ($this->$clause as $cond) {
|
||||
$set[] = " {$cond}";
|
||||
}
|
||||
$set[] = ")";
|
||||
|
||||
// ... then put the full set of conditions back into $this->$clause
|
||||
$this->$clause = $set;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Components\Database\SqlQuery;
|
||||
|
||||
use Aura\SqlQuery\Mysql\Update;
|
||||
|
||||
final class UpdateQuery extends Update
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# Data Manipulation
|
||||
|
||||
## `perform()`
|
||||
|
||||
Für alle SQL Anweisung außer `SELECT`. Die Methode gibt nichts zurück. Sollte die Ausführung
|
||||
fehlschlagen, so wird eine Exception geworfen.
|
||||
|
||||
```php
|
||||
$sql = 'UPDATE foo SET bar = :bar WHERE id = :id';
|
||||
|
||||
$values = [
|
||||
'bar' => 'baz',
|
||||
'id' => 123,
|
||||
];
|
||||
|
||||
$db->perform($sql, $values);
|
||||
```
|
||||
|
||||
## `fetchAffected()`
|
||||
|
||||
Für `INSERT`, `REPLACE`, `UPDATE` und `DELETE` Anweisungen. Gibt die Anzahl der betroffenen Datensätze zurück.
|
||||
|
||||
```php
|
||||
$sql = 'UPDATE foo SET bar = :bar WHERE 1';
|
||||
|
||||
$values = [
|
||||
'bar' => 'baz',
|
||||
];
|
||||
|
||||
echo $db->fetchAffected($sql, $values);
|
||||
```
|
||||
|
||||
###### Ausgabe
|
||||
|
||||
```
|
||||
42
|
||||
```
|
||||
|
||||
## `lastInsertId()`
|
||||
|
||||
Gibt den zuletzt erzeugten Auto-Increment-Wert zurück.
|
||||
|
||||
```php
|
||||
$sql = 'INSERT INTO foo (id, bar) VALUES (NULL, :bar)';
|
||||
|
||||
$values = [
|
||||
'bar' => 'baz',
|
||||
];
|
||||
|
||||
$db->perform($sql, $values);
|
||||
// $db->fetchAffected($sql, $values); // Alternative
|
||||
|
||||
echo $db->lastInsertId();
|
||||
```
|
||||
|
||||
###### Ausgabe
|
||||
```
|
||||
123
|
||||
```
|
||||
|
||||
## Multiple-Row-Insert
|
||||
|
||||
```php
|
||||
$sql = 'INSERT INTO foo (id, bar) VALUES (NULL, :bar1, :baz1), (NULL, :bar2, :baz2), (NULL, :bar3, :baz3)';
|
||||
|
||||
$values = [
|
||||
'bar1' => 'bar',
|
||||
'baz1' => 'baz',
|
||||
'bar2' => 'barbar',
|
||||
'baz2' => 'bazbaz',
|
||||
'bar3' => 'barbarbar',
|
||||
'baz3' => 'bazbazbaz',
|
||||
];
|
||||
|
||||
$db->perform($sql, $values);
|
||||
// $db->fetchAffected($sql, $values); // Alternative; Rückgabe wäre `3`
|
||||
```
|
||||
@@ -0,0 +1,158 @@
|
||||
|
||||
# Datensätze abrufen
|
||||
|
||||
## `fetchAll()`
|
||||
|
||||
```php
|
||||
$data = $db->fetchAll('SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'id' => 6,
|
||||
'typ' => 'produkt',
|
||||
'name_de' => 'LED Anzeige RLED 24-8',
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'id' => 7,
|
||||
'typ' => 'produkt',
|
||||
'name_de' => 'Schalter S3 24V 5A',
|
||||
),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchAssoc()`
|
||||
|
||||
Wie `fetchAll()` allerdings wird das Ergebnis der ersten Spalte als Index verwendet.
|
||||
|
||||
```php
|
||||
$data = $db->fetchAssoc('SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
6 =>
|
||||
array (
|
||||
'id' => 6,
|
||||
'typ' => 'produkt',
|
||||
'name_de' => 'LED Anzeige RLED 24-8',
|
||||
),
|
||||
7 =>
|
||||
array (
|
||||
'id' => 7,
|
||||
'typ' => 'produkt',
|
||||
'name_de' => 'Schalter S3 24V 5A',
|
||||
),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchRow()`
|
||||
|
||||
Liefert die Ergebnisse der ersten Zeile als assoziatives Array. Die Spaltennamen werden als Index verwendet.
|
||||
|
||||
```php
|
||||
$data = $db->fetchRow('SELECT a.id, a.name_de, a.name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
'id' => 6,
|
||||
'name_de' => 'LED Anzeige RLED 24-8',
|
||||
'name_en' => '',
|
||||
'logdatei' => '2015-10-26 17:26:27',
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchValue()`
|
||||
|
||||
Liefert nur das Ergebnis der ersten Zeile und Spalte zurück.
|
||||
|
||||
```php
|
||||
$data = $db->fetchValue('SELECT a.name_de, name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
'LED Anzeige RLED 24-8'
|
||||
```
|
||||
|
||||
## `fetchCol()`
|
||||
|
||||
Liefert nur die Ergebnisse der ersten Spalte zurück.
|
||||
|
||||
```php
|
||||
$data = $db->fetchCol('SELECT a.name_de, a.name_en, a.logdatei FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
0 => 'LED Anzeige RLED 24-8',
|
||||
1 => 'Schalter S3 24V 5A',
|
||||
2 => 'Gehäuse GHK5 20x30x10',
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchPairs()`
|
||||
|
||||
Gibt ein (eindimentionales) assoziatives Array zurück. Der Wert der ersten Spalte wird als Index verwendet und die zweite Spalte als Wert.
|
||||
**Erwartet werden genau zwei Spalten, andernfalls wird eine Exception geworfen.**
|
||||
|
||||
```php
|
||||
$data = $db->fetchPairs('SELECT a.nummer, a.name_de FROM artikel AS a WHERE a.id > 5');
|
||||
```
|
||||
```
|
||||
array (
|
||||
700006 => 'LED Anzeige RLED 24-8',
|
||||
700007 => 'Schalter S3 24V 5A',
|
||||
700008 => 'Gehäuse GHK5 20x30x10',
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
## `fetchGroup()`
|
||||
|
||||
Verhält sich wie `fetchAssoc()`; mit der Ausnahme dass die Werte der ersten Spalte gruppiert werden.
|
||||
|
||||
```php
|
||||
$data = $db->fetchGroup(
|
||||
'SELECT a.typ, a.nummer, a.name_de, a.logdatei FROM artikel AS a WHERE a.id > 5'
|
||||
);
|
||||
```
|
||||
```
|
||||
array (
|
||||
'produkt' =>
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'typ' => 'produkt',
|
||||
'nummer' => '700006',
|
||||
'name_de' => 'LED Anzeige RLED 24-8',
|
||||
'logdatei' => '2015-10-26 17:26:27',
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'typ' => 'produkt',
|
||||
'nummer' => '700007',
|
||||
'name_de' => 'Schalter S3 24V 5A',
|
||||
'logdatei' => '2015-10-26 17:26:59',
|
||||
),
|
||||
2 =>
|
||||
array (
|
||||
'typ' => 'produkt',
|
||||
'nummer' => '700008',
|
||||
'name_de' => 'Gehäuse GHK5 20x30x10',
|
||||
'logdatei' => '2018-05-23 05:18:52',
|
||||
),
|
||||
),
|
||||
'gebuehr' =>
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'typ' => 'gebuehr',
|
||||
'nummer' => '100001',
|
||||
'name_de' => 'Versandkosten',
|
||||
'logdatei' => '2018-06-17 10:09:36',
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# Database-Komponente
|
||||
|
||||
###### Verwendete Libraries
|
||||
|
||||
* **Aura.SqlQuery**:
|
||||
* Composer: `aura/sqlquery:2.7.*`
|
||||
* Packagist: https://packagist.org/packages/aura/sqlquery
|
||||
* GitHub: https://github.com/auraphp/Aura.SqlQuery
|
||||
* Docs: https://github.com/auraphp/Aura.SqlQuery/tree/2.x
|
||||
|
||||
* **~~Aura.Sql~~** Wird in 19.4 entfernt
|
||||
* Composer: `aura/sql:3.*`
|
||||
* Packagist: https://packagist.org/packages/aura/sql
|
||||
* GitHub: https://github.com/auraphp/Aura.Sql
|
||||
* Docs: https://github.com/auraphp/Aura.Sql/blob/3.x/docs/index.md
|
||||
|
||||
##### Database-Komponente aus Container holen
|
||||
|
||||
```php
|
||||
$db = $container->get('Database');
|
||||
```
|
||||
|
||||
Im alten Bereich:
|
||||
|
||||
```php
|
||||
$db = $this->app->Container->get('Database');
|
||||
```
|
||||
|
||||
## Themen
|
||||
|
||||
* [Datensätze abrufen (fetch)](fetch_results.md)
|
||||
* [Datensätze abrufen mit Generatoren (yield)](yield_results.md)
|
||||
* [Datensätze ändern](data_manipulation.md)
|
||||
* [Transaktionen](transactions.md)
|
||||
* [Named Parameter / Prepared Statements](named_parameter.md)
|
||||
* [SQL Query Builder](query_builder.md)
|
||||
|
||||
## Exceptions
|
||||
|
||||
Die Database-Komponente verwendet intern `mysqli`. Im Unterschied zu `mysqli` werden in Fehlerfällen aber
|
||||
Exceptions geworfen; z.B.:
|
||||
|
||||
* Wenn die Verbindung zur Datenbank fehlschlägt => `ConnectionException`
|
||||
* Wenn ein SQL-Statement fehlerhaft ist oder aus anderen Gründen nicht erfolgreich ausgeführt
|
||||
werden kann => `QueryFailureException`
|
||||
* Wenn `Named Parameter` fehlen => `MissingParameterException`
|
||||
|
||||
##### ExceptionInterface
|
||||
|
||||
Alle Exceptions die von der Database-Komponente geworfen werden implementieren das
|
||||
`\Xentral\Components\Database\Exception\DatabaseExceptionInterface` Interface.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Named Parameter
|
||||
|
||||
`perform()`, `fetch*()` und `yield*()`-Methoden nehmen als zweiten Parameter ein assoziatives Array entgegen.
|
||||
Mit diesem Array werden lassen sich Werte als *Named Parameter* 'binden'.
|
||||
|
||||
```php
|
||||
$sql =
|
||||
'SELECT a.id, a.nummer, a.name_de
|
||||
FROM artikel AS a
|
||||
WHERE a.typ IN (:types)
|
||||
AND a.nummer LIKE :nummers
|
||||
LIMIT :start, :length';
|
||||
|
||||
$values = [
|
||||
'types' => ['produkt', 'gebuehr'],
|
||||
'nummers' => '7000%',
|
||||
'start' => 0,
|
||||
'length' => 3,
|
||||
];
|
||||
|
||||
$data = $db->fetchAll($sql, $values);
|
||||
```
|
||||
```
|
||||
array (
|
||||
0 =>
|
||||
array (
|
||||
'id' => 1,
|
||||
'nummer' => '700001',
|
||||
'name_de' => 'Schraube M10x20',
|
||||
),
|
||||
1 =>
|
||||
array (
|
||||
'id' => 2,
|
||||
'nummer' => '700002',
|
||||
'name_de' => 'Sechskant-Mutter M10',
|
||||
),
|
||||
2 =>
|
||||
array (
|
||||
'id' => 3,
|
||||
'nummer' => '700003',
|
||||
'name_de' => 'Schalthebel 20x10',
|
||||
),
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# Profiler
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @var \Xentral\Components\Database\Profiler\Profiler $profiler */
|
||||
$profiler = $container->get('DatabaseProfiler');
|
||||
$profiler->setActive(true);
|
||||
|
||||
$database->fetchAll($sql);
|
||||
|
||||
var_dump($profiler->getContexts());
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
# SQL Query Builder
|
||||
|
||||
###### Verwendete Libraries
|
||||
|
||||
* **Aura.SqlQuery**:
|
||||
* Composer: `aura/sqlquery:2.7.*`
|
||||
* Packagist: https://packagist.org/packages/aura/sqlquery
|
||||
* GitHub: https://github.com/auraphp/Aura.SqlQuery
|
||||
* Docs: https://github.com/auraphp/Aura.SqlQuery/tree/2.x
|
||||
|
||||
## Query Builder erzeugen
|
||||
|
||||
```php
|
||||
$select = $db->select();
|
||||
$update = $db->update();
|
||||
$insert = $db->insert();
|
||||
$delete = $db->delete();
|
||||
```
|
||||
|
||||
## SELECT-Query
|
||||
|
||||
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#select
|
||||
|
||||
###### Beispiel mit Named-Placeholder
|
||||
|
||||
```php
|
||||
$select = $db->select();
|
||||
$select
|
||||
->cols(['u.id', 'u.description'])
|
||||
->from('user AS u')
|
||||
->where('u.id = :user_id')
|
||||
->bindValue('user_id', 1);
|
||||
|
||||
$result = $db->fetchAll(
|
||||
$select->getStatement(),
|
||||
$select->getBindValues()
|
||||
);
|
||||
|
||||
var_export($result);
|
||||
// array(
|
||||
// 0 => array(
|
||||
// 'id' => 1,
|
||||
// 'description' => 'Administrator',
|
||||
// ),
|
||||
// )
|
||||
```
|
||||
|
||||
###### Alternative mit ?-Placeholder
|
||||
|
||||
```php
|
||||
$select = $db->select();
|
||||
$select
|
||||
->cols(['u.id', 'u.description'])
|
||||
->from('user AS u')
|
||||
->where('u.id = ?', 1);
|
||||
```
|
||||
|
||||
###### Beispiel mit Verschachtelung im WHERE
|
||||
|
||||
```php
|
||||
$select = $db->select();
|
||||
$select
|
||||
->cols(['u.id', 'u.description'])
|
||||
->from('user AS u')
|
||||
->where('u.activ = ?', 1)
|
||||
->where(function (SelectQuery $query) {
|
||||
$query->where('u.type = ?', 'admin')
|
||||
->orWhere('u.type = ?', 'standard');
|
||||
});
|
||||
|
||||
echo $select->getStatement();
|
||||
```
|
||||
```sql
|
||||
SELECT
|
||||
`u`.`id`,
|
||||
`u`.`description`
|
||||
FROM
|
||||
`user` AS `u`
|
||||
WHERE
|
||||
`u`.`activ` = :_1_1_
|
||||
AND (
|
||||
`u`.`type` = :_1_2_
|
||||
OR `u`.`type` = :_1_3_
|
||||
)
|
||||
```
|
||||
|
||||
## INSERT-Query
|
||||
|
||||
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#insert
|
||||
|
||||
## UPDATE-Query
|
||||
|
||||
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#update
|
||||
|
||||
## DELETE-Query
|
||||
|
||||
https://github.com/auraphp/Aura.SqlQuery/tree/2.x#delete
|
||||
@@ -0,0 +1,25 @@
|
||||
# Transaktionnen
|
||||
|
||||
## Transaktion starten
|
||||
|
||||
```php
|
||||
$db->beginTransaction();
|
||||
```
|
||||
|
||||
## Transaktion übernehmen / Commit
|
||||
|
||||
```php
|
||||
$db->commit();
|
||||
```
|
||||
|
||||
## Transaktion zurücknehmen / Rollback
|
||||
|
||||
```php
|
||||
$db->rollback();
|
||||
```
|
||||
|
||||
## Prüfen ob Transaktion gestartet ist
|
||||
|
||||
```php
|
||||
$db->inTransaction();
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
|
||||
# Datensätze abrufen mit Generatoren
|
||||
|
||||
Um den Arbeitsspeicherverbrauch gering zu halten bieten sich zum Iterieren von großen Datenmengen
|
||||
Generatoren an: https://www.php.net/manual/de/language.generators.overview.php
|
||||
|
||||
Die Database-Komponente stellt für diesen Zweck `yield`-Methoden zur Verfügung.
|
||||
|
||||
|
||||
## `yieldAll()`
|
||||
|
||||
Wie `fetchAll()`; jede Zeile ist ein assoziatives Array.
|
||||
|
||||
```php
|
||||
$statement = 'SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
|
||||
$bindValues = ['typ' => 'produkt'];
|
||||
|
||||
foreach ($db->yieldAll($statement, $bindValues) as $row) {
|
||||
var_dump($row);
|
||||
}
|
||||
```
|
||||
```
|
||||
array (size=3)
|
||||
'id' => int 2
|
||||
'typ' => string 'produkt' (length=7)
|
||||
'name_de' => string 'Sechskant-Mutter M10' (length=20)
|
||||
|
||||
array (size=3)
|
||||
'id' => int 3
|
||||
'typ' => string 'produkt' (length=7)
|
||||
'name_de' => string 'Schalthebel 20x10' (length=17)
|
||||
```
|
||||
|
||||
|
||||
## `yieldAssoc()`
|
||||
|
||||
Wie `fetchAssoc()`; jede Zeile ist ein assoziatives Array; der Key beinhaltet den Wert der ersten Spalte
|
||||
|
||||
```php
|
||||
$statement = 'SELECT a.id, a.typ, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
|
||||
$bindValues = ['typ' => 'produkt'];
|
||||
|
||||
foreach ($db->yieldAssoc($statement, $bindValues) as $key => $row) {
|
||||
var_dump($key);
|
||||
var_dump($row);
|
||||
}
|
||||
```
|
||||
```
|
||||
int 2
|
||||
array (size=3)
|
||||
'id' => int 2
|
||||
'typ' => string 'produkt' (length=7)
|
||||
'name_de' => string 'Sechskant-Mutter M10' (length=20)
|
||||
|
||||
int 3
|
||||
array (size=3)
|
||||
'id' => int 3
|
||||
'typ' => string 'produkt' (length=7)
|
||||
'name_de' => string 'Schalthebel 20x10' (length=17)
|
||||
```
|
||||
|
||||
|
||||
## `yieldPairs()`
|
||||
|
||||
Wie `fetchPairs()`; jede Zeile besteht aus Key-Value-Paaren; der Key beinhaltet den Inhalt der ersten Spalte;
|
||||
der Wert den Inhalt der zweiten Spalte.
|
||||
|
||||
**Erwartet werden genau zwei Spalten, andernfalls wird eine Exception geworfen.**
|
||||
|
||||
```php
|
||||
$statement = 'SELECT a.id, a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
|
||||
$bindValues = ['typ' => 'produkt'];
|
||||
|
||||
foreach ($db->yieldPairs($statement, $bindValues) as $key => $value) {
|
||||
var_dump($key);
|
||||
var_dump($value);
|
||||
}
|
||||
```
|
||||
```
|
||||
int 2
|
||||
string 'Sechskant-Mutter M10' (length=20)
|
||||
|
||||
int 3
|
||||
string 'Schalthebel 20x10' (length=17)
|
||||
```
|
||||
|
||||
|
||||
## `yieldCol()`
|
||||
|
||||
Wie `fetchCol()`; jede Zeile beinhaltet nur den Wert der ersten Spalte.
|
||||
|
||||
```php
|
||||
$statement = 'SELECT a.name_de FROM artikel AS a WHERE a.typ = :typ LIMIT 2';
|
||||
$bindValues = ['typ' => 'produkt'];
|
||||
|
||||
foreach ($db->yieldCol($statement, $bindValues) as $key => $value) {
|
||||
var_dump($key);
|
||||
var_dump($value);
|
||||
}
|
||||
```
|
||||
```
|
||||
int 0
|
||||
string 'Sechskant-Mutter M10' (length=20)
|
||||
|
||||
int 1
|
||||
string 'Schalthebel 20x10' (length=17)
|
||||
```
|
||||
Reference in New Issue
Block a user