Merge branch 'holiday-branch' into 'master'

Release 1.2.0-RC4

See merge request fuzeworks/Database!1
This commit is contained in:
Abel Hoogeveen 2019-08-21 17:38:48 +00:00
commit 1c4029d094
15 changed files with 1275 additions and 66 deletions

View File

@ -15,13 +15,13 @@
],
"require": {
"php": ">=7.1.0",
"fuzeworks/core": "1.2.0-RC3",
"fuzeworks/mvcr": "1.2.0-RC3",
"fuzeworks/core": "1.2.0-RC4",
"fuzeworks/mvcr": "1.2.0-RC4",
"ext-pdo": "*"
},
"require-dev": {
"phpunit/phpunit": "^7",
"fuzeworks/tracycomponent": "1.2.0-RC3"
"fuzeworks/tracycomponent": "1.2.0-RC4"
},
"autoload": {
"psr-4": {

View File

@ -36,10 +36,15 @@
namespace FuzeWorks;
use FuzeWorks\DatabaseEngine\iDatabaseEngine;
use FuzeWorks\DatabaseEngine\MongoEngine;
use FuzeWorks\DatabaseEngine\PDOEngine;
use FuzeWorks\Event\DatabaseLoadDriverEvent;
use FuzeWorks\Event\DatabaseLoadTableModelEvent;
use FuzeWorks\Exception\DatabaseException;
use FuzeWorks\Exception\EventException;
use FuzeWorks\Model\iDatabaseTableModel;
use FuzeWorks\Model\MongoTableModel;
use FuzeWorks\Model\PDOTableModel;
/**
* Database loading class
@ -67,6 +72,13 @@ class Database
*/
protected $engines = [];
/**
* All tableModels that can be used for connections
*
* @var iDatabaseTableModel[]
*/
protected $tableModels = [];
/**
* Whether all DatabaseEngines have been loaded yet
*
@ -75,11 +87,18 @@ class Database
protected $enginesLoaded = false;
/**
* Array of all the non-default databases
* Array of all the database engines
*
* @var iDatabaseEngine[]
*/
protected $connections = [];
/**
* Array of all the tableModels
*
* @var iDatabaseTableModel[]
*/
protected $tables;
/**
* Register with the TracyBridge upon startup
@ -111,13 +130,16 @@ class Database
* @param array $parameters
* @return iDatabaseEngine
* @throws DatabaseException
* @throws EventException
*/
public function get(string $connectionName = 'default', string $engineName = '', array $parameters = []): iDatabaseEngine
{
// Fire the event to allow settings to be changed
/** @var DatabaseLoadDriverEvent $event */
$event = Events::fireEvent('databaseLoadDriverEvent', strtolower($engineName), $parameters, $connectionName);
try {
$event = Events::fireEvent('databaseLoadDriverEvent', strtolower($engineName), $parameters, $connectionName);
} catch (EventException $e) {
throw new DatabaseException("Could not get database. databaseLoadDriverEvent threw exception: '".$e->getMessage()."'");
}
if ($event->isCancelled())
throw new DatabaseException("Could not get database. Cancelled by databaseLoadDriverEvent.");
@ -138,7 +160,7 @@ class Database
elseif (!empty($event->engineName) && !empty($event->parameters))
{
// Do provided config third
$engineClass = get_class($this->getEngine($event->engineName));
$engineClass = get_class($this->fetchEngine($event->engineName));
$engine = $this->connections[$event->connectionName] = new $engineClass();
$engine->setUp($event->parameters);
}
@ -149,7 +171,7 @@ class Database
throw new DatabaseException("Could not get database. Database not found in config.");
$engineName = $this->dbConfig['connections'][$event->connectionName]['engineName'];
$engineClass = get_class($this->getEngine($engineName));
$engineClass = get_class($this->fetchEngine($engineName));
$engine = $this->connections[$event->connectionName] = new $engineClass();
$engine->setUp($this->dbConfig['connections'][$event->connectionName]);
}
@ -161,6 +183,51 @@ class Database
return $engine;
}
/**
* @param string $tableName
* @param string $tableModelName
* @param string $connectionName
* @param array $parameters
* @return iDatabaseTableModel
* @throws DatabaseException
*/
public function getTableModel(string $tableName, string $tableModelName, string $connectionName = 'default', array $parameters = []): iDatabaseTableModel
{
try {
/** @var DatabaseLoadTableModelEvent $event */
$event = Events::fireEvent('databaseLoadTableModelEvent', strtolower($tableModelName), $parameters, $connectionName, $tableName);
} catch (EventException $e) {
throw new DatabaseException("Could not get TableModel. databaseLoadTableModelEvent threw exception: '" . $e->getMessage() . "'");
}
if ($event->isCancelled())
throw new DatabaseException("Could not get TableModel. Cancelled by databaseLoadTableModelEvent.");
/** @var iDatabaseTableModel $tableModel */
// If a TableModel is provided by the event, use that. Otherwise search in the list of tableModels
if (is_object($event->tableModel) && $event->tableModel instanceof iDatabaseTableModel)
{
$tableModel = $this->tables[$event->connectionName . "|" . $event->tableName] = $event->tableModel;
if (!$tableModel->setup())
$tableModel->setUp($this->get($event->connectionName, $tableModel->getEngineName(), $event->parameters), $event->tableName);
}
// If the connection already exists, use that
elseif (isset($this->tables[$event->connectionName . "|" . $event->tableName]))
{
$tableModel = $this->tables[$event->connectionName . "|" . $event->tableName];
}
// Otherwise use the provided configuration
else
{
$tableModelClass = get_class($this->fetchTableModel($event->tableModelName));
$tableModel = $this->tables[$event->connectionName . "|" . $event->tableName] = new $tableModelClass();
$tableModel->setUp($this->get($event->connectionName, $tableModel->getEngineName(), $event->parameters), $event->tableName);
}
// And return the tableModel
return $tableModel;
}
/**
* Get a loaded database engine.
*
@ -168,13 +235,13 @@ class Database
* @return iDatabaseEngine
* @throws DatabaseException
*/
public function getEngine(string $engineName): iDatabaseEngine
public function fetchEngine(string $engineName): iDatabaseEngine
{
// First retrieve the name
$engineName = strtolower($engineName);
// Then load all engines
$this->loadDatabaseEngines();
$this->loadDatabaseComponents();
// If the engine exists, return it
if (isset($this->engines[$engineName]))
@ -184,6 +251,29 @@ class Database
throw new DatabaseException("Could not get engine. Engine does not exist.");
}
/**
* Fetch a loaded TableModel
*
* @param string $tableModelName
* @return iDatabaseTableModel
* @throws DatabaseException
*/
public function fetchTableModel(string $tableModelName): iDatabaseTableModel
{
// First retrieve the name
$tableModelName = strtolower($tableModelName);
// Then load all the tableModels
$this->loadDatabaseComponents();
// If the tableModel exists, return it
if (isset($this->tableModels[$tableModelName]))
return $this->tableModels[$tableModelName];
// Otherwise throw an exception
throw new DatabaseException("Could not get tableModel. TableModel does not exist.");
}
/**
* Register a new database engine
*
@ -208,13 +298,36 @@ class Database
return true;
}
/**
* Register a new database tableModel
*
* @param iDatabaseTableModel $tableModel
* @return bool
* @throws DatabaseException
*/
public function registerTableModel(iDatabaseTableModel $tableModel): bool
{
// First retrieve the name
$tableModelName = strtolower($tableModel->getName());
// Check if the tableModel is already set
if (isset($this->tableModels[$tableModelName]))
throw new DatabaseException("Could not register tableModel. TableModel '" . $tableModelName . "' already registered.");
// Install it
$this->tableModels[$tableModelName] = $tableModel;
Logger::log("Registered TableModel type: '" . $tableModelName . "'");
return true;
}
/**
* Load all databaseEngines by firing a databaseLoadEngineEvent and by loading all the default engines
*
* @return bool
* @throws DatabaseException
*/
protected function loadDatabaseEngines(): bool
protected function loadDatabaseComponents(): bool
{
// If already loaded, skip
if ($this->enginesLoaded)
@ -229,6 +342,11 @@ class Database
// Load the engines provided by the DatabaseComponent
$this->registerEngine(new PDOEngine());
$this->registerEngine(new MongoEngine());
// Load the tableModels provided by the DatabaseComponent
$this->registerTableModel(new PDOTableModel());
$this->registerTableModel(new MongoTableModel());
// And save results
$this->enginesLoaded = true;

View File

@ -0,0 +1,206 @@
<?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 MongoDB\Driver\Monitoring\CommandFailedEvent;
use MongoDB\Driver\Monitoring\CommandStartedEvent;
use MongoDB\Driver\Monitoring\CommandSubscriber;
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use Tracy\Debugger;
class MongoCommandSubscriber implements CommandSubscriber
{
/**
* @var MongoEngine
*/
private $mongoEngine;
/**
* @var float
*/
private $commandTimings = 0.0;
/**
* @var string
*/
private $queryString;
public function __construct(MongoEngine $engine)
{
$this->mongoEngine = $engine;
}
/**
* Notification method for a failed command.
* If the subscriber has been registered with MongoDB\Driver\Monitoring\addSubscriber(), the driver will call this method when a command has failed.
* @link https://secure.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandfailed.php
* @param CommandFailedEvent $event An event object encapsulating information about the failed command.
* @return void
* @throws \InvalidArgumentException on argument parsing errors.
* @since 1.3.0
*/
public function commandFailed(CommandFailedEvent $event)
{
// TODO: Implement commandFailed() method.
}
/**
* Notification method for a started command.
* If the subscriber has been registered with MongoDB\Driver\Monitoring\addSubscriber(), the driver will call this method when a command has started.
* @link https://secure.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandstarted.php
* @param CommandStartedEvent $event An event object encapsulating information about the started command.
* @return void
* @throws \InvalidArgumentException on argument parsing errors.
* @since 1.3.0
*/
public function commandStarted(CommandStartedEvent $event)
{
$this->commandTimings = microtime(true);
$this->queryString = strtoupper($event->getCommandName());
// Determine query string
$command = $event->getCommand();
$this->queryString .= ' \'' . $event->getDatabaseName() . '.' . $command->{$event->getCommandName()} . '\'';
// If a projection is provided, print it
if (isset($command->projection))
{
$projection = $command->projection;
$projectionStrings = [];
foreach ($projection as $projectionKey => $projectionVal)
$projectionStrings[] = $projectionKey;
$this->queryString .= " PROJECT[" . implode(',', $projectionStrings) . ']';
}
// If a filter is provided, print it
if (isset($command->filter) && !empty((array) $command->filter))
{
$filter = $command->filter;
$filterStrings = [];
foreach ($filter as $filterKey => $filterVal)
$filterStrings[] = $filterKey;
$this->queryString .= " FILTER[" . implode(',', $filterStrings) . ']';
}
// If a sort is provided, print it
if (isset($command->sort))
{
$sort = $command->sort;
$sortStrings = [];
foreach ($sort as $sortKey => $sortVal)
$sortStrings[] = $sortKey . ($sortVal == 1 ? ' ASC' : ' DESC');
$this->queryString .= " SORT[" . implode(',', $sortStrings) . ']';
}
// If documents are provided, print it
if (isset($command->documents))
{
$documents = $command->documents;
$documentKeys = [];
foreach ($documents as $document)
$documentKeys = array_merge($documentKeys, array_keys((array) $document));
$this->queryString .= " VALUES[" . implode(',', $documentKeys) . ']';
}
// If a deletes is provided, print it
if (isset($command->deletes))
{
$deletes = $command->deletes;
$deleteKeys = [];
foreach ($deletes as $delete)
{
if (!isset($delete->q))
continue;
$deleteKeys = array_merge($deleteKeys, array_keys((array) $delete->q));
}
$this->queryString .= " FILTER[" . implode(',', $deleteKeys) . ']';
}
// If a limit is provided, print it
if (isset($command->limit))
$this->queryString .= " LIMIT(".$command->limit.")";
}
/**
* Notification method for a successful command.
* If the subscriber has been registered with MongoDB\Driver\Monitoring\addSubscriber(), the driver will call this method when a command has succeeded.
* @link https://secure.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandsucceeded.php
* @param CommandSucceededEvent $event An event object encapsulating information about the successful command.
* @return void
* @throws \InvalidArgumentException on argument parsing errors.
* @since 1.3.0
*/
public function commandSucceeded(CommandSucceededEvent $event)
{
// Get variables
$queryTimings = microtime(true) - $this->commandTimings;
$queryString = $this->queryString;
$queryData = 0;
switch ($event->getCommandName())
{
case 'find':
$queryData = count($event->getReply()->cursor->firstBatch);
break;
case 'insert':
$queryData = $event->getReply()->n;
break;
case 'update':
$queryData = $event->getReply()->n;
break;
case 'delete':
$queryData = $event->getReply()->n;
break;
}
// And log query
$this->mongoEngine->logMongoQuery($queryString, $queryData, $queryTimings, []);
// And reset timings
$this->commandTimings = 0.0;
}
}

View File

@ -0,0 +1,234 @@
<?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;
use MongoDB\ChangeStream;
use MongoDB\Client;
use MongoDB\Collection;
use MongoDB\Database;
use MongoDB\Driver\Exception\InvalidArgumentException;
use MongoDB\Driver\Manager;
use function MongoDB\Driver\Monitoring\addSubscriber;
use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Session;
use MongoDB\Driver\WriteConcern;
use MongoDB\Exception\RuntimeException;
use MongoDB\Model\DatabaseInfoIterator;
/**
* Class MongoEngine
*
* The following additional methods can be accessed through the __call method
* @method array|object dropDatabase(string $databaseName, array $options = [])
* @method Manager getManager()
* @method ReadConcern getReadConcern()
* @method ReadPreference getReadPreference()
* @method array getTypeMap()
* @method WriteConcern getWriteConcern()
* @method DatabaseInfoIterator listDatabases(array $options = [])
* @method Collection selectCollection(string $databaseName, string $collectionName, array $options = [])
* @method Database selectDatabase(string $databaseName, array $options = [])
* @method Session startSession(array $options = [])
* @method ChangeStream watch(array $pipeline = [], array $options = [])
*/
class MongoEngine extends DatabaseDriver implements iDatabaseEngine
{
/**
* Whether the Engine has been set up
*
* @var bool
*/
protected $setUp = false;
/**
* @var Client
*/
protected $mongoConnection;
/**
* Connection string with the database
*
* @var string
*/
protected $uri;
/**
* Returns the name of this engine
*
* @return string
*/
public function getName(): string
{
return 'mongo';
}
public function getConnectionDescription(): string
{
if (is_null($this->mongoConnection))
return 'none';
return $this->uri;
}
/**
* 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
$this->uri = isset($parameters['uri']) ? $parameters['uri'] : null;
$uriOptions = isset($parameters['uriOptions']) ? $parameters['uriOptions'] : [];
$driverOptions = isset($parameters['driverOptions']) ? $parameters['driverOptions'] : [];
// Don't attempt and connect without a URI
if (is_null($this->uri))
throw new DatabaseException("Could not setUp MongoEngine. No URI provided");
// Import username and password
if (isset($parameters['username']) && isset($parameters['password']))
{
$uriOptions['username'] = $parameters['username'];
$uriOptions['password'] = $parameters['password'];
}
// And set FuzeWorks app name
$uriOptions['appname'] = 'FuzeWorks';
try {
$this->mongoConnection = new Client($this->uri, $uriOptions, $driverOptions);
} catch (InvalidArgumentException | RuntimeException $e) {
throw new DatabaseException("Could not setUp MongoEngine. MongoDB threw exception: '" . $e->getMessage() . "'");
}
// Set this engine as set up
$this->setUp = true;
// Register subscriber
$subscriber = new MongoCommandSubscriber($this);
addSubscriber($subscriber);
// And return true upon success
return true;
}
public function logMongoQuery(string $queryString, int $queryData, float $queryTimings, array $errorInfo = [])
{
$this->logQuery($queryString, $queryData, $queryTimings, $errorInfo);
}
/**
* Method called by \FuzeWorks\Database to tear down the database connection upon shutdown
*
* @return bool
*/
public function tearDown(): bool
{
// MongoDB does not require any action. Always return true
return true;
}
/**
* Call methods on the Mongo Connection
*
* @param $name
* @param $arguments
* @return mixed
*/
public function __call($name, $arguments)
{
return $this->mongoConnection->{$name}(...$arguments);
}
/**
* Get properties from the Mongo Connection
*
* @param $name
* @return Database
*/
public function __get($name): Database
{
return $this->mongoConnection->$name;
}
/**
* @return bool
*/
public function transactionStart(): bool
{
// TODO: Implement transactionStart() method.
}
/**
* @return bool
*/
public function transactionEnd(): bool
{
// TODO: Implement transactionEnd() method.
}
/**
* @return bool
*/
public function transactionCommit(): bool
{
// TODO: Implement transactionCommit() method.
}
/**
* @return bool
*/
public function transactionRollback(): bool
{
// TODO: Implement transactionRollback() method.
}
}

View File

@ -36,6 +36,7 @@
namespace FuzeWorks\DatabaseEngine;
use FuzeWorks\Exception\DatabaseException;
use FuzeWorks\Exception\TransactionException;
use FuzeWorks\Logger;
use PDO;
use PDOException;
@ -156,6 +157,7 @@ class PDOEngine extends DatabaseDriver implements iDatabaseEngine
* Method called by \FuzeWorks\Database to tear down the database connection upon shutdown
*
* @return bool
* @throws TransactionException
*/
public function tearDown(): bool
{
@ -209,8 +211,9 @@ class PDOEngine extends DatabaseDriver implements iDatabaseEngine
* @param string $queryString
* @param int $queryData
* @param float $queryTimings
* @param array $errorInfo
*/
public function logPDOQuery(string $queryString, int $queryData, float $queryTimings, $errorInfo = [])
public function logPDOQuery(string $queryString, int $queryData, float $queryTimings, array $errorInfo = [])
{
$errorInfo = empty($errorInfo) ? $this->error() : $errorInfo;
$this->logQuery($queryString, $queryData, $queryTimings, $errorInfo);
@ -236,7 +239,7 @@ class PDOEngine extends DatabaseDriver implements iDatabaseEngine
$benchmarkEnd = microtime(true) - $benchmarkStart;
// Log the query
$this->logPDOQuery($sql, [], $benchmarkEnd);
$this->logPDOQuery($sql, 0, $benchmarkEnd);
// If the query failed, handle the error
if ($result === false)
@ -262,7 +265,8 @@ class PDOEngine extends DatabaseDriver implements iDatabaseEngine
{
return new PDOStatementWrapper(
$this->pdoConnection->prepare($statement, $driver_options),
array($this, 'logPDOQuery')
array($this, 'logPDOQuery'),
$this
);
}
@ -289,10 +293,15 @@ class PDOEngine extends DatabaseDriver implements iDatabaseEngine
* Start a transaction
*
* @return bool
* @throws TransactionException
*/
public function transactionStart(): bool
{
return $this->pdoConnection->beginTransaction();
try {
return $this->pdoConnection->beginTransaction();
} catch (PDOException $e) {
throw new TransactionException("Could not start transaction. PDO threw PDOException: '" . $e->getMessage() . "'");
}
}
/**
@ -302,6 +311,7 @@ class PDOEngine extends DatabaseDriver implements iDatabaseEngine
* Automatically rolls back changes if an error occurs with a query
*
* @return bool
* @throws TransactionException
*/
public function transactionEnd(): bool
{
@ -327,19 +337,37 @@ class PDOEngine extends DatabaseDriver implements iDatabaseEngine
* Commit a transaction
*
* @return bool
* @throws TransactionException
*/
public function transactionCommit(): bool
{
return $this->pdoConnection->commit();
try {
return $this->pdoConnection->commit();
} catch (PDOException $e) {
throw new TransactionException("Could not commit transaction. PDO threw PDOException: '" . $e->getMessage() . "'");
}
}
/**
* Roll back a transaction
*
* @return bool
* @throws TransactionException
*/
public function transactionRollback(): bool
{
return $this->pdoConnection->rollBack();
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;
}
}

View File

@ -38,7 +38,33 @@ namespace FuzeWorks\DatabaseEngine;
use FuzeWorks\Exception\DatabaseException;
use PDOStatement;
use PDO;
/**
* Class PDOStatementWrapper
*
* Provides a wrapper for PDOStatement objects so that these can be logged
*
* The following additional methods can be accessed through the __call method:
* @method bool bindColumn(mixed $column, mixed &$param, int $type = null, int $maxlen = null, mixed $driverdata = null)
* @method bool bindParam(mixed $parameter, mixed &$variable, int $data_type = PDO::PARAM_STR, int $length = null, mixed $driver_options = null)
* @method bool bindValue(mixed $parameter, mixed $value, int $data_type = PDO::PARAM_STR)
* @method bool closeCursor()
* @method int columnCount()
* @method void debugDumpParams()
* @method string errorCode()
* @method array errorInfo()
* @method mixed fetch(int $fetch_style = null, int $cursor_orientation = PDO::FETCH_ORI_NEXT, int $cursor_offset = 0)
* @method array fetchAll(int $fetch_style = null, mixed $fetch_argument = null, array $ctor_args = array())
* @method mixed fetchColumn(int $column_number = 0)
* @method mixed fetchObject(string $class_name = "stdClass", array $ctor_args = array())
* @method mixed getAttribute(int $attribute)
* @method array getColumnMeta(int $column)
* @method bool nextRowset()
* @method int rowCount()
* @method bool setAttribute(int $attribute, mixed $value)
* @method bool setFetchMode(int $mode)
*/
class PDOStatementWrapper
{
@ -54,12 +80,23 @@ class PDOStatementWrapper
*/
private $logQueryCallable;
public function __construct(PDOStatement $statement, callable $logQueryCallable)
/**
* @var PDOEngine
*/
private $engine;
public function __construct(PDOStatement $statement, callable $logQueryCallable, PDOEngine $engine)
{
$this->statement = $statement;
$this->logQueryCallable = $logQueryCallable;
$this->engine = $engine;
}
/**
* @param array $input_parameters
* @return bool
* @throws DatabaseException
*/
public function execute(array $input_parameters = [])
{
// Run the query and benchmark the time
@ -72,6 +109,8 @@ class PDOStatementWrapper
// If the query failed, throw an error
if ($result === false)
{
$this->engine->transactionFail();
// And throw an exception
throw new DatabaseException("Could not run query. Database returned an error. Error code: " . $errInfo['code']);
}

View File

@ -44,4 +44,24 @@ interface iDatabaseEngine
public function isSetup(): bool;
public function setUp(array $parameters): bool;
public function tearDown(): bool;
/**
* @return bool
*/
public function transactionStart(): bool;
/**
* @return bool
*/
public function transactionEnd(): bool;
/**
* @return bool
*/
public function transactionCommit(): bool;
/**
* @return bool
*/
public function transactionRollback(): bool;
}

View File

@ -133,8 +133,6 @@ class DatabaseTracyBridge implements IBarPanel
$results = array_slice($results, -10);
}
//dump($results['queries']['mysql:host=localhost;dbname=hello']);
return $this->results = $results;
}

View File

@ -0,0 +1,90 @@
<?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\Event;
use FuzeWorks\Event;
use FuzeWorks\Model\iDatabaseTableModel;
class DatabaseLoadTableModelEvent extends Event
{
/**
* A possible database that can be loaded.
*
* Provide a database in this variable and it will be loaded. It shall be identified as default if
* the parameters variable is empty. If there is a string in parameters this database shall be identified as
* such.
*
* @var iDatabaseTableModel|null
*/
public $tableModel = null;
/**
* The name of the engine to be loaded
*
* @var string
*/
public $tableModelName;
/**
* The name of the table this model manages
*
* @var string
*/
public $tableName;
/**
* Parameters of the database to be loaded
*
* @var array
*/
public $parameters;
/**
* Database group to load
*
* @var bool
*/
public $connectionName;
public function init(string $tableModelName, array $parameters, string $connectionName, string $tableName)
{
$this->tableModelName = $tableModelName;
$this->parameters = $parameters;
$this->connectionName = $connectionName;
$this->tableName = $tableName;
}
}

View File

@ -44,6 +44,4 @@ namespace FuzeWorks\Exception;
*/
class DatabaseException extends Exception
{
}
?>
}

View File

@ -0,0 +1,47 @@
<?php
/**
* FuzeWorks Framework Database 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\Exception;
/**
* Class DatabaseException.
*
* @author Abel Hoogeveen <abel@techfuze.net>
* @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net)
*/
class TransactionException extends Exception
{
}

View File

@ -0,0 +1,273 @@
<?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\Model;
use FuzeWorks\Database;
use FuzeWorks\DatabaseEngine\iDatabaseEngine;
use FuzeWorks\DatabaseEngine\MongoEngine;
use FuzeWorks\Exception\DatabaseException;
use MongoDB\Collection;
class MongoTableModel implements iDatabaseTableModel
{
/**
* Holds the FuzeWorks Database loader
*
* @var Database
*/
private $databases;
/**
* Whether the tableModel has been properly setup
*
* @var bool
*/
protected $setup = false;
/**
* Holds the PDOEngine for this model
*
* @var MongoEngine
*/
protected $dbEngine;
/**
* Holds the collection that is being modified
*
* @var Collection
*/
protected $collection;
/**
* Initializes the model
*
* @param iDatabaseEngine $engine
* @param string $tableName
* @throws DatabaseException
*/
public function setUp(iDatabaseEngine $engine, string $tableName)
{
$this->dbEngine = $engine;
$this->collection = $this->getCollection($tableName);
$this->setup = true;
}
public function isSetup(): bool
{
return $this->setup;
}
/**
* @param string $collectionString
* @return Collection
* @throws DatabaseException
*/
protected function getCollection(string $collectionString)
{
// Determine collection
$coll = explode('.', $collectionString);
if (count($coll) != 2)
throw new DatabaseException("Could not load MongoTableModel. Provided tableName is not a valid collection string.");
return $this->dbEngine->{$coll[0]}->{$coll[1]};
}
/**
* @return string
*/
public function getName(): string
{
return 'mongo';
}
/**
* @return string
*/
public function getEngineName(): string
{
return 'mongo';
}
/**
* @param array $data
* @param array $options
* @param string $table
* @return int
* @throws DatabaseException
*/
public function create(array $data, array $options = [], string $table = 'default'): int
{
// If not data is provided, stop now
if (empty($data))
throw new DatabaseException("Could not create data. No data provided.");
// Select collection
if ($table == 'default')
$collection = $this->collection;
else
$collection = $this->getCollection($table);
// And execute the request
if ($this->arrIsAssoc($data))
$res = $collection->insertOne($data, $options);
else
$res = $collection->insertMany($data, $options);
// And return the count of inserted documents
return $res->getInsertedCount();
}
/**
* @param array $filter
* @param array $options
* @param string $table
* @return array
* @throws DatabaseException
*/
public function read(array $filter = [], array $options = [], string $table = 'default'): array
{
// Select collection
if ($table == 'default')
$collection = $this->collection;
else
$collection = $this->getCollection($table);
// Execute the request
$results = $collection->find($filter, $options);
// Return the result
$return = [];
foreach ($results->toArray() as $result)
$return[] = iterator_to_array($result);
return $return;
}
/**
* @param array $data
* @param array $filter
* @param array $options
* @param string $table
* @return int
* @throws DatabaseException
*/
public function update(array $data, array $filter, array $options = [], string $table = 'default'): int
{
// If not data is provided, stop now
if (empty($data))
throw new DatabaseException("Could not create data. No data provided.");
// Select collection
if ($table == 'default')
$collection = $this->collection;
else
$collection = $this->getCollection($table);
// And execute the request
$data = ['$set' => $data];
$res = $collection->updateMany($filter, $data, $options);
// Return the result
return $res->getModifiedCount();
}
/**
* @param array $filter
* @param array $options
* @param string $table
* @return int
* @throws DatabaseException
*/
public function delete(array $filter, array $options = [], string $table = 'default'): int
{
// Select collection
if ($table == 'default')
$collection = $this->collection;
else
$collection = $this->getCollection($table);
// Execute the request
$res = $collection->deleteMany($filter, $options);
// Return the result
return $res->getDeletedCount();
}
/**
* @return bool
*/
public function transactionStart(): bool
{
// TODO: Implement transactionStart() method.
}
/**
* @return bool
*/
public function transactionEnd(): bool
{
// TODO: Implement transactionEnd() method.
}
/**
* @return bool
*/
public function transactionCommit(): bool
{
// TODO: Implement transactionCommit() method.
}
/**
* @return bool
*/
public function transactionRollback(): bool
{
// TODO: Implement transactionRollback() method.
}
/**
* Determines whether an array is associative or numeric
*
* @param array $arr
* @return bool
*/
private function arrIsAssoc(array $arr): bool
{
if (array() === $arr) return false;
return array_keys($arr) !== range(0, count($arr) - 1);
}
}

View File

@ -36,12 +36,11 @@
namespace FuzeWorks\Model;
use FuzeWorks\Database;
use FuzeWorks\DatabaseEngine\iDatabaseEngine;
use FuzeWorks\DatabaseEngine\PDOEngine;
use FuzeWorks\DatabaseEngine\PDOStatementWrapper;
use FuzeWorks\Exception\DatabaseException;
use FuzeWorks\Exception\EventException;
use FuzeWorks\Factory;
use FuzeWorks\Exception\TransactionException;
use PDO;
use PDOStatement;
@ -53,10 +52,6 @@ use PDOStatement;
* The following additional methods can be accessed through the __call method
* @method PDOStatement query(string $sql)
* @method PDOStatementWrapper prepare(string $statement, array $driver_options = [])
* @method bool transactionStart()
* @method bool transactionEnd()
* @method bool transactionCommit()
* @method bool transactionRollback()
* @method bool exec(string $statement)
* @method mixed getAttribute(int $attribute)
* @method string lastInsertId(string $name = null)
@ -65,13 +60,6 @@ use PDOStatement;
*/
class PDOTableModel implements iDatabaseTableModel
{
/**
* Holds the FuzeWorks Database loader
*
* @var Database
*/
private $databases;
/**
* Holds the PDOEngine for this model
*
@ -79,6 +67,13 @@ class PDOTableModel implements iDatabaseTableModel
*/
protected $dbEngine;
/**
* Whether the tableModel has been properly setup
*
* @var bool
*/
protected $setup = false;
/**
* The table this model manages on the database
*
@ -94,37 +89,60 @@ class PDOTableModel implements iDatabaseTableModel
protected $lastStatement;
/**
* Initializes the model to connect with the database.
* Initializes the model
*
* @param string $connectionName
* @param array $parameters
* @param string|null $tableName
* @throws DatabaseException
* @throws EventException
* @see PDOEngine::setUp()
* @param iDatabaseEngine $engine
* @param string $tableName
*/
public function __construct(string $connectionName = 'default', array $parameters = [], string $tableName = null)
public function setUp(iDatabaseEngine $engine, string $tableName)
{
if (is_null($this->databases))
$this->databases = Factory::getInstance()->databases;
$this->dbEngine = $this->databases->get($connectionName, 'pdo', $parameters);
$this->dbEngine = $engine;
$this->tableName = $tableName;
$this->setup = true;
}
public function create(array $data, array $options = []): bool
public function isSetup(): bool
{
return $this->setup;
}
public function getName(): string
{
return 'pdo';
}
/**
* @return string
*/
public function getEngineName(): string
{
return 'pdo';
}
/**
* @param array $data
* @param array $options
* @param string $table
* @return int
* @throws DatabaseException
*/
public function create(array $data, array $options = [], string $table = 'default'): int
{
// If no data is provided, stop now
if (empty($data))
throw new DatabaseException("Could not create data. No data provided.");
// Select table
if ($table == 'default')
$table = $this->tableName;
// Determine which fields will be inserted
$fieldsArr = $this->createFields($data);
$fields = $fieldsArr['fields'];
$values = $fieldsArr['values'];
// Generate the sql and create a PDOStatement
$sql = "INSERT INTO {$this->tableName} ({$fields}) VALUES ({$values})";
$sql = "INSERT INTO {$table} ({$fields}) VALUES ({$values})";
/** @var PDOStatement $statement */
$this->lastStatement = $this->dbEngine->prepare($sql);
@ -137,7 +155,7 @@ class PDOTableModel implements iDatabaseTableModel
$this->lastStatement->execute($record);
// And return true for success
return true;
return $this->lastStatement->rowCount();
}
/**
@ -145,23 +163,47 @@ class PDOTableModel implements iDatabaseTableModel
*
* @param array $filter
* @param array $options
* @param string $table
* @return array
* @throws DatabaseException
*/
public function read(array $filter = [], array $options = []): array
public function read(array $filter = [], array $options = [], string $table = 'default'): array
{
// Select table
if ($table == 'default')
$table = $this->tableName;
// Determine which fields to select. If none provided, select all
$fields = (isset($options['fields']) && is_array($options['fields']) ? implode(',', $options['fields']) : '*');
// Apply the filter. If none provided, don't condition it
$where = $this->filter($filter);
// If a JOIN is provided, create the statement
if (isset($options['join']))
{
$joinType = (isset($options['join']['joinType']) ? strtoupper($options['join']['joinType']) : 'LEFT');
$targetTable = (isset($options['join']['targetTable']) ? $options['join']['targetTable'] : null);
$targetField = (isset($options['join']['targetField']) ? $options['join']['targetField'] : null);
$sourceField = (isset($options['join']['sourceField']) ? $options['join']['sourceField'] : null);
if (is_null($targetTable) || is_null($targetField) || is_null($sourceField))
throw new DatabaseException("Could not read from '" . $table . "'. Missing fields in join options.");
$join = "{$joinType} JOIN {$targetTable} ON {$table}.{$sourceField} = {$targetTable}.{$targetField}";
}
else
$join = '';
// Generate the sql and create a PDOStatement
$sql = "SELECT " . $fields . " FROM {$this->tableName} " . $where;
$sql = "SELECT " . $fields . " FROM {$table} {$join} " . $where;
/** @var PDOStatement $statement */
$this->lastStatement = $this->dbEngine->prepare($sql);
// Return prepared statement, if requested to do so
if (isset($options['returnPreparedStatement']) && $options['returnPreparedStatement'] == true)
return [];
// And execute the query
$this->lastStatement->execute($filter);
@ -173,12 +215,16 @@ class PDOTableModel implements iDatabaseTableModel
return $this->lastStatement->fetchAll($fetchMode);
}
public function update(array $data, array $filter, array $options = []): bool
public function update(array $data, array $filter, array $options = [], string $table = 'default'): int
{
// If no data is provided, stop now
if (empty($data))
throw new DatabaseException("Could not update data. No data provided.");
// Select table
if ($table == 'default')
$table = $this->tableName;
// Apply the filter
$where = $this->filter($filter);
@ -189,7 +235,7 @@ class PDOTableModel implements iDatabaseTableModel
$fields = implode(', ', $fields);
// Generate the sql and create a PDOStatement
$sql = "UPDATE {$this->tableName} SET {$fields} {$where}";
$sql = "UPDATE {$table} SET {$fields} {$where}";
/** @var PDOStatement $statement */
$this->lastStatement = $this->dbEngine->prepare($sql);
@ -201,16 +247,20 @@ class PDOTableModel implements iDatabaseTableModel
$this->lastStatement->execute($parameters);
// And return true for success
return true;
return $this->lastStatement->rowCount();
}
public function delete(array $filter, array $options = []): bool
public function delete(array $filter, array $options = [], string $table = 'default'): int
{
// Select table
if ($table == 'default')
$table = $this->tableName;
// Apply the filter
$where = $this->filter($filter);
// Generate the sql and create a PDOStatement
$sql = "DELETE FROM {$this->tableName} " . $where;
$sql = "DELETE FROM {$table} " . $where;
/** @var PDOStatement $statement */
$this->lastStatement = $this->dbEngine->prepare($sql);
@ -219,7 +269,7 @@ class PDOTableModel implements iDatabaseTableModel
$this->lastStatement->execute($filter);
// And return true for success
return true;
return $this->lastStatement->rowCount();
}
public function getLastStatement(): PDOStatementWrapper
@ -227,6 +277,42 @@ class PDOTableModel implements iDatabaseTableModel
return $this->lastStatement;
}
/**
* @return bool
* @throws TransactionException
*/
public function transactionStart(): bool
{
return $this->dbEngine->transactionStart();
}
/**
* @return bool
* @throws TransactionException
*/
public function transactionEnd(): bool
{
return $this->dbEngine->transactionEnd();
}
/**
* @return bool
* @throws TransactionException
*/
public function transactionCommit(): bool
{
return $this->dbEngine->transactionCommit();
}
/**
* @return bool
* @throws TransactionException
*/
public function transactionRollback(): bool
{
return $this->dbEngine->transactionRollback();
}
/**
* Call methods on the PDO Engine, which calls methods on the PDO Connection
*

View File

@ -37,10 +37,79 @@
namespace FuzeWorks\Model;
use FuzeWorks\DatabaseEngine\iDatabaseEngine;
use FuzeWorks\Exception\DatabaseException;
interface iDatabaseTableModel
{
public function create(array $data, array $options = []): bool;
public function read(array $filter = [], array $options = []): array;
public function update(array $data, array $filter, array $options = []): bool;
public function delete(array $filter, array $options = []): bool;
/**
* @return string
*/
public function getName(): string;
/**
* @return string
*/
public function getEngineName(): string;
public function setUp(iDatabaseEngine $engine, string $tableName);
public function isSetup(): bool;
/**
* @param array $data
* @param array $options
* @param string $table
* @return int
* @throws DatabaseException
*/
public function create(array $data, array $options = [], string $table = 'default'): int;
/**
* @param array $filter
* @param array $options
* @param string $table
* @return array
* @throws DatabaseException
*/
public function read(array $filter = [], array $options = [], string $table = 'default'): array;
/**
* @param array $data
* @param array $filter
* @param array $options
* @param string $table
* @return int
* @throws DatabaseException
*/
public function update(array $data, array $filter, array $options = [], string $table = 'default'): int;
/**
* @param array $filter
* @param array $options
* @param string $table
* @return int
* @throws DatabaseException
*/
public function delete(array $filter, array $options = [], string $table = 'default'): int;
/**
* @return bool
*/
public function transactionStart(): bool;
/**
* @return bool
*/
public function transactionEnd(): bool;
/**
* @return bool
*/
public function transactionCommit(): bool;
/**
* @return bool
*/
public function transactionRollback(): bool;
}

View File

@ -34,6 +34,9 @@
* @version Version 1.1.4
*/
use FuzeWorks\DatabaseComponent;
use FuzeWorks\TracyComponent;
require_once(dirname(__DIR__) . '/vendor/autoload.php');
$configurator = new FuzeWorks\Configurator();
@ -47,8 +50,8 @@ $configurator->setTimeZone('Europe/Amsterdam');
$configurator->enableDebugMode(true)->setDebugAddress('ALL');
// Implement the Layout Component
$configurator->addComponent(new \FuzeWorks\DatabaseComponent());
$configurator->addComponent(new \FuzeWorks\TracyComponent());
$configurator->addComponent(new DatabaseComponent());
$configurator->addComponent(new TracyComponent());
// Create container
$container = $configurator->createContainer();