From b8ad345365290b52054fb681a27a9aad880f444a Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 22 Jul 2019 11:43:01 +0200 Subject: [PATCH 1/4] Implemented MongoDB engine into `FuzeWorks\Database` and other improvements - A standard MongoDB engine has been implemented - Implemented transactions into the iDatabaseEngine and PDOEngine. - Added a TransactionException when transactions fail --- src/FuzeWorks/Database.php | 9 +- .../DatabaseEngine/MongoCommandSubscriber.php | 206 ++++++++++++++ src/FuzeWorks/DatabaseEngine/MongoEngine.php | 234 ++++++++++++++++ src/FuzeWorks/DatabaseEngine/PDOEngine.php | 40 ++- .../DatabaseEngine/PDOStatementWrapper.php | 10 +- .../DatabaseEngine/iDatabaseEngine.php | 20 ++ src/FuzeWorks/Exception/DatabaseException.php | 4 +- .../Exception/TransactionException.php | 47 ++++ src/FuzeWorks/Model/MongoTableModel.php | 260 ++++++++++++++++++ src/FuzeWorks/Model/PDOTableModel.php | 114 ++++++-- src/FuzeWorks/Model/iDatabaseTableModel.php | 67 ++++- 11 files changed, 978 insertions(+), 33 deletions(-) create mode 100644 src/FuzeWorks/DatabaseEngine/MongoCommandSubscriber.php create mode 100644 src/FuzeWorks/DatabaseEngine/MongoEngine.php create mode 100644 src/FuzeWorks/Exception/TransactionException.php create mode 100644 src/FuzeWorks/Model/MongoTableModel.php diff --git a/src/FuzeWorks/Database.php b/src/FuzeWorks/Database.php index ca1a478..1cfae0b 100644 --- a/src/FuzeWorks/Database.php +++ b/src/FuzeWorks/Database.php @@ -36,6 +36,7 @@ namespace FuzeWorks; use FuzeWorks\DatabaseEngine\iDatabaseEngine; +use FuzeWorks\DatabaseEngine\MongoEngine; use FuzeWorks\DatabaseEngine\PDOEngine; use FuzeWorks\Event\DatabaseLoadDriverEvent; use FuzeWorks\Exception\DatabaseException; @@ -111,13 +112,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."); @@ -229,6 +233,7 @@ class Database // Load the engines provided by the DatabaseComponent $this->registerEngine(new PDOEngine()); + $this->registerEngine(new MongoEngine()); // And save results $this->enginesLoaded = true; diff --git a/src/FuzeWorks/DatabaseEngine/MongoCommandSubscriber.php b/src/FuzeWorks/DatabaseEngine/MongoCommandSubscriber.php new file mode 100644 index 0000000..92f578d --- /dev/null +++ b/src/FuzeWorks/DatabaseEngine/MongoCommandSubscriber.php @@ -0,0 +1,206 @@ +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; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/DatabaseEngine/MongoEngine.php b/src/FuzeWorks/DatabaseEngine/MongoEngine.php new file mode 100644 index 0000000..ea088e1 --- /dev/null +++ b/src/FuzeWorks/DatabaseEngine/MongoEngine.php @@ -0,0 +1,234 @@ +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. + } +} \ No newline at end of file diff --git a/src/FuzeWorks/DatabaseEngine/PDOEngine.php b/src/FuzeWorks/DatabaseEngine/PDOEngine.php index 2e75a03..c902af5 100644 --- a/src/FuzeWorks/DatabaseEngine/PDOEngine.php +++ b/src/FuzeWorks/DatabaseEngine/PDOEngine.php @@ -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; } } \ No newline at end of file diff --git a/src/FuzeWorks/DatabaseEngine/PDOStatementWrapper.php b/src/FuzeWorks/DatabaseEngine/PDOStatementWrapper.php index 7141340..e548382 100644 --- a/src/FuzeWorks/DatabaseEngine/PDOStatementWrapper.php +++ b/src/FuzeWorks/DatabaseEngine/PDOStatementWrapper.php @@ -54,10 +54,16 @@ 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; } public function execute(array $input_parameters = []) @@ -72,6 +78,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']); } diff --git a/src/FuzeWorks/DatabaseEngine/iDatabaseEngine.php b/src/FuzeWorks/DatabaseEngine/iDatabaseEngine.php index 438db06..ffe8181 100644 --- a/src/FuzeWorks/DatabaseEngine/iDatabaseEngine.php +++ b/src/FuzeWorks/DatabaseEngine/iDatabaseEngine.php @@ -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; } \ No newline at end of file diff --git a/src/FuzeWorks/Exception/DatabaseException.php b/src/FuzeWorks/Exception/DatabaseException.php index 9417f35..9653d66 100644 --- a/src/FuzeWorks/Exception/DatabaseException.php +++ b/src/FuzeWorks/Exception/DatabaseException.php @@ -44,6 +44,4 @@ namespace FuzeWorks\Exception; */ class DatabaseException extends Exception { -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/src/FuzeWorks/Exception/TransactionException.php b/src/FuzeWorks/Exception/TransactionException.php new file mode 100644 index 0000000..5616cb3 --- /dev/null +++ b/src/FuzeWorks/Exception/TransactionException.php @@ -0,0 +1,47 @@ + + * @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net) + */ +class TransactionException extends Exception +{ +} \ No newline at end of file diff --git a/src/FuzeWorks/Model/MongoTableModel.php b/src/FuzeWorks/Model/MongoTableModel.php new file mode 100644 index 0000000..a2008f6 --- /dev/null +++ b/src/FuzeWorks/Model/MongoTableModel.php @@ -0,0 +1,260 @@ +databases)) + $this->databases = Factory::getInstance()->databases; + + // Load databaseEngine + $this->dbEngine = $this->databases->get($connectionName, 'mongo', $parameters); + + // Determine the collection + $this->collection = $this->getCollection($tableName); + } + + /** + * @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'; + } + + /** + * @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); + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Model/PDOTableModel.php b/src/FuzeWorks/Model/PDOTableModel.php index 62614be..35e29d8 100644 --- a/src/FuzeWorks/Model/PDOTableModel.php +++ b/src/FuzeWorks/Model/PDOTableModel.php @@ -41,6 +41,7 @@ use FuzeWorks\DatabaseEngine\PDOEngine; use FuzeWorks\DatabaseEngine\PDOStatementWrapper; use FuzeWorks\Exception\DatabaseException; use FuzeWorks\Exception\EventException; +use FuzeWorks\Exception\TransactionException; use FuzeWorks\Factory; use PDO; use PDOStatement; @@ -53,10 +54,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) @@ -100,10 +97,9 @@ class PDOTableModel implements iDatabaseTableModel * @param array $parameters * @param string|null $tableName * @throws DatabaseException - * @throws EventException * @see PDOEngine::setUp() */ - public function __construct(string $connectionName = 'default', array $parameters = [], string $tableName = null) + public function __construct(string $connectionName = 'default', array $parameters = [], string $tableName = 'default') { if (is_null($this->databases)) $this->databases = Factory::getInstance()->databases; @@ -112,19 +108,35 @@ class PDOTableModel implements iDatabaseTableModel $this->tableName = $tableName; } - public function create(array $data, array $options = []): bool + public function getName(): 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 +149,7 @@ class PDOTableModel implements iDatabaseTableModel $this->lastStatement->execute($record); // And return true for success - return true; + return $this->lastStatement->rowCount(); } /** @@ -145,23 +157,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 +209,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 +229,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 +241,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 +263,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 +271,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 * diff --git a/src/FuzeWorks/Model/iDatabaseTableModel.php b/src/FuzeWorks/Model/iDatabaseTableModel.php index fd28605..1edb160 100644 --- a/src/FuzeWorks/Model/iDatabaseTableModel.php +++ b/src/FuzeWorks/Model/iDatabaseTableModel.php @@ -37,10 +37,69 @@ namespace FuzeWorks\Model; +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; + + /** + * @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; } \ No newline at end of file From ae8da38cd96ab596be0aac77fec92e8d78f71ec7 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Fri, 16 Aug 2019 17:17:17 +0200 Subject: [PATCH 2/4] Implemented comments on PDOStatementWrapper. - Now has hints on what methods it extends --- .../DatabaseEngine/PDOStatementWrapper.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/FuzeWorks/DatabaseEngine/PDOStatementWrapper.php b/src/FuzeWorks/DatabaseEngine/PDOStatementWrapper.php index e548382..fe1b76a 100644 --- a/src/FuzeWorks/DatabaseEngine/PDOStatementWrapper.php +++ b/src/FuzeWorks/DatabaseEngine/PDOStatementWrapper.php @@ -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 { @@ -66,6 +92,11 @@ class PDOStatementWrapper $this->engine = $engine; } + /** + * @param array $input_parameters + * @return bool + * @throws DatabaseException + */ public function execute(array $input_parameters = []) { // Run the query and benchmark the time From 18f2a6bb40c977f2115edcd3fe98d042d1403107 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Wed, 21 Aug 2019 16:42:47 +0200 Subject: [PATCH 3/4] Moved retrieval of TableModels into the \FuzeWorks\Database class. - BREAKING: tableModels can no longer be fetched using the iDatabaseTableModel::__construct() method. From now on users must implement the Database::getTableModel() method using the Factory. - TableModels now have a few more required methods from the iDatabaseTableModel interface. This change shouldn't impact much --- src/FuzeWorks/Database.php | 125 +++++++++++++++++- src/FuzeWorks/DatabaseTracyBridge.php | 2 - .../Event/DatabaseLoadTableModelEvent.php | 90 +++++++++++++ src/FuzeWorks/Model/MongoTableModel.php | 39 ++++-- src/FuzeWorks/Model/PDOTableModel.php | 48 ++++--- src/FuzeWorks/Model/iDatabaseTableModel.php | 10 ++ test/bootstrap.php | 7 +- 7 files changed, 277 insertions(+), 44 deletions(-) create mode 100644 src/FuzeWorks/Event/DatabaseLoadTableModelEvent.php diff --git a/src/FuzeWorks/Database.php b/src/FuzeWorks/Database.php index 1cfae0b..e89bb04 100644 --- a/src/FuzeWorks/Database.php +++ b/src/FuzeWorks/Database.php @@ -39,8 +39,12 @@ 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 @@ -68,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 * @@ -76,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 @@ -142,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); } @@ -153,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]); } @@ -165,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. * @@ -172,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])) @@ -188,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 * @@ -212,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) @@ -235,6 +344,10 @@ class Database $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; return true; diff --git a/src/FuzeWorks/DatabaseTracyBridge.php b/src/FuzeWorks/DatabaseTracyBridge.php index 1a44674..6496a98 100644 --- a/src/FuzeWorks/DatabaseTracyBridge.php +++ b/src/FuzeWorks/DatabaseTracyBridge.php @@ -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; } diff --git a/src/FuzeWorks/Event/DatabaseLoadTableModelEvent.php b/src/FuzeWorks/Event/DatabaseLoadTableModelEvent.php new file mode 100644 index 0000000..0f32831 --- /dev/null +++ b/src/FuzeWorks/Event/DatabaseLoadTableModelEvent.php @@ -0,0 +1,90 @@ +tableModelName = $tableModelName; + $this->parameters = $parameters; + $this->connectionName = $connectionName; + $this->tableName = $tableName; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Model/MongoTableModel.php b/src/FuzeWorks/Model/MongoTableModel.php index a2008f6..c0d2a6a 100644 --- a/src/FuzeWorks/Model/MongoTableModel.php +++ b/src/FuzeWorks/Model/MongoTableModel.php @@ -36,9 +36,9 @@ namespace FuzeWorks\Model; use FuzeWorks\Database; +use FuzeWorks\DatabaseEngine\iDatabaseEngine; use FuzeWorks\DatabaseEngine\MongoEngine; use FuzeWorks\Exception\DatabaseException; -use FuzeWorks\Factory; use MongoDB\Collection; class MongoTableModel implements iDatabaseTableModel @@ -51,6 +51,13 @@ class MongoTableModel implements iDatabaseTableModel */ private $databases; + /** + * Whether the tableModel has been properly setup + * + * @var bool + */ + protected $setup = false; + /** * Holds the PDOEngine for this model * @@ -66,24 +73,22 @@ class MongoTableModel implements iDatabaseTableModel protected $collection; /** - * Initializes the model to connect with the database. + * Initializes the model * - * @param string $connectionName - * @param array $parameters + * @param iDatabaseEngine $engine * @param string $tableName * @throws DatabaseException - * @see MongoEngine::setUp() */ - public function __construct(string $connectionName = 'default', array $parameters = [], string $tableName = 'default.default') + public function setUp(iDatabaseEngine $engine, string $tableName) { - if (is_null($this->databases)) - $this->databases = Factory::getInstance()->databases; - - // Load databaseEngine - $this->dbEngine = $this->databases->get($connectionName, 'mongo', $parameters); - - // Determine the collection + $this->dbEngine = $engine; $this->collection = $this->getCollection($tableName); + $this->setup = true; + } + + public function isSetup(): bool + { + return $this->setup; } /** @@ -109,6 +114,14 @@ class MongoTableModel implements iDatabaseTableModel return 'mongo'; } + /** + * @return string + */ + public function getEngineName(): string + { + return 'mongo'; + } + /** * @param array $data * @param array $options diff --git a/src/FuzeWorks/Model/PDOTableModel.php b/src/FuzeWorks/Model/PDOTableModel.php index 35e29d8..14f60ac 100644 --- a/src/FuzeWorks/Model/PDOTableModel.php +++ b/src/FuzeWorks/Model/PDOTableModel.php @@ -36,13 +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\Exception\TransactionException; -use FuzeWorks\Factory; use PDO; use PDOStatement; @@ -62,13 +60,6 @@ use PDOStatement; */ class PDOTableModel implements iDatabaseTableModel { - /** - * Holds the FuzeWorks Database loader - * - * @var Database - */ - private $databases; - /** * Holds the PDOEngine for this model * @@ -76,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 * @@ -91,21 +89,21 @@ 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 - * @see PDOEngine::setUp() + * @param iDatabaseEngine $engine + * @param string $tableName */ - public function __construct(string $connectionName = 'default', array $parameters = [], string $tableName = 'default') + 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 isSetup(): bool + { + return $this->setup; } public function getName(): string @@ -113,6 +111,14 @@ class PDOTableModel implements iDatabaseTableModel return 'pdo'; } + /** + * @return string + */ + public function getEngineName(): string + { + return 'pdo'; + } + /** * @param array $data * @param array $options diff --git a/src/FuzeWorks/Model/iDatabaseTableModel.php b/src/FuzeWorks/Model/iDatabaseTableModel.php index 1edb160..f92df40 100644 --- a/src/FuzeWorks/Model/iDatabaseTableModel.php +++ b/src/FuzeWorks/Model/iDatabaseTableModel.php @@ -37,6 +37,7 @@ namespace FuzeWorks\Model; +use FuzeWorks\DatabaseEngine\iDatabaseEngine; use FuzeWorks\Exception\DatabaseException; interface iDatabaseTableModel @@ -46,6 +47,15 @@ interface iDatabaseTableModel */ 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 diff --git a/test/bootstrap.php b/test/bootstrap.php index 1451ba7..fc7a619 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -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(); From 2be0b7b1586cf43f287e6990cf9054eb048e0f6f Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Wed, 21 Aug 2019 19:35:31 +0200 Subject: [PATCH 4/4] Release 1.2.0-RC4 --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index ba07c74..3969319 100644 --- a/composer.json +++ b/composer.json @@ -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": {