Change PDO to singleton instance

- Add SQLite throwaway DB implementation
- Add integration tests for Custom class
This commit is contained in:
Daniel J. Summers 2024-06-07 22:14:17 -04:00
parent d9ffc36fe6
commit 1ab961e35a
10 changed files with 244 additions and 11 deletions

View File

@ -17,7 +17,8 @@
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Test\\Unit\\": "./tests/unit", "Test\\Unit\\": "./tests/unit",
"Test\\Integration\\": "./tests/integration" "Test\\Integration\\": "./tests/integration",
"Test\\Integration\\SQLite\\": "./tests/integration/sqlite"
} }
} }
} }

14
composer.lock generated
View File

@ -619,16 +619,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.1.3", "version": "11.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "d475be032238173ca3b0a516f5cc291d174708ae" "reference": "705eba0190afe04bc057f565ad843267717cf109"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d475be032238173ca3b0a516f5cc291d174708ae", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/705eba0190afe04bc057f565ad843267717cf109",
"reference": "d475be032238173ca3b0a516f5cc291d174708ae", "reference": "705eba0190afe04bc057f565ad843267717cf109",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -667,7 +667,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "11.1-dev" "dev-main": "11.2-dev"
} }
}, },
"autoload": { "autoload": {
@ -699,7 +699,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.1.3" "source": "https://github.com/sebastianbergmann/phpunit/tree/11.2.0"
}, },
"funding": [ "funding": [
{ {
@ -715,7 +715,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-04-24T06:34:25+00:00" "time": "2024-06-07T04:48:50+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",

View File

@ -58,4 +58,10 @@ class Configuration
return self::$_pdo; return self::$_pdo;
} }
public static function resetPDO(): void
{
self::$_pdo = null;
}
} }

View File

