From 4272e3ae4a4d2c6e4105957f208b822e7e643185 Mon Sep 17 00:00:00 2001 From: Thiago Cordeiro Date: Fri, 16 May 2025 09:06:17 +0200 Subject: [PATCH] Added query builder --- README.md | 15 +-- composer.json | 5 +- composer.lock | 18 +-- src/Connection/Connection.php | 2 +- src/Connection/ConnectionDriver.php | 11 -- src/Connection/Driver.php | 19 ++++ src/Connection/Pdo/MysqlConnection.php | 6 +- src/EntityRecordRepository.php | 19 +++- src/Functions/query.php | 92 ++++++++++++++++ src/Query/Conditions/Condition.php | 15 +++ src/Query/Conditions/ConditionList.php | 24 ++++ src/Query/Conditions/FieldCondition.php | 24 ++++ src/Query/Conditions/FilteringCondition.php | 26 +++++ src/Query/Conditions/GroupedCondition.php | 26 +++++ src/Query/Operator.php | 13 +++ src/Query/Query.php | 104 ++++++++++++++++++ src/RecordRepository.php | 65 ++++------- tests/TestCase.php | 11 ++ .../Connection/Pdo/GenericConnectionTest.php | 10 +- .../Pdo/NestedTransactionConnectionTest.php | 6 +- tests/Unit/EntityRecordRepositoryTest.php | 50 +++++---- tests/Unit/Functions/BetweenTest.php | 22 ++++ tests/Unit/Functions/DifferentOfTest.php | 22 ++++ tests/Unit/Functions/EqualsToTest.php | 22 ++++ .../Functions/GreaterThanOrEqualToTest.php | 23 ++++ tests/Unit/Functions/GreaterThanTest.php | 22 ++++ tests/Unit/Functions/InTest.php | 22 ++++ tests/Unit/Functions/IsNotNullTest.php | 22 ++++ tests/Unit/Functions/IsNullTest.php | 22 ++++ tests/Unit/Functions/LikeTest.php | 22 ++++ tests/Unit/Functions/NotInTest.php | 22 ++++ tests/Unit/Functions/NotLikeTest.php | 22 ++++ .../Functions/SmallerThanOrEqualToTest.php | 22 ++++ tests/Unit/Functions/SmallerThanTest.php | 22 ++++ tests/Unit/Functions/WhereTest.php | 33 ++++++ .../Conditions/FieldConditionListTest.php | 37 +++++++ .../Query/Conditions/FieldConditionTest.php | 23 ++++ tests/Unit/Query/QueryTest.php | 47 ++++++++ tests/Unit/RecordRepositoryTest.php | 40 ++++--- 39 files changed, 898 insertions(+), 130 deletions(-) delete mode 100644 src/Connection/ConnectionDriver.php create mode 100644 src/Connection/Driver.php create mode 100644 src/Functions/query.php create mode 100644 src/Query/Conditions/Condition.php create mode 100644 src/Query/Conditions/ConditionList.php create mode 100644 src/Query/Conditions/FieldCondition.php create mode 100644 src/Query/Conditions/FilteringCondition.php create mode 100644 src/Query/Conditions/GroupedCondition.php create mode 100644 src/Query/Operator.php create mode 100644 src/Query/Query.php create mode 100644 tests/Unit/Functions/BetweenTest.php create mode 100644 tests/Unit/Functions/DifferentOfTest.php create mode 100644 tests/Unit/Functions/EqualsToTest.php create mode 100644 tests/Unit/Functions/GreaterThanOrEqualToTest.php create mode 100644 tests/Unit/Functions/GreaterThanTest.php create mode 100644 tests/Unit/Functions/InTest.php create mode 100644 tests/Unit/Functions/IsNotNullTest.php create mode 100644 tests/Unit/Functions/IsNullTest.php create mode 100644 tests/Unit/Functions/LikeTest.php create mode 100644 tests/Unit/Functions/NotInTest.php create mode 100644 tests/Unit/Functions/NotLikeTest.php create mode 100644 tests/Unit/Functions/SmallerThanOrEqualToTest.php create mode 100644 tests/Unit/Functions/SmallerThanTest.php create mode 100644 tests/Unit/Functions/WhereTest.php create mode 100644 tests/Unit/Query/Conditions/FieldConditionListTest.php create mode 100644 tests/Unit/Query/Conditions/FieldConditionTest.php create mode 100644 tests/Unit/Query/QueryTest.php diff --git a/README.md b/README.md index 7822586..d7d123a 100644 --- a/README.md +++ b/README.md @@ -231,13 +231,13 @@ class UserRepository extends EntityRecordRepository ```php insertOne($entry) -selectOneWhere(where: ['id' => 10]) +selectOneWhere(where: where(['id' => equalsTo(10)])) selectOneByQuery(selectQuery: 'SELECT * FROM table where id = :id', bindings: ['id' => 10]) -selectManyWhere([where: 'deleted_at' => null], limit: 10, offset: 100) +selectManyWhere(where: where(['deleted_at' => isNull()]), limit: 10, offset: 100) selectManyByQuery(selectQuery: 'SELECT * FROM table where deleted_at is null', bindings: []) -existsWhere(where: ['id' => 10]) -deleteWhere(where: ['id' => 10]) -updateWhere(values: ['name' => 'Arthur Dent', 'date_of_birth' => '1990-01-01'], where: ['id' => 10]) +existsWhere(where: where(['id' => equalsTo(10)])) +deleteWhere(where: where(['id' => equalsTo(10)])) +updateWhere(values: ['name' => 'Arthur Dent', 'date_of_birth' => '1990-01-01'], where: where(['id' => equalsTo(10)])) ``` 📙 `EntityRecordRepository` extends RecordRepository with additional features for managing entity lifecycles: @@ -260,11 +260,6 @@ Contributions are welcome! If you have ideas, find a bug, or want to improve the Please follow PSR-12 coding standards and ensure tests pass before submitting changes. -## 🚀 Next steps - -- Query builder -- Extend where comparisons - ## 📄 License This project is open-sourced under the [MIT license](LICENSE). diff --git a/composer.json b/composer.json index f2efb17..0549bbe 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,10 @@ "autoload": { "psr-4": { "Tcds\\Io\\Orm\\": "src/" - } + }, + "files": [ + "src/Functions/query.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index 5a1b7cf..db7b34e 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/tcds-io/php-better-generics.git", - "reference": "7f5726fe78108ff44a883adb0713de138259efc1" + "reference": "786201e011cbe2d8dc7b0b5e1332e8308e3c8ced" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tcds-io/php-better-generics/zipball/7f5726fe78108ff44a883adb0713de138259efc1", - "reference": "7f5726fe78108ff44a883adb0713de138259efc1", + "url": "https://api.github.com/repos/tcds-io/php-better-generics/zipball/786201e011cbe2d8dc7b0b5e1332e8308e3c8ced", + "reference": "786201e011cbe2d8dc7b0b5e1332e8308e3c8ced", "shasum": "" }, "require": { @@ -54,7 +54,7 @@ "issues": "https://github.com/tcds-io/php-better-generics/issues", "source": "https://github.com/tcds-io/php-better-generics/tree/main" }, - "time": "2025-05-14T11:14:28+00:00" + "time": "2025-05-16T06:32:44+00:00" } ], "packages-dev": [ @@ -1174,16 +1174,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.14", + "version": "2.1.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2" + "reference": "402d11c1aa40ae2e1c3a512e6a4edb957527b20b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2", - "reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/402d11c1aa40ae2e1c3a512e6a4edb957527b20b", + "reference": "402d11c1aa40ae2e1c3a512e6a4edb957527b20b", "shasum": "" }, "require": { @@ -1228,7 +1228,7 @@ "type": "github" } ], - "time": "2025-05-02T15:32:28+00:00" + "time": "2025-05-14T11:16:08+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/src/Connection/Connection.php b/src/Connection/Connection.php index 76f683f..cedb523 100644 --- a/src/Connection/Connection.php +++ b/src/Connection/Connection.php @@ -8,7 +8,7 @@ interface Connection { - public function driver(): ConnectionDriver; + public function driver(): Driver; public function begin(): void; diff --git a/src/Connection/ConnectionDriver.php b/src/Connection/ConnectionDriver.php deleted file mode 100644 index e8748b3..0000000 --- a/src/Connection/ConnectionDriver.php +++ /dev/null @@ -1,11 +0,0 @@ - "`$column`", + Driver::GENERIC => "$column", + }; + } +} diff --git a/src/Connection/Pdo/MysqlConnection.php b/src/Connection/Pdo/MysqlConnection.php index 5eeebdb..57d5474 100644 --- a/src/Connection/Pdo/MysqlConnection.php +++ b/src/Connection/Pdo/MysqlConnection.php @@ -5,7 +5,7 @@ namespace Tcds\Io\Orm\Connection\Pdo; use PDO; -use Tcds\Io\Orm\Connection\ConnectionDriver; +use Tcds\Io\Orm\Connection\Driver; class MysqlConnection extends NestedTransactionConnection { @@ -17,8 +17,8 @@ public function __construct(PDO $read, PDO $write) parent::__construct($read, $write); } - public function driver(): ConnectionDriver + public function driver(): Driver { - return ConnectionDriver::MYSQL; + return Driver::MYSQL; } } diff --git a/src/EntityRecordRepository.php b/src/EntityRecordRepository.php index 7863eaa..793db61 100644 --- a/src/EntityRecordRepository.php +++ b/src/EntityRecordRepository.php @@ -5,6 +5,7 @@ namespace Tcds\Io\Orm; use Tcds\Io\Orm\Connection\Connection; +use Tcds\Io\Orm\Query\Query; /** * @template EntryType @@ -28,7 +29,7 @@ public function __construct( */ public function selectEntityById($id) { - return $this->selectOneWhere(['id' => $id]); + return $this->selectOneWhere(where(['id' => equalsTo($id)])); } /** @@ -38,7 +39,7 @@ public function updateOne($entity): void { $this->updateWhere( $this->mapper->plain($entity), - ['id' => $this->entityMapper->primaryKey->plain($entity)], + $this->equalsToPrimaryKey($entity), ); } @@ -57,7 +58,9 @@ public function updateMany(...$entities): void */ public function deleteOne($entity): void { - $this->deleteWhere(['id' => $this->entityMapper->primaryKey->plain($entity)]); + $this->deleteWhere( + $this->equalsToPrimaryKey($entity), + ); } /** @@ -69,4 +72,14 @@ public function deleteMany(...$entities): void $this->deleteOne($entity); } } + + /** + * @param EntryType $entity + */ + private function equalsToPrimaryKey($entity): Query + { + return where([ + 'id' => equalsTo($this->entityMapper->primaryKey->plain($entity)), + ]); + } } diff --git a/src/Functions/query.php b/src/Functions/query.php new file mode 100644 index 0000000..5c74fe3 --- /dev/null +++ b/src/Functions/query.php @@ -0,0 +1,92 @@ + $where + * @return Query + */ +function where(array $where): Query +{ + $query = null; + + foreach ($where as $column => $condition) { + $query === null + ? $query = Query::where($column, $condition) + : $query->and($column, $condition); + } + + return $query; +} + +function between(mixed $first, mixed $last): FilteringCondition +{ + return new FilteringCondition('BETWEEN ? AND ?', [$first, $last]); +} + +function differentOf(mixed $value): FilteringCondition +{ + return new FilteringCondition('!= ?', [$value]); +} + +function equalsTo(mixed $value): FilteringCondition +{ + return new FilteringCondition('= ?', [$value]); +} + +function greaterThan(mixed $value): FilteringCondition +{ + return new FilteringCondition('> ?', [$value]); +} + +function greaterThanOrEqualTo(mixed $value): FilteringCondition +{ + return new FilteringCondition('>= ?', [$value]); +} + +function in(array $values): FilteringCondition +{ + $marks = join(',', array_fill(0, count($values), '?')); + + return new FilteringCondition("IN ($marks)", $values); +} + +function isNotNull(): FilteringCondition +{ + return new FilteringCondition('IS NOT NULL', []); +} + +function isNull(): FilteringCondition +{ + return new FilteringCondition('IS NULL', []); +} + +function like(mixed $value): FilteringCondition +{ + return new FilteringCondition('LIKE ?', [$value]); +} + +function notIn(array $values): FilteringCondition +{ + $marks = join(',', array_fill(0, count($values), '?')); + + return new FilteringCondition("NOT IN ($marks)", $values); +} + +function notLike(mixed $value): FilteringCondition +{ + return new FilteringCondition('NOT LIKE ?', [$value]); +} + +function smallerThan(mixed $value): FilteringCondition +{ + return new FilteringCondition('< ?', [$value]); +} + +function smallerThanOrEqualTo(mixed $value): FilteringCondition +{ + return new FilteringCondition('<= ?', [$value]); +} diff --git a/src/Query/Conditions/Condition.php b/src/Query/Conditions/Condition.php new file mode 100644 index 0000000..9f7edd4 --- /dev/null +++ b/src/Query/Conditions/Condition.php @@ -0,0 +1,15 @@ +} + */ + public function build(Driver $driver): array; +} diff --git a/src/Query/Conditions/ConditionList.php b/src/Query/Conditions/ConditionList.php new file mode 100644 index 0000000..34174ab --- /dev/null +++ b/src/Query/Conditions/ConditionList.php @@ -0,0 +1,24 @@ + + */ +class ConditionList extends MutableArrayList +{ + public function __construct() + { + parent::__construct([]); + } + + public function add(Operator $operator, Condition $condition): void + { + $this->push([$operator, $condition]); + } +} diff --git a/src/Query/Conditions/FieldCondition.php b/src/Query/Conditions/FieldCondition.php new file mode 100644 index 0000000..32339c3 --- /dev/null +++ b/src/Query/Conditions/FieldCondition.php @@ -0,0 +1,24 @@ +wrap($this->column); + [$query, $params] = $this->condition->build($driver); + + return ["$column $query", $params]; + } +} diff --git a/src/Query/Conditions/FilteringCondition.php b/src/Query/Conditions/FilteringCondition.php new file mode 100644 index 0000000..23a97bc --- /dev/null +++ b/src/Query/Conditions/FilteringCondition.php @@ -0,0 +1,26 @@ + */ + protected array $params = [], + ) { + } + + #[Override] public function build(Driver $driver): array + { + return [ + $this->comparator, + $this->params, + ]; + } +} diff --git a/src/Query/Conditions/GroupedCondition.php b/src/Query/Conditions/GroupedCondition.php new file mode 100644 index 0000000..a4b4140 --- /dev/null +++ b/src/Query/Conditions/GroupedCondition.php @@ -0,0 +1,26 @@ +query->build($driver); + + return [ + "($query)", + $params, + ]; + } +} diff --git a/src/Query/Operator.php b/src/Query/Operator.php new file mode 100644 index 0000000..4bec3be --- /dev/null +++ b/src/Query/Operator.php @@ -0,0 +1,13 @@ +add(Operator::WHERE, new FieldCondition($field, $condition)); + + return new self($conditions); + } + + public static function empty(): self + { + $conditions = new ConditionList(); + + return new self($conditions); + } + + public function field(string $field, FilteringCondition $condition): self + { + $this->conditions->add(Operator::NONE, new FieldCondition($field, $condition)); + + return $this; + } + + public function and(string $field, FilteringCondition $condition): self + { + $this->conditions->add(Operator::AND, new FieldCondition($field, $condition)); + + return $this; + } + + public function or(string $field, FilteringCondition $condition): self + { + $this->conditions->add(Operator::OR, new FieldCondition($field, $condition)); + + return $this; + } + + /** + * @param callable(Query $query): Query $inner + * @return self + */ + public function andGrouped(callable $inner): self + { + $query = $inner(Query::empty()); + $this->conditions->add(Operator::AND, new GroupedCondition($query)); + + return $this; + } + + /** + * @param callable(Query $query): Query $inner + * @return self + */ + public function orGrouped(callable $inner): self + { + $query = $inner(Query::empty()); + $this->conditions->add(Operator::OR, new GroupedCondition($query)); + + return $this; + } + + /** + * @return array{ + * 0: string, + * 1: list + * } + */ + public function build(Driver $driver): array + { + $statements = []; + $bindings = []; + + foreach ($this->conditions->items() as $item) { + [$operator, $condition] = $item; + [$statement, $params] = $condition->build($driver); + + $statements[] = trim("$operator->value $statement"); + array_push($bindings, ...$params); + } + + return [ + join(' ', $statements), + $bindings, + ]; + } +} diff --git a/src/RecordRepository.php b/src/RecordRepository.php index 31e1b5e..e602ab8 100644 --- a/src/RecordRepository.php +++ b/src/RecordRepository.php @@ -7,6 +7,7 @@ use PDO; use Tcds\Io\Orm\Column\Column; use Tcds\Io\Orm\Connection\Connection; +use Tcds\Io\Orm\Query\Query; use Traversable; /** @@ -35,13 +36,12 @@ public function insertOne($entry): void } /** - * @param array $where * @return EntryType|null */ - public function selectOneWhere(array $where) + public function selectOneWhere(Query $where) { - [$whereColumnsString, $whereBindings] = $this->prepareWhere($where); - $sql = trim("SELECT * FROM $this->table$whereColumnsString LIMIT 1"); + [$whereColumnsString, $whereBindings] = $where->build($this->connection->driver()); + $sql = trim("SELECT * FROM {$this->wrap($this->table)} $whereColumnsString LIMIT 1"); $items = $this->connection->read($sql, $whereBindings); /** @var array $item */ @@ -65,14 +65,13 @@ public function selectOneByQuery(string $selectQuery, array $bindings) } /** - * @param array $where * @return Traversable */ - public function selectManyWhere(array $where = [], ?int $limit = null, ?int $offset = null): Traversable + public function selectManyWhere(?Query $where = null, ?int $limit = null, ?int $offset = null): Traversable { - [$whereColumnsString, $whereBindings] = $this->prepareWhere($where); + [$whereColumnsString, $whereBindings] = $where?->build($this->connection->driver()) ?? ['', []]; $limitOffset = $this->prepareLimitOffset($limit, $offset); - $sql = trim("SELECT * FROM $this->table$whereColumnsString$limitOffset"); + $sql = trim("SELECT * FROM {$this->wrap($this->table)} $whereColumnsString$limitOffset"); $items = $this->connection->read($sql, $whereBindings); while ($item = $items->fetch(PDO::FETCH_ASSOC)) { @@ -95,43 +94,36 @@ public function selectManyByQuery(string $selectQuery, array $bindings): Travers } } - /** - * @param array $where - */ - public function existsWhere(array $where): bool + public function existsWhere(Query $query): bool { - return $this->selectOneWhere($where) !== null; + return $this->selectOneWhere($query) !== null; } - /** - * @param array $where - */ - public function deleteWhere(array $where): void + public function deleteWhere(Query $where): void { - [$whereColumnsString, $whereBindings] = $this->prepareWhere($where); - $sql = trim("DELETE FROM $this->table$whereColumnsString"); + [$whereColumnsString, $whereBindings] = $where->build($this->connection->driver()); + $sql = trim("DELETE FROM {$this->wrap($this->table)} $whereColumnsString"); $this->connection->write($sql, $whereBindings); } /** * @param array $values - * @param array $where */ - public function updateWhere(array $values, array $where): void + public function updateWhere(array $values, Query $where): void { $columnBindings = []; $valuesBinding = []; foreach ($values as $column => $value) { - $columnBindings[] = "$column = :$column"; - $valuesBinding[$column] = $value; + $columnBindings[] = "{$this->wrap($column)} = ?"; + $valuesBinding[] = $value; } $columnBindingsString = join(", ", $columnBindings); - [$whereColumnsString, $whereBindings] = $this->prepareWhere($where); - $sql = trim("UPDATE $this->table SET $columnBindingsString$whereColumnsString"); + [$whereColumnsString, $whereBindings] = $where->build($this->connection->driver()); + $sql = trim("UPDATE {$this->wrap($this->table)} SET $columnBindingsString $whereColumnsString"); $this->connection->write($sql, array_merge($valuesBinding, $whereBindings)); } @@ -141,28 +133,13 @@ private function bindings(): string return join(', ', array_map(fn(Column $column) => ":$column->name", $this->mapper->columns)); } - /** - * @param array $where - * @return array{0: string, 1: array} - */ - private function prepareWhere(array $where): array + private function prepareLimitOffset(?int $limit, ?int $offset): string { - $whereColumns = []; - $whereBindings = []; - - foreach ($where as $column => $value) { - $whereColumns[] = "$column = :$column"; - $whereBindings[$column] = $value; - } - - return [ - empty($whereColumns) ? '' : sprintf(" WHERE %s", join(' AND ', $whereColumns)), - $whereBindings, - ]; + return ($limit ? " LIMIT $limit" : '') . ($offset ? " OFFSET $offset" : ''); } - private function prepareLimitOffset(?int $limit, ?int $offset): string + private function wrap(string $column): string { - return ($limit ? " LIMIT $limit" : '') . ($offset ? " OFFSET $offset" : ''); + return $this->connection->driver()->wrap($column); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 2cc0a73..c9bb8b7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,6 +7,9 @@ use PHPUnit\Framework\Constraint\Callback; use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher; use PHPUnit\Framework\TestCase as PhpUnitTestCase; +use Tcds\Io\Orm\Connection\Driver; +use Tcds\Io\Orm\Query\Conditions\FilteringCondition; +use Tcds\Io\Orm\Query\Query; class TestCase extends PhpUnitTestCase { @@ -25,4 +28,12 @@ public function consecutive(InvokedCountMatcher $matcher, array ...$expectedCall return true; }); } + + /** + * @param list $params + */ + protected function assertConditionQuery(FilteringCondition $condition, string $query, array $params): void + { + $this->assertEquals([$query, $params], Query::where($condition)->build(Driver::MYSQL)); + } } diff --git a/tests/Unit/Connection/Pdo/GenericConnectionTest.php b/tests/Unit/Connection/Pdo/GenericConnectionTest.php index 2cbe987..d712cc0 100644 --- a/tests/Unit/Connection/Pdo/GenericConnectionTest.php +++ b/tests/Unit/Connection/Pdo/GenericConnectionTest.php @@ -8,7 +8,7 @@ use PDO; use PDOStatement; use PHPUnit\Framework\MockObject\MockObject; -use Tcds\Io\Orm\Connection\ConnectionDriver; +use Tcds\Io\Orm\Connection\Driver; use Tcds\Io\Orm\Connection\Pdo\GenericConnection; use Test\Tcds\Io\Orm\TestCase; @@ -28,9 +28,9 @@ protected function setUp(): void $this->connection = new class ($this->read, $this->write) extends GenericConnection { - public function driver(): ConnectionDriver + public function driver(): Driver { - return ConnectionDriver::GENERIC; + return Driver::GENERIC; } }; } @@ -42,9 +42,9 @@ public function testGivenPdoThenConfigurePdo(): void new class ($this->read, $this->write) extends GenericConnection { - public function driver(): ConnectionDriver + public function driver(): Driver { - return ConnectionDriver::GENERIC; + return Driver::GENERIC; } }; } diff --git a/tests/Unit/Connection/Pdo/NestedTransactionConnectionTest.php b/tests/Unit/Connection/Pdo/NestedTransactionConnectionTest.php index 7a26653..6c5c282 100644 --- a/tests/Unit/Connection/Pdo/NestedTransactionConnectionTest.php +++ b/tests/Unit/Connection/Pdo/NestedTransactionConnectionTest.php @@ -7,7 +7,7 @@ use PDO; use PDOException; use PHPUnit\Framework\MockObject\MockObject; -use Tcds\Io\Orm\Connection\ConnectionDriver; +use Tcds\Io\Orm\Connection\Driver; use Tcds\Io\Orm\Connection\Pdo\NestedTransactionConnection; use Test\Tcds\Io\Orm\TestCase; @@ -24,9 +24,9 @@ protected function setUp(): void $this->connection = new class ($read, $this->write) extends NestedTransactionConnection { - public function driver(): ConnectionDriver + public function driver(): Driver { - return ConnectionDriver::GENERIC; + return Driver::GENERIC; } }; } diff --git a/tests/Unit/EntityRecordRepositoryTest.php b/tests/Unit/EntityRecordRepositoryTest.php index 5204ac4..3c6915b 100644 --- a/tests/Unit/EntityRecordRepositoryTest.php +++ b/tests/Unit/EntityRecordRepositoryTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use Tcds\Io\Orm\Connection\Connection; +use Tcds\Io\Orm\Connection\Driver; use Tcds\Io\Orm\EntityRecordRepository; use Test\Tcds\Io\Orm\Fixtures\AddressRepository; use Test\Tcds\Io\Orm\Fixtures\User; @@ -26,6 +27,10 @@ protected function setUp(): void $this->connection = $this->createMock(Connection::class); $this->addressRepository = $this->createMock(AddressRepository::class); + $this->connection + ->method('driver') + ->willReturn(Driver::MYSQL); + $this->repository = new UserRepository( $this->connection, new UserMapper($this->addressRepository), @@ -37,7 +42,7 @@ protected function setUp(): void $this->connection ->expects($this->once()) ->method('read') - ->with('SELECT * FROM users WHERE id = :id LIMIT 1', ['id' => 'galaxy-1']); + ->with('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', ['galaxy-1']); $this->repository->selectEntityById('galaxy-1'); } @@ -48,13 +53,8 @@ protected function setUp(): void ->expects($this->exactly(1)) ->method('write') ->with( - 'UPDATE users SET id = :id, name = :name, date_of_birth = :date_of_birth, address_id = :address_id WHERE id = :id', - [ - 'id' => 1, - 'name' => 'First User', - 'date_of_birth' => '2020-01-01', - 'address_id' => 1, - ], + 'UPDATE `users` SET `id` = ?, `name` = ?, `date_of_birth` = ?, `address_id` = ? WHERE `id` = ?', + [1, 'First User', '2020-01-01', 1, 1], ); $this->repository->updateOne(User::first()); @@ -70,21 +70,23 @@ protected function setUp(): void ->with($this->consecutive( $matcher, [ - 'UPDATE users SET id = :id, name = :name, date_of_birth = :date_of_birth, address_id = :address_id WHERE id = :id', + 'UPDATE `users` SET `id` = ?, `name` = ?, `date_of_birth` = ?, `address_id` = ? WHERE `id` = ?', [ - 'id' => 1, - 'name' => 'First User', - 'date_of_birth' => '2020-01-01', - 'address_id' => 1, + 1, + 'First User', + '2020-01-01', + 1, + 1, ], ], [ - 'UPDATE users SET id = :id, name = :name, date_of_birth = :date_of_birth, address_id = :address_id WHERE id = :id', + 'UPDATE `users` SET `id` = ?, `name` = ?, `date_of_birth` = ?, `address_id` = ? WHERE `id` = ?', [ - 'id' => 2, - 'name' => 'Second User', - 'date_of_birth' => '2022-10-15', - 'address_id' => 2, + 2, + 'Second User', + '2022-10-15', + 2, + 2, ], ], )); @@ -101,8 +103,8 @@ protected function setUp(): void ->expects($this->exactly(1)) ->method('write') ->with( - 'DELETE FROM users WHERE id = :id', - ['id' => 1], + 'DELETE FROM `users` WHERE `id` = ?', + [1], ); $this->repository->deleteOne(User::first()); @@ -118,12 +120,12 @@ protected function setUp(): void ->with($this->consecutive( $matcher, [ - 'DELETE FROM users WHERE id = :id', - ['id' => 1], + 'DELETE FROM `users` WHERE `id` = ?', + [1], ], [ - 'DELETE FROM users WHERE id = :id', - ['id' => 2], + 'DELETE FROM `users` WHERE `id` = ?', + [2], ], )); diff --git a/tests/Unit/Functions/BetweenTest.php b/tests/Unit/Functions/BetweenTest.php new file mode 100644 index 0000000..e80dd42 --- /dev/null +++ b/tests/Unit/Functions/BetweenTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('BETWEEN ? AND ?', $query); + $this->assertEquals([18, 55], $params); + } +} diff --git a/tests/Unit/Functions/DifferentOfTest.php b/tests/Unit/Functions/DifferentOfTest.php new file mode 100644 index 0000000..e15fa77 --- /dev/null +++ b/tests/Unit/Functions/DifferentOfTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('!= ?', $query); + $this->assertEquals(['Arthur'], $params); + } +} diff --git a/tests/Unit/Functions/EqualsToTest.php b/tests/Unit/Functions/EqualsToTest.php new file mode 100644 index 0000000..0c8e7d6 --- /dev/null +++ b/tests/Unit/Functions/EqualsToTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('= ?', $query); + $this->assertEquals(['Amsterdam'], $params); + } +} diff --git a/tests/Unit/Functions/GreaterThanOrEqualToTest.php b/tests/Unit/Functions/GreaterThanOrEqualToTest.php new file mode 100644 index 0000000..852c3df --- /dev/null +++ b/tests/Unit/Functions/GreaterThanOrEqualToTest.php @@ -0,0 +1,23 @@ +build(Driver::MYSQL); + + $this->assertEquals('>= ?', $query); + $this->assertEquals([55], $params); + } +} diff --git a/tests/Unit/Functions/GreaterThanTest.php b/tests/Unit/Functions/GreaterThanTest.php new file mode 100644 index 0000000..c244a94 --- /dev/null +++ b/tests/Unit/Functions/GreaterThanTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('> ?', $query); + $this->assertEquals([55], $params); + } +} diff --git a/tests/Unit/Functions/InTest.php b/tests/Unit/Functions/InTest.php new file mode 100644 index 0000000..edf8c43 --- /dev/null +++ b/tests/Unit/Functions/InTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('IN (?,?,?)', $query); + $this->assertEquals([10, 20, 30], $params); + } +} diff --git a/tests/Unit/Functions/IsNotNullTest.php b/tests/Unit/Functions/IsNotNullTest.php new file mode 100644 index 0000000..80f5d86 --- /dev/null +++ b/tests/Unit/Functions/IsNotNullTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('IS NOT NULL', $query); + $this->assertEquals([], $params); + } +} diff --git a/tests/Unit/Functions/IsNullTest.php b/tests/Unit/Functions/IsNullTest.php new file mode 100644 index 0000000..370a2e8 --- /dev/null +++ b/tests/Unit/Functions/IsNullTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('IS NULL', $query); + $this->assertEquals([], $params); + } +} diff --git a/tests/Unit/Functions/LikeTest.php b/tests/Unit/Functions/LikeTest.php new file mode 100644 index 0000000..766f93d --- /dev/null +++ b/tests/Unit/Functions/LikeTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('LIKE ?', $query); + $this->assertEquals(['Berlin'], $params); + } +} diff --git a/tests/Unit/Functions/NotInTest.php b/tests/Unit/Functions/NotInTest.php new file mode 100644 index 0000000..fc1072d --- /dev/null +++ b/tests/Unit/Functions/NotInTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('NOT IN (?,?,?)', $query); + $this->assertEquals([10, 20, 30], $params); + } +} diff --git a/tests/Unit/Functions/NotLikeTest.php b/tests/Unit/Functions/NotLikeTest.php new file mode 100644 index 0000000..52bbb26 --- /dev/null +++ b/tests/Unit/Functions/NotLikeTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('NOT LIKE ?', $query); + $this->assertEquals(['Berlin'], $params); + } +} diff --git a/tests/Unit/Functions/SmallerThanOrEqualToTest.php b/tests/Unit/Functions/SmallerThanOrEqualToTest.php new file mode 100644 index 0000000..3c74593 --- /dev/null +++ b/tests/Unit/Functions/SmallerThanOrEqualToTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('<= ?', $query); + $this->assertEquals([15], $params); + } +} diff --git a/tests/Unit/Functions/SmallerThanTest.php b/tests/Unit/Functions/SmallerThanTest.php new file mode 100644 index 0000000..c494f20 --- /dev/null +++ b/tests/Unit/Functions/SmallerThanTest.php @@ -0,0 +1,22 @@ +build(Driver::MYSQL); + + $this->assertEquals('< ?', $query); + $this->assertEquals([15], $params); + } +} diff --git a/tests/Unit/Functions/WhereTest.php b/tests/Unit/Functions/WhereTest.php new file mode 100644 index 0000000..cfaefa5 --- /dev/null +++ b/tests/Unit/Functions/WhereTest.php @@ -0,0 +1,33 @@ + equalsTo('Arthur'), + 'last_name' => like('Dent%'), + 'year' => between(1900, 2018), + ]); + + [$query, $params] = $where->build(Driver::MYSQL); + + $this->assertEquals( + Query::where('name', equalsTo('Arthur')) + ->and('last_name', like('Dent%')) + ->and('year', between(1900, 2018)), + $where, + ); + $this->assertEquals('WHERE `name` = ? AND `last_name` LIKE ? AND `year` BETWEEN ? AND ?', $query); + $this->assertEquals(['Arthur', 'Dent%', 1900, 2018], $params); + } +} diff --git a/tests/Unit/Query/Conditions/FieldConditionListTest.php b/tests/Unit/Query/Conditions/FieldConditionListTest.php new file mode 100644 index 0000000..be7eb63 --- /dev/null +++ b/tests/Unit/Query/Conditions/FieldConditionListTest.php @@ -0,0 +1,37 @@ +add(Operator::AND, equalsTo(22)); + + $this->assertEquals([ + [Operator::AND, equalsTo(22)], + ], $conditions->items()); + } + + #[Test] public function when_condition_is_not_empty_then_add_with_given_operator(): void + { + $conditions = new ConditionList(); + + $conditions->add(Operator::WHERE, equalsTo(22)); + $conditions->add(Operator::AND, equalsTo(33)); + + $this->assertEquals([ + [Operator::WHERE, equalsTo(22)], + [Operator::AND, equalsTo(33)], + ], $conditions->items()); + } +} diff --git a/tests/Unit/Query/Conditions/FieldConditionTest.php b/tests/Unit/Query/Conditions/FieldConditionTest.php new file mode 100644 index 0000000..33b0420 --- /dev/null +++ b/tests/Unit/Query/Conditions/FieldConditionTest.php @@ -0,0 +1,23 @@ +build(Driver::MYSQL); + + $this->assertEquals('`name` = ?', $query); + $this->assertEquals(['Arthur'], $params); + } +} diff --git a/tests/Unit/Query/QueryTest.php b/tests/Unit/Query/QueryTest.php new file mode 100644 index 0000000..6a01c7b --- /dev/null +++ b/tests/Unit/Query/QueryTest.php @@ -0,0 +1,47 @@ +or('last_name', like('Dent%')) + ->and('age', greaterThan(18)); + + $this->assertEquals( + [ + 'WHERE `name` LIKE ? OR `last_name` LIKE ? AND `age` > ?', + ['Arthur%', 'Dent%', 18], + ], + $query->build(Driver::MYSQL), + ); + } + + #[Test] public function build_query_with_and_group(): void + { + $query = Query::where('active', equalsTo(true)) + ->andGrouped(fn(Query $query) => $query + ->field('city', equalsTo('Berlin')) + ->and('country', equalsTo('Germany'))) + ->orGrouped(fn(Query $query) => $query + ->field('city', equalsTo('Amsterdam')) + ->and('country', equalsTo('Netherlands'))); + + $this->assertEquals( + [ + 'WHERE `active` = ? AND (`city` = ? AND `country` = ?) OR (`city` = ? AND `country` = ?)', + [true, 'Berlin', 'Germany', 'Amsterdam', 'Netherlands'], + ], + $query->build(Driver::MYSQL), + ); + } +} diff --git a/tests/Unit/RecordRepositoryTest.php b/tests/Unit/RecordRepositoryTest.php index 7ca35b9..4c2a6e4 100644 --- a/tests/Unit/RecordRepositoryTest.php +++ b/tests/Unit/RecordRepositoryTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use Tcds\Io\Orm\Connection\Connection; +use Tcds\Io\Orm\Connection\Driver; use Tcds\Io\Orm\RecordRepository; use Test\Tcds\Io\Orm\Fixtures\Address; use Test\Tcds\Io\Orm\Fixtures\AddressMapper; @@ -24,6 +25,10 @@ protected function setUp(): void $this->connection = $this->createMock(Connection::class); $this->statement = $this->createMock(PDOStatement::class); + $this->connection + ->method('driver') + ->willReturn(Driver::MYSQL); + $this->manager = new class (new AddressMapper(), $this->connection, 'addresses') extends RecordRepository { }; @@ -62,12 +67,12 @@ protected function setUp(): void ->expects($this->once()) ->method('read') ->with( - "SELECT * FROM addresses WHERE id = :id LIMIT 1", - ['id' => 'address-xxx'], + "SELECT * FROM `addresses` WHERE `id` = ? LIMIT 1", + ['address-xxx'], ) ->willReturn($this->statement); - $result = $this->manager->selectOneWhere(['id' => 'address-xxx']); + $result = $this->manager->selectOneWhere(where(['id' => equalsTo('address-xxx')])); $this->assertEquals(Address::second(), $result); } @@ -102,13 +107,16 @@ protected function setUp(): void ->expects($this->once()) ->method('read') ->with( - 'SELECT * FROM addresses WHERE id = :id AND street = :street LIMIT 5 OFFSET 15', - ['id' => 'address-xxx', 'street' => "Galaxy Avenue"], + 'SELECT * FROM `addresses` WHERE `id` = ? AND `street` = ? LIMIT 5 OFFSET 15', + ['address-xxx', "Galaxy Avenue"], ) ->willReturn($this->statement); $result = $this->manager->selectManyWhere( - ['id' => 'address-xxx', 'street' => 'Galaxy Avenue'], + where([ + 'id' => equalsTo('address-xxx'), + 'street' => equalsTo('Galaxy Avenue'), + ]), limit: 5, offset: 15, ); @@ -184,10 +192,10 @@ public function list_by_returns_multiple_entries(): void $this->connection ->expects($this->once()) ->method('read') - ->with("SELECT * FROM addresses WHERE id = :id LIMIT 1", ['id' => 'address-xxx']) + ->with("SELECT * FROM `addresses` WHERE `id` = ? LIMIT 1", ['address-xxx']) ->willReturn($this->statement); - $exists = $this->manager->existsWhere(['id' => 'address-xxx']); + $exists = $this->manager->existsWhere(where(['id' => equalsTo('address-xxx')])); $this->assertTrue($exists); } @@ -200,10 +208,10 @@ public function list_by_returns_multiple_entries(): void $this->connection ->expects($this->once()) ->method('read') - ->with("SELECT * FROM addresses WHERE id = :id LIMIT 1", ['id' => 'address-xxx']) + ->with("SELECT * FROM `addresses` WHERE `id` = ? LIMIT 1", ['address-xxx']) ->willReturn($this->statement); - $exists = $this->manager->existsWhere(['id' => 'address-xxx']); + $exists = $this->manager->existsWhere(where(['id' => equalsTo('address-xxx')])); $this->assertFalse($exists); } @@ -214,11 +222,11 @@ public function list_by_returns_multiple_entries(): void ->expects($this->once()) ->method('write') ->with( - "DELETE FROM addresses WHERE id = :id", - ['id' => 'address-xxx'], + "DELETE FROM `addresses` WHERE `id` = ?", + ['address-xxx'], ); - $this->manager->deleteWhere(['id' => 'address-xxx']); + $this->manager->deleteWhere(where(['id' => equalsTo('address-xxx')])); } #[Test] public function update(): void @@ -227,10 +235,10 @@ public function list_by_returns_multiple_entries(): void ->expects($this->once()) ->method('write') ->with( - "UPDATE addresses SET street = :street WHERE id = :id", - ['street' => 'Galaxy Avenue', 'id' => 'address-xxx'], + "UPDATE `addresses` SET `street` = ? WHERE `id` = ?", + ['Galaxy Avenue', 'address-xxx'], ); - $this->manager->updateWhere(['street' => 'Galaxy Avenue'], ['id' => 'address-xxx']); + $this->manager->updateWhere(['street' => 'Galaxy Avenue'], where(['id' => equalsTo('address-xxx')])); } }