From c371fc762a887c7030b9667bed1a21ee954858b4 Mon Sep 17 00:00:00 2001 From: Greg Bowler <greg.bowler@g105b.com> Date: Thu, 31 Oct 2019 12:41:46 +0000 Subject: [PATCH] Implementation of type-safe fetch methods (#175) * Write tests for #115 * Implement type-safe fetch functionality, closes #115 * Use existing fetch functions * Introduce `Fetchable` trait to share code over Database, QueryCollection --- src/Connection/Driver.php | 16 ++ src/Database.php | 13 +- src/Fetchable.php | 158 ++++++++++++++++ src/Query/QueryCollection.php | 41 +++-- src/Result/ResultSet.php | 1 - src/Type.php | 10 ++ test/unit/DatabaseTest.php | 1 - test/unit/IntegrationTest.php | 180 ++++++++++++++++++- test/unit/Query/QueryCollectionCRUDsTest.php | 1 - 9 files changed, 387 insertions(+), 34 deletions(-) create mode 100644 src/Fetchable.php create mode 100644 src/Type.php diff --git a/src/Connection/Driver.php b/src/Connection/Driver.php index a6159a3..157628b 100644 --- a/src/Connection/Driver.php +++ b/src/Connection/Driver.php @@ -2,6 +2,22 @@ namespace Gt\Database\Connection; class Driver { + const AVAILABLE_DRIVERS = [ + "cubrid", + "dblib", // Sybase databases + "sybase", + "firebird", + "ibm", + "informix", + "mysql", + "sqlsrv", // MS SQL Server and SQL Azure databases + "oci", // Oracle + "odbc", + "pgsql", // PostgreSQL + "sqlite", + "4D", + ]; + /** @var SettingsInterface */ protected $settings; /** @var Connection */ diff --git a/src/Database.php b/src/Database.php index 09aca67..a556b4b 100644 --- a/src/Database.php +++ b/src/Database.php @@ -1,6 +1,7 @@ <?php namespace Gt\Database; +use DateTime; use Gt\Database\Connection\Connection; use Gt\Database\Result\Row; use Gt\Database\Connection\DefaultSettings; @@ -18,6 +19,8 @@ * required as the default name will be used. */ class Database { + use Fetchable; + const COLLECTION_SEPARATOR_CHARACTERS = [".", "/", "\\"]; /** @var QueryCollectionFactory[] */ protected $queryCollectionFactoryArray; @@ -36,16 +39,6 @@ public function __construct(SettingsInterface...$connectionSettings) { $this->storeQueryCollectionFactoryFromSettings($connectionSettings); } - public function fetch(string $queryName, ...$bindings):?Row { - $result = $this->query($queryName, $bindings); - - return $result->fetch(); - } - - public function fetchAll(string $queryName, ...$bindings):ResultSet { - return $this->query($queryName, $bindings); - } - public function insert(string $queryName, ...$bindings):int { $result = $this->query($queryName, $bindings); diff --git a/src/Fetchable.php b/src/Fetchable.php new file mode 100644 index 0000000..f8e5d65 --- /dev/null +++ b/src/Fetchable.php @@ -0,0 +1,158 @@ +<?php +namespace Gt\Database; + +use DateTime; +use Gt\Database\Result\ResultSet; +use Gt\Database\Result\Row; + +trait Fetchable { + public function fetch(string $queryName, ...$bindings):?Row { + /** @var ResultSet $result */ + $result = $this->query($queryName, ...$bindings); + return $result->current(); + } + + public function fetchAll(string $queryName, ...$bindings):ResultSet { + return $this->query($queryName, ...$bindings); + } + + public function fetchBool(string $queryName, ...$bindings):?bool { + return $this->fetchTyped( + Type::BOOL, + $queryName, + $bindings + ); + } + + public function fetchString(string $queryName, ...$bindings):?string { + return $this->fetchTyped( + Type::STRING, + $queryName, + $bindings + ); + } + + public function fetchInt(string $queryName, ...$bindings):?int { + return $this->fetchTyped( + Type::INT, + $queryName, + $bindings + ); + } + + public function fetchFloat(string $queryName, ...$bindings):?float { + return $this->fetchTyped( + Type::FLOAT, + $queryName, + $bindings + ); + } + + public function fetchDateTime(string $queryName, ...$bindings):?DateTime { + return $this->fetchTyped( + Type::DATETIME, + $queryName, + $bindings + ); + } + + /** @return bool[] */ + public function fetchAllBool(string $queryName, ...$bindings):array { + return $this->fetchAllTyped( + Type::BOOL, + $queryName, + $bindings + ); + } + + /** @return string[] */ + public function fetchAllString(string $queryName, ...$bindings):array { + return $this->fetchAllTyped( + Type::STRING, + $queryName, + $bindings + ); + } + + /** @return int[] */ + public function fetchAllInt(string $queryName, ...$bindings):array { + return $this->fetchAllTyped( + Type::INT, + $queryName, + $bindings + ); + } + + /** @return float[] */ + public function fetchAllFloat(string $queryName, ...$bindings):array { + return $this->fetchAllTyped( + Type::FLOAT, + $queryName, + $bindings + ); + } + + /** @return DateTime[] */ + public function fetchAllDateTime(string $queryName, ...$bindings):array { + return $this->fetchAllTyped( + Type::DATETIME, + $queryName, + $bindings + ); + } + + protected function fetchTyped( + string $type, + string $queryName, + ...$bindings + ) { + $row = $this->fetch($queryName, ...$bindings); + if(is_null($row)) { + return null; + } + + return $this->castRow($type, $row); + } + + protected function fetchAllTyped( + string $type, + string $queryName, + ...$bindings + ):array { + $array = []; + + $resultSet = $this->fetchAll($queryName, ...$bindings); + foreach($resultSet as $row) { + $array []= $this->castRow($type, $row); + } + + return $array; + } + + protected function castRow(string $type, Row $row) { + $assocArray = $row->toArray(); + reset($assocArray); + $key = key($assocArray); + $value = $assocArray[$key]; + + switch($type) { + case Type::BOOL: + case "boolean": + return (bool)$value; + + case Type::STRING: + return (string)$value; + + case Type::INT: + case "integer": + return (int)$value; + + case Type::FLOAT: + return (float)$value; + + case Type::DATETIME: + case "datetime": + return new DateTime($value); + } + } +} \ No newline at end of file diff --git a/src/Query/QueryCollection.php b/src/Query/QueryCollection.php index bc591ed..4d0b70b 100644 --- a/src/Query/QueryCollection.php +++ b/src/Query/QueryCollection.php @@ -2,10 +2,13 @@ namespace Gt\Database\Query; use Gt\Database\Connection\Driver; +use Gt\Database\Fetchable; use Gt\Database\Result\ResultSet; use Gt\Database\Result\Row; class QueryCollection { + use Fetchable; + /** @var string */ protected $directoryPath; /** @var QueryFactory */ @@ -56,25 +59,25 @@ public function insert( )->lastInsertId(); } - public function fetch( - string $name, - ...$placeholderMap - ):?Row { - return $this->query( - $name, - ...$placeholderMap - )->current(); - } - - public function fetchAll( - string $name, - ...$placeholderMap - ):ResultSet { - return $this->query( - $name, - ...$placeholderMap - ); - } +// public function fetch( +// string $name, +// ...$placeholderMap +// ):?Row { +// return $this->query( +// $name, +// ...$placeholderMap +// )->current(); +// } +// +// public function fetchAll( +// string $name, +// ...$placeholderMap +// ):ResultSet { +// return $this->query( +// $name, +// ...$placeholderMap +// ); +// } public function update( string $name, diff --git a/src/Result/ResultSet.php b/src/Result/ResultSet.php index 01b399c..80f4637 100644 --- a/src/Result/ResultSet.php +++ b/src/Result/ResultSet.php @@ -106,7 +106,6 @@ public function rewind():void { public function current():?Row { $this->fetchUpToIteratorIndex(); - return $this->current_row; } diff --git a/src/Type.php b/src/Type.php new file mode 100644 index 0000000..1569c4a --- /dev/null +++ b/src/Type.php @@ -0,0 +1,10 @@ +<?php +namespace Gt\Database; + +class Type { + const BOOL = "bool"; + const STRING = "string"; + const INT = "int"; + const FLOAT = "float"; + const DATETIME = "datetime"; +} \ No newline at end of file diff --git a/test/unit/DatabaseTest.php b/test/unit/DatabaseTest.php index 078aab2..24b4f3f 100644 --- a/test/unit/DatabaseTest.php +++ b/test/unit/DatabaseTest.php @@ -5,7 +5,6 @@ use Gt\Database\Query\QueryCollection; use Gt\Database\Query\QueryCollectionNotFoundException; use PHPUnit\Framework\TestCase; -use Gt\Database\Test\Helper\Helper; class DatabaseTest extends TestCase { public function testInterface() { diff --git a/test/unit/IntegrationTest.php b/test/unit/IntegrationTest.php index 80bc6ad..f146b03 100644 --- a/test/unit/IntegrationTest.php +++ b/test/unit/IntegrationTest.php @@ -1,6 +1,7 @@ <?php namespace Gt\Database; +use DateTime; use Exception; use Gt\Database\Connection\Driver; use Gt\Database\Connection\Settings; @@ -24,14 +25,14 @@ public function setUp():void { $driver = $this->db->getDriver(); $connection = $driver->getConnection(); - $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);"); + $output = $connection->exec("CREATE TABLE test_table ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(32), number integer, isEven bool, halfNumber float, 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, number) VALUES ('one', 1), ('two', 2), ('three', 3)"); + $insertStatement = $connection->prepare("INSERT INTO test_table (name, number, isEven, halfNumber) VALUES ('one', 1, false, 0.5), ('two', 2, true, 1), ('three', 3, false, 1.5)"); $success = $insertStatement->execute(); if($success === false) { $error = $connection->errorInfo(); @@ -236,6 +237,181 @@ public function testMultipleArrayParameterUsage() { static::assertEquals(3, $result3->id); } + public function testFetchBool() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/isNumberEven.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT isEven FROM test_table where number = ?" + ); + + $result1 = $this->db->fetchBool("exampleCollection/isNumberEven", 1); + $result2 = $this->db->fetchBool("exampleCollection/isNumberEven", 2); + self::assertFalse($result1); + self::assertTrue($result2); + } + + public function testFetchAllBool() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getAllBools.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT isEven FROM test_table" + ); + + $result = $this->db->fetchAllBool("exampleCollection/getAllBools"); + self::assertCount(3, $result); + self::assertIsArray($result); + foreach($result as $i => $value) { + if($i % 2 !== 0) { + self::assertTrue($value); + } + else { + self::assertFalse($value); + } + } + } + + + public function testFetchInt() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getIdByName.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT id FROM test_table WHERE name = :name LIMIT 1" + ); + + $result = $this->db->fetchInt("exampleCollection/getIdByName", "two"); + self::assertEquals(2, $result); + self::assertIsInt($result); + } + + public function testFetchAllInt() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getAllNumbers.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT number FROM test_table" + ); + + $result = $this->db->fetchAllInt("exampleCollection/getAllNumbers"); + self::assertCount(3, $result); + self::assertIsArray($result); + foreach($result as $value) { + self::assertIsInt($value); + } + } + + + public function testFetchString() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getNameById.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT name FROM test_table WHERE id = ? LIMIT 1" + ); + + $result = $this->db->fetchString("exampleCollection/getNameById", 2); + self::assertEquals("two", $result); + self::assertIsString($result); + } + + public function testFetchAllString() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getAllNames.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT name FROM test_table" + ); + + $result = $this->db->fetchAllString("exampleCollection/getAllNames"); + self::assertCount(3, $result); + self::assertIsArray($result); + foreach($result as $value) { + self::assertIsString($value); + } + } + + public function testFetchFloat() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getHalfByNumber.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT halfNumber FROM test_table WHERE number = ? LIMIT 1" + ); + + $result2 = $this->db->fetchFloat("exampleCollection/getHalfByNumber", 2); + $result3 = $this->db->fetchFloat("exampleCollection/getHalfByNumber", 3); + self::assertIsFloat($result2); + self::assertIsFloat($result3); + self::assertEquals(1, $result2); + self::assertEquals(1.5, $result3); + } + + public function testFetchAllFloat() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getAllHalves.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT halfNumber FROM test_table" + ); + + $result = $this->db->fetchAllFloat("exampleCollection/getAllHalves"); + self::assertCount(3, $result); + self::assertIsArray($result); + foreach($result as $value) { + self::assertIsFloat($value); + } + } + + public function testFetchDateTime() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getLatestTimestamp.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT timestamp FROM test_table ORDER BY timestamp DESC LIMIT 1" + ); + + $result = $this->db->fetchDateTime("exampleCollection/getLatestTimestamp"); + self::assertInstanceOf(DateTime::class, $result); + } + + public function testFetchAllDateTime() { + $queryCollectionPath = $this->queryBase . "/exampleCollection"; + $getByNameNumberQueryPath = $queryCollectionPath . "/getAllTimestamps.sql"; + + mkdir($queryCollectionPath, 0775, true); + file_put_contents( + $getByNameNumberQueryPath, + "SELECT timestamp FROM test_table ORDER BY timestamp" + ); + + $result = $this->db->fetchAllDateTime("exampleCollection/getAllTimestamps"); + self::assertCount(3, $result); + self::assertIsArray($result); + foreach($result as $value) { + self::assertInstanceOf(DateTime::class, $value); + } + } + private function settingsSingleton():Settings { if(is_null($this->settings)) { $this->settings = new Settings( diff --git a/test/unit/Query/QueryCollectionCRUDsTest.php b/test/unit/Query/QueryCollectionCRUDsTest.php index 9656a13..9410fe0 100644 --- a/test/unit/Query/QueryCollectionCRUDsTest.php +++ b/test/unit/Query/QueryCollectionCRUDsTest.php @@ -25,7 +25,6 @@ public function setUp():void { ->willReturn($this->mockQuery); /** @var QueryFactory $mockQueryFactory */ - $this->queryCollection = new QueryCollection( __DIR__, new Driver(new DefaultSettings()),