@ -4,6 +4,7 @@ namespace BitBadger\PDODocument;
use BitBadger\PDODocument\Mapper\Mapper; use BitBadger\PDODocument\Mapper\Mapper;
use PDO; use PDO;
use PDOException;
use PDOStatement; use PDOStatement;
/** /**
@ -22,7 +23,13 @@ class Custom
public static function &runQuery(string $query, array $parameters): PDOStatement public static function &runQuery(string $query, array $parameters): PDOStatement
{ {
$debug = defined('PDO_DOC_DEBUG_SQL'); $debug = defined('PDO_DOC_DEBUG_SQL');
$stmt = Configuration::dbConn()->prepare($query); try {
$stmt = Configuration::dbConn()->prepare($query);
} catch (PDOException $ex) {
$keyword = explode(' ', $query, 2)[0];
throw new DocumentException("Error executing $keyword statement: " . Configuration::dbConn()->errorCode(),
previous: $ex);
}
foreach ($parameters as $key => $value) { foreach ($parameters as $key => $value) {
if ($debug) echo "<pre>Binding $value to $key\n</pre>"; if ($debug) echo "<pre>Binding $value to $key\n</pre>";
$dataType = match (true) { $dataType = match (true) {
@ -115,7 +122,7 @@ class Custom
* @return mixed|false|T The scalar value if found, false if not * @return mixed|false|T The scalar value if found, false if not
* @throws DocumentException If any is encountered * @throws DocumentException If any is encountered
*/ */
public static function scalar(string $query, array $parameters, Mapper $mapper, ?PDO $pdo = null): mixed public static function scalar(string $query, array $parameters, Mapper $mapper): mixed
{ {
try { try {
$stmt = &self::runQuery($query, $parameters); $stmt = &self::runQuery($query, $parameters);

View File

@ -3,7 +3,7 @@
namespace BitBadger\PDODocument\Mapper; namespace BitBadger\PDODocument\Mapper;
/** /**
* Map a string result from the given * Map a string result from the
* *
* @implements Mapper<string> * @implements Mapper<string>
*/ */

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace Test\Integration;
/**
* A sub-document for testing
*/
class SubDocument
{
public function __construct(public string $foo = '', public string $bar = '') { }
}

View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Test\Integration;
class TestDocument
{
public function __construct(public string $id = '', public string $value = '', public int $num_value = 0,
public ?SubDocument $sub = null) { }
}

View File

@ -0,0 +1,128 @@
<?php declare(strict_types=1);
namespace Test\Integration\SQLite;
use BitBadger\PDODocument\Count;
use BitBadger\PDODocument\Custom;
use BitBadger\PDODocument\DocumentException;
use BitBadger\PDODocument\Mapper\CountMapper;
use BitBadger\PDODocument\Mapper\DocumentMapper;
use BitBadger\PDODocument\Mapper\StringMapper;
use BitBadger\PDODocument\Query;
use PHPUnit\Framework\TestCase;
use Test\Integration\TestDocument;
/**
* SQLite Integration tests for the Custom class
*/
class CustomTest extends TestCase
{
/** @var string Database name for throwaway database */
private string $dbName;
public function setUp(): void
{
parent::setUp();
$this->dbName = ThrowawayDb::create();
}
public function tearDown(): void
{
ThrowawayDb::destroy($this->dbName);
}
public function testRunQuerySucceedsWithAValidQuery()
{
$stmt = &Custom::runQuery('SELECT data FROM ' . ThrowawayDb::TABLE . ' LIMIT 1', []);
try {
$this->assertNotNull($stmt, 'The statement should not have been null');
} finally {
$stmt = null;
}
}
public function testRunQueryFailsWithAnInvalidQuery()
{
$this->expectException(DocumentException::class);
$stmt = &Custom::runQuery('GRAB stuff FROM over_there UNTIL done', []);
try {
$this->assertTrue(false, 'This code should not be reached');
} finally {
$stmt = null;
}
}
public function testListSucceedsWhenDataIsFound()
{
$list = Custom::list(Query::selectFromTable(ThrowawayDb::TABLE), [], new DocumentMapper(TestDocument::class));
$this->assertNotNull($list, 'The document list should not be null');
$count = 0;
foreach ($list->items() as $ignored) $count++;
$this->assertEquals(5, $count, 'There should have been 5 documents in the list');
}
public function testListSucceedsWhenNoDataIsFound()
{
$list = Custom::list(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'num_value' > :value",
[':value' => 100], new DocumentMapper(TestDocument::class));
$this->assertNotNull($list, 'The document list should not be null');
$count = 0;
foreach ($list->items() as $ignored) $count++;
$this->assertEquals(0, $count, 'There should have been no documents in the list');
}
public function testArraySucceedsWhenDataIsFound()
{
$array = Custom::array(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'sub' IS NOT NULL", [],
new DocumentMapper(TestDocument::class));
$this->assertNotNull($array, 'The document array should not be null');
$this->assertCount(2, $array, 'There should have been 2 documents in the array');
}
public function testArraySucceedsWhenNoDataIsFound()
{
$array = Custom::array(Query::selectFromTable(ThrowawayDb::TABLE) . " WHERE data->>'value' = :value",
[':value' => 'not there'], new DocumentMapper(TestDocument::class));
$this->assertNotNull($array, 'The document array should not be null');
$this->assertCount(0, $array, 'There should have been no documents in the array');
}
public function testSingleSucceedsWhenARowIsFound(): void
{
$doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id", [':id' => 'one'],
new DocumentMapper(TestDocument::class));
$this->assertNotNull($doc, 'There should have been a document returned');
$this->assertEquals('one', $doc->id, 'The incorrect document was returned');
}
public function testSingleSucceedsWhenARowIsNotFound(): void
{
$doc = Custom::single('SELECT data FROM ' . ThrowawayDb::TABLE . " WHERE data->>'id' = :id",
[':id' => 'eighty'], new DocumentMapper(TestDocument::class));
$this->assertFalse($doc, 'There should not have been a document returned');
}
public function testNonQuerySucceedsWhenOperatingOnData()
{
Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE, []);
try {
$remaining = Count::all(ThrowawayDb::TABLE);
$this->assertEquals(0, $remaining, 'There should be no documents remaining in the table');
} finally {
$this->dbName = ThrowawayDb::exchange($this->dbName);
}
}
public function testNonQuerySucceedsWhenNoDataMatchesWhereClause()
{
Custom::nonQuery('DELETE FROM ' . ThrowawayDb::TABLE . " WHERE data->>'num_value' > :value", [':value' => 100]);
$remaining = Count::all(ThrowawayDb::TABLE);
$this->assertEquals(5, $remaining, 'There should be 5 documents remaining in the table');
}
public function testScalarSucceeds()
{
$value = Custom::scalar("SELECT 5 AS it", [], new CountMapper());
$this->assertEquals(5, $value, 'The scalar value was not returned correctly');
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Test\Integration\SQLite;
use BitBadger\PDODocument\Configuration;
use BitBadger\PDODocument\Definition;
use BitBadger\PDODocument\Document;
use BitBadger\PDODocument\DocumentException;
use BitBadger\PDODocument\Mode;
use Test\Integration\SubDocument;
use Test\Integration\TestDocument;
/**
* Utilities to create and destroy a throwaway SQLite database to use for testing
*/
class ThrowawayDb
{
/** @var string The table used for document manipulation */
public const string TABLE = "test_table";
/**
* Create a throwaway SQLite database
*
* @param bool $withData Whether to initialize this database with data (optional; defaults to `true`)
* @return string The name of the database (use to pass to `destroy` function at end of test)
* @throws DocumentException If any is encountered
*/
public static function create(bool $withData = true): string
{
$fileName = sprintf('throwaway-%s-%d.db', date('His'), rand(10, 99));
Configuration::$pdoDSN = "sqlite:./$fileName";
Configuration::$mode = Mode::SQLite;
Configuration::resetPDO();
if ($withData) {
Definition::ensureTable(self::TABLE);
Document::insert(self::TABLE, new TestDocument('one', 'FIRST!', 0));
Document::insert(self::TABLE, new TestDocument('two', 'another', 10, new SubDocument('green', 'blue')));
Document::insert(self::TABLE, new TestDocument('three', '', 4));
Document::insert(self::TABLE, new TestDocument('four', 'purple', 17, new SubDocument('green', 'red')));
Document::insert(self::TABLE, new TestDocument('five', 'purple', 18));
}
return $fileName;
}
/**
* Destroy a throwaway SQLite database
*
* @param string $fileName The name of the SQLite database to be deleted
*/
public static function destroy(string $fileName): void
{
Configuration::resetPDO();
unlink("./$fileName");
}
/**
* Destroy the given throwaway database and create another
*
* @param string $fileName The name of the database to be destroyed
* @param bool $withData Whether to initialize the database with data (optional; defaults to `true`)
* @return string The name of the new database
* @throws DocumentException If any is encountered
*/
public static function exchange(string $fileName, bool $withData = true): string
{
self::destroy($fileName);
return self::create($withData);
}
}