diff --git a/bin/db-migrate b/bin/db-migrate index a854f7b..14b80c9 100755 --- a/bin/db-migrate +++ b/bin/db-migrate @@ -7,43 +7,32 @@ use Gt\Database\Migration\Migrator; * Database migration iterates over a set of incremental schema changes and * stores the currently-migrated schema version within the database itself. */ -$autoloadPath = ""; -$currentDir = __DIR__; - -// The bin directory may be in multiple places, depending on how this library -// was installed. Iterate up the tree until either the autoloader or the root -// directory is found: -while((empty($autoloadPath)) && $currentDir !== "/") { - $currentDir = realpath($currentDir . "/.."); - if(is_file("$currentDir/autoload.php")) { - $autoloadPath = "$currentDir/autoload.php"; - } -} -if(empty($autoloadPath)) { - $autoloadPath = realpath(__DIR__ . "/../vendor/autoload.php"); -} +// The script must be run from the context of a project's root directory. +$repoBasePath = getcwd(); +$autoloadPath = implode(DIRECTORY_SEPARATOR, [ + $repoBasePath, + "vendor", + "autoload.php", +]); require($autoloadPath); -// Repository will always be the parent directory above autoload.php. -$repoBasePath = dirname(dirname($autoloadPath)); - $forced = false; if(!empty($argv[1]) && ($argv[1] === "--force" || $argv[1] === "-f")) { $forced = true; } -$config = new Config("$repoBasePath/config.ini", [ - "database" => [ - "query_path" => "src/query", - "migration_path" => "_migration", - "migration_table" => "_migration", - ] +// Load the default config supplied by WebEngine, if available: +$webEngineConfig = new Config("$repoBasePath/vendor/phpgt/webengine/config.default.ini"); +$config = new Config( + "$repoBasePath/config.ini", + [ + "database" => $webEngineConfig["database"] ?? [], ] ); $settings = new Settings( - implode("/", [ + implode(DIRECTORY_SEPARATOR, [ $repoBasePath, $config["database"]["query_path"] ]), @@ -55,7 +44,7 @@ $settings = new Settings( $config["database"]["password"] ); -$migrationPath = implode("/", [ +$migrationPath = implode(DIRECTORY_SEPARATOR, [ $repoBasePath, $config["database"]["query_path"], $config["database"]["migration_path"], @@ -63,7 +52,8 @@ $migrationPath = implode("/", [ $migrationTable = $config["database"]["migration_table"]; $migrator = new Migrator($settings, $migrationPath, $migrationTable, $forced); +$migrator->createMigrationTable(); $migrationCount = $migrator->getMigrationCount(); $migrationFileList = $migrator->getMigrationFileList(); -$migrator->checkIntegrity($migrationCount, $migrationFileList); +$migrator->checkIntegrity($migrationFileList, $migrationCount); $migrator->performMigration($migrationFileList, $migrationCount); \ No newline at end of file diff --git a/src/Client.php b/src/Database.php similarity index 97% rename from src/Client.php rename to src/Database.php index 2233867..fb721a3 100644 --- a/src/Client.php +++ b/src/Database.php @@ -17,7 +17,7 @@ * connections. If only one database connection is required, a name is not * required as the default name will be used. */ -class Client { +class Database { /** @var QueryCollectionFactory[] */ protected $queryCollectionFactoryArray; /** @var Driver[] */ @@ -64,11 +64,6 @@ public function update(string $queryName, ...$bindings):int { } public function query(string $queryName, ...$bindings):ResultSet { - while(isset($bindings[0]) - && is_array($bindings[0])) { - $bindings = $bindings[0]; - } - $queryCollectionName = substr( $queryName, 0, diff --git a/src/Migration/Migrator.php b/src/Migration/Migrator.php index 15ae638..9d131bf 100644 --- a/src/Migration/Migrator.php +++ b/src/Migration/Migrator.php @@ -2,9 +2,10 @@ namespace Gt\Database\Migration; use DirectoryIterator; -use Gt\Database\Client; +use Gt\Database\Database; use Gt\Database\Connection\Settings; use Gt\Database\DatabaseException; +use PDOException; use SplFileInfo; class Migrator { @@ -33,7 +34,7 @@ public function __construct( $settings = $settings->withoutSchema(); // @codeCoverageIgnore } - $this->dbClient = new Client($settings); + $this->dbClient = new Database($settings); if($forced) { $this->deleteAndRecreateSchema(); @@ -72,7 +73,7 @@ public function checkMigrationTableExists():bool { public function createMigrationTable():void { $this->dbClient->executeSql(implode("\n", [ - "create table `{$this->tableName}` (", + "create table if not exists `{$this->tableName}` (", "`" . self::COLUMN_QUERY_NUMBER . "` int primary key,", "`" . self::COLUMN_QUERY_HASH . "` varchar(32) not null,", "`" . self::COLUMN_MIGRATED_AT . "` datetime not null )", @@ -88,7 +89,7 @@ public function getMigrationCount():int { ); $row = $result->fetch(); } - catch(DatabaseException $exception) { + catch(PDOException $exception) { return 0; } diff --git a/src/Query/QueryCollection.php b/src/Query/QueryCollection.php index 001f849..bc591ed 100644 --- a/src/Query/QueryCollection.php +++ b/src/Query/QueryCollection.php @@ -40,49 +40,60 @@ public function __call($name, $args) { public function query( string $name, - iterable $placeholderMap = [] + ...$placeholderMap ):ResultSet { $query = $this->queryFactory->create($name); - return $query->execute($placeholderMap); } public function insert( string $name, - iterable $placeholderMap = [] + ...$placeholderMap ):int { return (int)$this->query( $name, - $placeholderMap + ...$placeholderMap )->lastInsertId(); } public function fetch( string $name, - iterable $placeholderMap = [] + ...$placeholderMap ):?Row { - return $this->query($name, $placeholderMap)->current(); + return $this->query( + $name, + ...$placeholderMap + )->current(); } public function fetchAll( string $name, - iterable $placeholderMap = [] + ...$placeholderMap ):ResultSet { - return $this->query($name, $placeholderMap); + return $this->query( + $name, + ...$placeholderMap + ); } public function update( string $name, - iterable $placeholderMap = [] + ...$placeholderMap ):int { - return $this->query($name, $placeholderMap)->affectedRows(); + return $this->query( + $name, + ...$placeholderMap + )->affectedRows(); } public function delete( string $name, - iterable $placeholderMap = [] + ...$placeholderMap ):int { - return $this->query($name, $placeholderMap)->affectedRows(); + return $this->query( + $name, + ...$placeholderMap + )->affectedRows(); } public function getDirectoryPath():string { diff --git a/src/Query/SqlQuery.php b/src/Query/SqlQuery.php index 94d0d0c..747d03c 100644 --- a/src/Query/SqlQuery.php +++ b/src/Query/SqlQuery.php @@ -23,6 +23,8 @@ public function getSql(array $bindings = []):string { } public function execute(array $bindings = []):ResultSet { + $bindings = $this->flattenBindings($bindings); + $pdo = $this->preparePdo(); $sql = $this->getSql($bindings); $statement = $this->prepareStatement($pdo, $sql); @@ -126,4 +128,32 @@ protected function bindingsEmptyOrNonAssociative(array $bindings):bool { $bindings === [] || array_keys($bindings) === range(0, count($bindings) - 1); } + + /** + * $bindings can either be : + * 1) An array of individual values for binding to the question mark placeholder, + * passed in as variable arguments. + * 2) An array containing one single subarray containing key-value-pairs for binding to + * named placeholders. + * + * Due to the use of variable arguments on the Database and QueryCollection classes, + * key-value-pair bindings may be double or triple nested. + */ + protected function flattenBindings(array $bindings):array { + if(!isset($bindings[0])) { + return $bindings; + } + + $flatArray = []; + foreach($bindings as $i => $b) { + while(isset($b[0]) + && is_array($b[0])) { + $b = $b[0]; + } + + $flatArray = array_merge($flatArray, $b); + } + + return $flatArray; + } } diff --git a/test/unit/ClientTest.php b/test/unit/DatabaseTest.php similarity index 85% rename from test/unit/ClientTest.php rename to test/unit/DatabaseTest.php index b075aeb..f2daa26 100644 --- a/test/unit/ClientTest.php +++ b/test/unit/DatabaseTest.php @@ -5,10 +5,10 @@ use Gt\Database\Query\QueryCollection; use PHPUnit\Framework\TestCase; -class ClientTest extends TestCase { +class DatabaseTest extends TestCase { public function testInterface() { - $db = new Client(); - static::assertInstanceOf(Client::class, $db); + $db = new Database(); + static::assertInstanceOf(Database::class, $db); } /** @@ -21,7 +21,7 @@ public function testQueryCollectionPathExists(string $name, string $path) { Settings::DRIVER_SQLITE, Settings::SCHEMA_IN_MEMORY ); - $db = new Client($settings); + $db = new Database($settings); $queryCollection = $db->queryCollection($name); static::assertInstanceOf(QueryCollection::class, $queryCollection); @@ -39,7 +39,7 @@ public function testQueryCollectionPathNotExists(string $name, string $path) { Settings::DRIVER_SQLITE, Settings::SCHEMA_IN_MEMORY ); - $db = new Client($settings); + $db = new Database($settings); $db->queryCollection($name); } } \ No newline at end of file diff --git a/test/unit/IntegrationTest.php b/test/unit/IntegrationTest.php index 19fba2c..05b7bcd 100644 --- a/test/unit/IntegrationTest.php +++ b/test/unit/IntegrationTest.php @@ -14,24 +14,24 @@ class IntegrationTest extends TestCase { private $settings; /** @var string */ private $queryBase; - /** @var Client */ + /** @var Database */ private $db; public function setUp() { $this->queryBase = Helper::getTmpDir() . "/query"; - $this->db = new Client($this->settingsSingleton()); + $this->db = new Database($this->settingsSingleton()); $driver = $this->db->getDriver(); $connection = $driver->getConnection(); - $output = $connection->exec("CREATE TABLE test_table ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(32), timestamp DATETIME DEFAULT current_timestamp); CREATE UNIQUE INDEX test_table_name_uindex ON test_table (name);"); + $output = $connection->exec("CREATE TABLE test_table ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(32), number integer, timestamp DATETIME DEFAULT current_timestamp); CREATE UNIQUE INDEX test_table_name_uindex ON test_table (name);"); if($output === false) { $error = $connection->errorInfo(); throw new Exception($error[2]); } - $insertStatement = $connection->prepare("INSERT INTO test_table (name) VALUES ('one'), ('two'), ('three')"); + $insertStatement = $connection->prepare("INSERT INTO test_table (name, number) VALUES ('one', 1), ('two', 2), ('three', 3)"); $success = $insertStatement->execute(); if($success === false) { $error = $connection->errorInfo(); @@ -94,7 +94,7 @@ public function testQuestionMarkParameter() { mkdir($queryCollectionPath, 0775, true); file_put_contents( $getByIdQueryPath, - "SELECT id, name FROM test_table WHERE id = ?" + "SELECT id, name, number FROM test_table WHERE id = ?" ); $result2 = $this->db->fetch("exampleCollection/getById", 2); @@ -107,6 +107,36 @@ public function testQuestionMarkParameter() { static::assertCount(3, $rqr); } + public function testMultipleParameterUsage() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getByNameNumber.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT id, name, number FROM test_table WHERE name = :name and number = :number" + ); + + $result1 = $this->db->fetch("exampleCollection/getByNameNumber", [ + "name" => "one", + "number" => 1, + ]); + $result2 = $this->db->fetch("exampleCollection/getByNameNumber", [ + "name" => "two", + "number" => 2, + ]); + $resultNull = $this->db->fetch("exampleCollection/getByNameNumber", [ + "name" => "three", + "number" => 55, + ]); + + $rqr = $this->db->executeSql("SELECT id, name FROM test_table"); + + static::assertEquals(1, $result1->id); + static::assertEquals(2, $result2->id); + static::assertNull($resultNull); + } + private function settingsSingleton():Settings { if(is_null($this->settings)) { $this->settings = new Settings( diff --git a/test/unit/Migration/MigratorTest.php b/test/unit/Migration/MigratorTest.php index 1e2c357..6a25f55 100644 --- a/test/unit/Migration/MigratorTest.php +++ b/test/unit/Migration/MigratorTest.php @@ -2,7 +2,7 @@ namespace Gt\Database\Test\Migration; use Exception; -use Gt\Database\Client; +use Gt\Database\Database; use Gt\Database\Connection\Settings; use Gt\Database\Migration\MigrationDirectoryNotFoundException; use Gt\Database\Migration\MigrationFileNameFormatException; @@ -34,6 +34,7 @@ public function testMigrationZeroAtStartWithoutTable() { $path = $this->getMigrationDirectory(); $settings = $this->createSettings($path); $migrator = new Migrator($settings, $path); + $migrator->createMigrationTable(); self::assertEquals(0, $migrator->getMigrationCount()); } @@ -197,6 +198,7 @@ public function testMigrationCountZeroAtStart() { $path = $this->getMigrationDirectory(); $settings = $this->createSettings($path); $migrator = new Migrator($settings, $path); + $migrator->createMigrationTable(); self::assertEquals(0, $migrator->getMigrationCount()); } @@ -304,7 +306,7 @@ public function testPerformMigrationGood(array $fileList):void { } catch(Exception $exception) {} - $db = new Client($settings); + $db = new Database($settings); $result = $db->executeSql("PRAGMA table_info(test);"); // There should be one more column than the number of files, due to the fact that the first // migration creates the table with two columns. @@ -375,7 +377,7 @@ protected function hashMigrationToDb( } $settings = $this->createSettings($path); - $db = new Client($settings); + $db = new Database($settings); $db->executeSql(implode("\n", [ "create table `_migration` (", "`" . Migrator::COLUMN_QUERY_NUMBER . "` int primary key,", diff --git a/test/unit/Query/QueryCollectionCRUDsTest.php b/test/unit/Query/QueryCollectionCRUDsTest.php index 1cb1bcd..d6570f7 100644 --- a/test/unit/Query/QueryCollectionCRUDsTest.php +++ b/test/unit/Query/QueryCollectionCRUDsTest.php @@ -41,12 +41,16 @@ public function testCreate() { $this->mockQuery ->expects(static::once()) ->method("execute") - ->with($placeholderVars) + ->with([$placeholderVars]) ->willReturn($mockResultSet); static::assertEquals( $lastInsertID, - $this->queryCollection->insert("something", $placeholderVars)); + $this->queryCollection->insert( + "something", + $placeholderVars + ) + ); } public function testCreateNoParams() { @@ -78,7 +82,7 @@ public function testRetrieve() { $this->mockQuery ->expects(static::once()) ->method("execute") - ->with($placeholderVars) + ->with([$placeholderVars]) ->willReturn($mockResultSet); $actual = $this->queryCollection->fetch("something", $placeholderVars); @@ -133,7 +137,7 @@ public function testRetrieveAll() { $this->mockQuery ->expects(static::once()) ->method("execute") - ->with($placeholderVars) + ->with([$placeholderVars]) ->willReturn($mockResultSet); $actual = $this->queryCollection->fetchAll("something", $placeholderVars); @@ -188,7 +192,7 @@ public function testUpdate() { $this->mockQuery ->expects(static::once()) ->method("execute") - ->with($placeholderVars) + ->with([$placeholderVars]) ->willReturn($mockResultSet); static::assertEquals($recordsUpdatedCount, $this->queryCollection->update("something", $placeholderVars)); @@ -223,7 +227,7 @@ public function testDelete() { $this->mockQuery ->expects(static::once()) ->method("execute") - ->with($placeholderVars) + ->with([$placeholderVars]) ->willReturn($mockResultSet); static::assertEquals( diff --git a/test/unit/Query/QueryCollectionTest.php b/test/unit/Query/QueryCollectionTest.php index 0a7201e..c753ef9 100644 --- a/test/unit/Query/QueryCollectionTest.php +++ b/test/unit/Query/QueryCollectionTest.php @@ -18,9 +18,12 @@ public function testQueryCollectionQuery() { $this->mockQuery ->expects(static::once()) ->method("execute") - ->with($placeholderVars); + ->with([$placeholderVars]); - $resultSet = $this->queryCollection->query("something", $placeholderVars); + $resultSet = $this->queryCollection->query( + "something", + $placeholderVars + ); static::assertInstanceOf( ResultSet::class, @@ -44,7 +47,7 @@ public function testQueryShorthand() { $this->mockQuery ->expects(static::once()) ->method("execute") - ->with($placeholderVars); + ->with([$placeholderVars]); static::assertInstanceOf( ResultSet::class,