From 86e820e05e5f18a3e2fd7938fbd525f52a5b0fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Fri, 19 Apr 2024 18:10:56 +0200 Subject: [PATCH] Better enum support when interacting with SQL databases --- CHANGELOG.md | 1 + src/mako/database/connections/Connection.php | 17 +++- .../database/query/compilers/Compiler.php | 6 -- ...derTestCase.php => InMemoryDbTestCase.php} | 4 +- tests/integration/ORMTestCase.php | 2 +- tests/integration/database/EnumTest.php | 99 +++++++++++++++++++ .../query/compilers/BaseCompilerTest.php | 64 +++++++++++- .../query/compilers/BaseCompilerTest.php | 50 ---------- 8 files changed, 177 insertions(+), 66 deletions(-) rename tests/integration/{BuilderTestCase.php => InMemoryDbTestCase.php} (91%) create mode 100644 tests/integration/database/EnumTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e804f05..9122229d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The major version bump is due to dropped support for PHP `8.1` and a several bre * Added `Connection::blob()` method that allows you to easily fetch a blob column as a stream. * Added `Query::blob()` method that allows you to easily fetch a blob column as a stream. * The `ManyToMany::unlink()` and `ManyToMany::synchronize()` methods now support junction attributes. +* Enums are now also supported when writing "raw" SQL. #### Improvements diff --git a/src/mako/database/connections/Connection.php b/src/mako/database/connections/Connection.php index be73e8931..5aa945818 100644 --- a/src/mako/database/connections/Connection.php +++ b/src/mako/database/connections/Connection.php @@ -20,6 +20,7 @@ use PDOStatement; use SensitiveParameter; use Throwable; +use UnitEnum; use function array_shift; use function array_splice; @@ -383,10 +384,18 @@ protected function bindParameter(PDOStatement $statement, int $key, mixed $value elseif (is_null($value)) { $type = PDO::PARAM_NULL; } - elseif (is_object($value) && $value instanceof TypeInterface) { - $value = $value->getValue(); - - $type = $value->getType(); + elseif (is_object($value)) { + if ($value instanceof TypeInterface) { + $value = $value->getValue(); + $type = $value->getType(); + } + elseif ($value instanceof UnitEnum) { + $value = $value->value ?? $value->name; + $type = is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR; + } + else { + throw new DatabaseException(vsprintf('Unable to bind object of type [ %s ] to the prepared statement.', [$value::class])); + } } else { $type = PDO::PARAM_STR; diff --git a/src/mako/database/query/compilers/Compiler.php b/src/mako/database/query/compilers/Compiler.php index 18c1a2218..e56734148 100644 --- a/src/mako/database/query/compilers/Compiler.php +++ b/src/mako/database/query/compilers/Compiler.php @@ -16,7 +16,6 @@ use function array_keys; use function array_shift; -use function enum_exists; use function explode; use function implode; use function is_array; @@ -356,11 +355,6 @@ protected function param(mixed $param, bool $enclose = true): string elseif ($param instanceof DateTimeInterface) { $this->params[] = $param->format(static::$dateFormat); - return '?'; - } - elseif (enum_exists($param::class)) { - $this->params[] = $param->value ?? $param->name; - return '?'; } } diff --git a/tests/integration/BuilderTestCase.php b/tests/integration/InMemoryDbTestCase.php similarity index 91% rename from tests/integration/BuilderTestCase.php rename to tests/integration/InMemoryDbTestCase.php index cb19e55ad..735945b25 100644 --- a/tests/integration/BuilderTestCase.php +++ b/tests/integration/InMemoryDbTestCase.php @@ -11,9 +11,9 @@ use mako\tests\TestCase; /** - * Builder test case. + * In memory DB test case. */ -abstract class BuilderTestCase extends TestCase +abstract class InMemoryDbTestCase extends TestCase { /** * diff --git a/tests/integration/ORMTestCase.php b/tests/integration/ORMTestCase.php index dedcc305a..557674338 100644 --- a/tests/integration/ORMTestCase.php +++ b/tests/integration/ORMTestCase.php @@ -10,7 +10,7 @@ /** * ORM test case. */ -abstract class ORMTestCase extends BuilderTestCase +abstract class ORMTestCase extends InMemoryDbTestCase { /** *{@inheritDoc} diff --git a/tests/integration/database/EnumTest.php b/tests/integration/database/EnumTest.php new file mode 100644 index 000000000..80374f396 --- /dev/null +++ b/tests/integration/database/EnumTest.php @@ -0,0 +1,99 @@ +connectionManager->getConnection()->first(<<<'SQL' + SELECT * FROM users WHERE email = ? + SQL, [BackedUserEmailEnum::FOO]); + + $this->assertSame(BackedUserEmailEnum::FOO->value, $user->email); + + $user = $this->connectionManager->getConnection()->first(<<<'SQL' + SELECT * FROM users WHERE email = ? + SQL, [BackedUserEmailEnum::BAR]); + + $this->assertSame(BackedUserEmailEnum::BAR->value, $user->email); + } + + /** + * + */ + public function testBackedIntEnum(): void + { + $user = $this->connectionManager->getConnection()->first(<<<'SQL' + SELECT * FROM users WHERE id = ? + SQL, [BackedUserIdEnum::FOO]); + + $this->assertSame(BackedUserIdEnum::FOO->value, $user->id); + + $user = $this->connectionManager->getConnection()->first(<<<'SQL' + SELECT * FROM users WHERE id = ? + SQL, [BackedUserIdEnum::BAR]); + + $this->assertSame(BackedUserIdEnum::BAR->value, $user->id); + } + + /** + * + */ + public function testEnum(): void + { + $user = $this->connectionManager->getConnection()->first(<<<'SQL' + SELECT * FROM users WHERE username = ? + SQL, [UsernameEnum::foo]); + + $this->assertSame(UsernameEnum::foo->name, $user->username); + + $user = $this->connectionManager->getConnection()->first(<<<'SQL' + SELECT * FROM users WHERE username = ? + SQL, [UsernameEnum::bar]); + + $this->assertSame(UsernameEnum::bar->name, $user->username); + } +} diff --git a/tests/integration/database/query/compilers/BaseCompilerTest.php b/tests/integration/database/query/compilers/BaseCompilerTest.php index dcabab7cc..fd18f3b78 100644 --- a/tests/integration/database/query/compilers/BaseCompilerTest.php +++ b/tests/integration/database/query/compilers/BaseCompilerTest.php @@ -13,9 +13,31 @@ use mako\database\query\Subquery; use mako\pagination\PaginationFactoryInterface; use mako\pagination\PaginationInterface; -use mako\tests\integration\BuilderTestCase; +use mako\tests\integration\InMemoryDbTestCase; use Mockery; -use PDO; + +// -------------------------------------------------------------------------- +// START CLASSES +// -------------------------------------------------------------------------- + +enum BackedUserEmailEnum: string +{ + case FOO = 'foo@example.org'; +} + +enum BackedUserIdEnum: int +{ + case FOO = 1; +} + +enum UsernameEnum +{ + case foo; +} + +// -------------------------------------------------------------------------- +// END CLASSES +// -------------------------------------------------------------------------- /** * @group integration @@ -23,7 +45,7 @@ * @requires extension PDO * @requires extension pdo_sqlite */ -class BaseCompilerTest extends BuilderTestCase +class BaseCompilerTest extends InMemoryDbTestCase { public function setUp(): void { @@ -397,4 +419,40 @@ public function testBlob(): void $this->assertEquals('SELECT "blob" FROM "blobs" LIMIT 1', $this->connectionManager->getConnection()->getLog()[0]['query']); } + + /** + * + */ + public function testBackedStringEnum(): void + { + $query = new Query($this->connectionManager->getConnection()); + + $user = $query->table('users')->where('email', '=', BackedUserEmailEnum::FOO)->first(); + + $this->assertSame(BackedUserEmailEnum::FOO->value, $user->email); + } + + /** + * + */ + public function testBackedIntEnum(): void + { + $query = new Query($this->connectionManager->getConnection()); + + $user = $query->table('users')->where('id', '=', BackedUserIdEnum::FOO)->first(); + + $this->assertSame(BackedUserIdEnum::FOO->value, $user->id); + } + + /** + * + */ + public function testEnum(): void + { + $query = new Query($this->connectionManager->getConnection()); + + $user = $query->table('users')->where('username', '=', UsernameEnum::foo)->first(); + + $this->assertSame(UsernameEnum::foo->name, $user->username); + } } diff --git a/tests/unit/database/query/compilers/BaseCompilerTest.php b/tests/unit/database/query/compilers/BaseCompilerTest.php index 141001985..f63c73f34 100644 --- a/tests/unit/database/query/compilers/BaseCompilerTest.php +++ b/tests/unit/database/query/compilers/BaseCompilerTest.php @@ -19,26 +19,6 @@ use mako\tests\TestCase; use Mockery; -// -------------------------------------------------------------------------- -// START CLASSES -// -------------------------------------------------------------------------- - -enum FooEnum -{ - case ONE; - case TWO; -} - -enum BarEnum: int -{ - case ONE = 1; - case TWO = 2; -} - -// -------------------------------------------------------------------------- -// END CLASSES -// -------------------------------------------------------------------------- - /** * @group unit */ @@ -2059,34 +2039,4 @@ public function testDateTimeParameter(): void $this->assertEquals('SELECT * FROM "foobar" WHERE "foo" = ?', $query['sql']); $this->assertEquals(['2021-11-02 13:37:00'], $query['params']); } - - /** - * - */ - public function testEnumParameter(): void - { - $query = $this->getBuilder(); - - $query->where('foo', '=', FooEnum::ONE); - - $query = $query->getCompiler()->select(); - - $this->assertEquals('SELECT * FROM "foobar" WHERE "foo" = ?', $query['sql']); - $this->assertEquals(['ONE'], $query['params']); - } - - /** - * - */ - public function testBackedEnumParameter(): void - { - $query = $this->getBuilder(); - - $query->where('foo', '=', BarEnum::ONE); - - $query = $query->getCompiler()->select(); - - $this->assertEquals('SELECT * FROM "foobar" WHERE "foo" = ?', $query['sql']); - $this->assertEquals([1], $query['params']); - } }