From b8ad345365290b52054fb681a27a9aad880f444a Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 22 Jul 2019 11:43:01 +0200 Subject: [PATCH] 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