-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add Doctrine DBAL QueryBuilder support #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
romankonz
wants to merge
5
commits into
4.0.x
Choose a base branch
from
feat/doctrine-query-builder
base: 4.0.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
0f22b77
feat: Add Doctrine DBAL QueryBuilder support via Connection::createQu…
romankonz 889386e
docs: Add complex AND/OR/IN example to query_builder_sqlite demo
romankonz c23ae86
docs: Add JOIN example and reviews table to query_builder_sqlite demo
romankonz 5b271f6
fix: Resolve PHPStan errors in Doctrine adapter
romankonz 4b99ecd
fix: Add doctrine/dbal as a runtime dependency and fix CI failures
romankonz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * This file is part of the Artemeon Core - Web Application Framework. | ||
| * | ||
| * (c) Artemeon <www.artemeon.de> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * Runnable demo of Connection::createQueryBuilder() backed by an in-memory SQLite database. | ||
| * | ||
| * Usage (from the repo root): | ||
| * | ||
| * php examples/query_builder_sqlite.php | ||
| * | ||
| * Requires `doctrine/dbal:^4` to be installed (already in require-dev). | ||
| */ | ||
|
|
||
| use Artemeon\Database\Connection; | ||
| use Artemeon\Database\ConnectionParameters; | ||
| use Artemeon\Database\DriverFactory; | ||
| use Artemeon\Database\Schema\DataType; | ||
|
|
||
| require __DIR__ . '/../vendor/autoload.php'; | ||
|
|
||
| $params = new ConnectionParameters( | ||
| host: 'localhost', | ||
| username: '', | ||
| password: '', | ||
| database: ':memory:', | ||
| port: null, | ||
| driver: 'sqlite3', | ||
| ); | ||
|
|
||
| $connection = new Connection($params, new DriverFactory()); | ||
|
|
||
| // --- Schema + seed ----------------------------------------------------------- | ||
|
|
||
| $table = 'demo_books'; | ||
| $tableReviews = 'demo_reviews'; | ||
|
|
||
| $connection->dropTable($table); | ||
| $connection->dropTable($tableReviews); | ||
|
|
||
| $connection->createTable( | ||
| $table, | ||
| [ | ||
| 'id' => [DataType::CHAR20, false], | ||
| 'title' => [DataType::CHAR100, false], | ||
| 'author' => [DataType::CHAR100, false], | ||
| 'year' => [DataType::INT, true], | ||
| ], | ||
| ['id'], | ||
| ); | ||
|
|
||
| $connection->createTable( | ||
| $tableReviews, | ||
| [ | ||
| 'id' => [DataType::CHAR20, false], | ||
| 'book_id' => [DataType::CHAR20, false], | ||
| 'stars' => [DataType::INT, true], | ||
| 'comment' => [DataType::CHAR100, true], | ||
| ], | ||
| ['id'], | ||
| ); | ||
|
|
||
| $books = [ | ||
| ['id' => 'b-1', 'title' => 'The Pragmatic Programmer', 'author' => 'Andy Hunt', 'year' => 1999], | ||
| ['id' => 'b-2', 'title' => 'Refactoring', 'author' => 'Martin Fowler', 'year' => 1999], | ||
| ['id' => 'b-3', 'title' => 'Domain-Driven Design', 'author' => 'Eric Evans', 'year' => 2003], | ||
| ['id' => 'b-4', 'title' => 'Clean Code', 'author' => 'Robert Martin', 'year' => 2008], | ||
| ['id' => 'b-5', 'title' => 'Designing Data-Intensive Applications', 'author' => 'Martin Kleppmann', 'year' => 2017], | ||
| ]; | ||
| foreach ($books as $book) { | ||
| $connection->insert($table, $book); | ||
| } | ||
|
|
||
| $reviews = [ | ||
| ['id' => 'r-1', 'book_id' => 'b-1', 'stars' => 5, 'comment' => 'Essential read'], | ||
| ['id' => 'r-2', 'book_id' => 'b-1', 'stars' => 4, 'comment' => 'Very practical'], | ||
| ['id' => 'r-3', 'book_id' => 'b-3', 'stars' => 5, 'comment' => 'Changed how I think'], | ||
| ['id' => 'r-4', 'book_id' => 'b-4', 'stars' => 3, 'comment' => 'Good but dated'], | ||
| ['id' => 'r-5', 'book_id' => 'b-5', 'stars' => 5, 'comment' => 'Comprehensive'], | ||
| ]; | ||
| foreach ($reviews as $review) { | ||
| $connection->insert($tableReviews, $review); | ||
| } | ||
|
|
||
| // --- Build queries via the Doctrine DBAL QueryBuilder ------------------------ | ||
|
|
||
| echo "1) SELECT with WHERE + ORDER BY (positional parameter)\n"; | ||
| echo "------------------------------------------------------\n"; | ||
| $rows = $connection->createQueryBuilder() | ||
| ->select('title', 'author', 'year') | ||
| ->from($table) | ||
| ->where('year >= ?') | ||
| ->orderBy('year', 'ASC') | ||
| ->setParameter(0, 2000) | ||
| ->executeQuery() | ||
| ->fetchAllAssociative(); | ||
|
|
||
| foreach ($rows as $row) { | ||
| printf(" %d %-45s %s\n", $row['year'], $row['title'], $row['author']); | ||
| } | ||
|
|
||
| echo "\n2) SELECT with named parameter (DBAL rewrites to positional)\n"; | ||
| echo "------------------------------------------------------------\n"; | ||
| $row = $connection->createQueryBuilder() | ||
| ->select('title', 'author') | ||
| ->from($table) | ||
| ->where('id = :id') | ||
| ->setParameter('id', 'b-3') | ||
| ->executeQuery() | ||
| ->fetchAssociative(); | ||
|
|
||
| printf(" Book b-3 → %s by %s\n", $row['title'], $row['author']); | ||
|
|
||
| echo "\n3) UPDATE returning the affected row count\n"; | ||
| echo "-----------------------------------------\n"; | ||
| $affected = $connection->createQueryBuilder() | ||
| ->update($table) | ||
| ->set('author', '?') | ||
| ->where('id = ?') | ||
| ->setParameter(0, 'Andy Hunt and Dave Thomas') | ||
| ->setParameter(1, 'b-1') | ||
| ->executeStatement(); | ||
|
|
||
| printf(" Updated %d row(s).\n", $affected); | ||
|
|
||
| $confirmed = $connection->fetchOne( | ||
| 'SELECT author FROM ' . $table . ' WHERE id = ?', | ||
| ['b-1'], | ||
| ); | ||
| printf(" New author for b-1: %s\n", $confirmed); | ||
|
|
||
| echo "\n4) Aggregate via QueryBuilder\n"; | ||
| echo "-----------------------------\n"; | ||
| $count = $connection->createQueryBuilder() | ||
| ->select('COUNT(*)') | ||
| ->from($table) | ||
| ->executeQuery() | ||
| ->fetchOne(); | ||
|
|
||
| printf(" Total books in table: %d\n", $count); | ||
|
|
||
| echo "\n5) Complex WHERE with AND, OR, and IN\n"; | ||
| echo "---------------------------------------\n"; | ||
| // Books published before 2000 OR after 2010, but only from a specific id set | ||
| $qb = $connection->createQueryBuilder(); | ||
| $rows = $qb | ||
| ->select('title', 'author', 'year') | ||
| ->from($table) | ||
| ->where( | ||
| $qb->expr()->and( | ||
| $qb->expr()->or( | ||
| $qb->expr()->lt('year', ':cutoff_low'), | ||
| $qb->expr()->gt('year', ':cutoff_high'), | ||
| ), | ||
| $qb->expr()->in('id', [':b1', ':b2', ':b3']), | ||
| ), | ||
| ) | ||
| ->setParameter('cutoff_low', 2000) | ||
| ->setParameter('cutoff_high', 2010) | ||
| ->setParameter('b1', 'b-1') | ||
| ->setParameter('b2', 'b-3') | ||
| ->setParameter('b3', 'b-5') | ||
| ->orderBy('year', 'ASC') | ||
| ->executeQuery() | ||
| ->fetchAllAssociative(); | ||
|
|
||
| foreach ($rows as $row) { | ||
| printf(" %d %-45s %s\n", $row['year'], $row['title'], $row['author']); | ||
| } | ||
|
|
||
| echo "\n6) JOIN books with reviews, filter by minimum rating\n"; | ||
| echo "------------------------------------------------------\n"; | ||
| $qb = $connection->createQueryBuilder(); | ||
| $rows = $qb | ||
| ->select('b.title', 'b.author', 'r.stars', 'r.comment') | ||
| ->from($table, 'b') | ||
| ->innerJoin('b', $tableReviews, 'r', 'r.book_id = b.id') | ||
| ->where($qb->expr()->gte('r.stars', ':min_stars')) | ||
| ->setParameter('min_stars', 5) | ||
| ->orderBy('b.year', 'ASC') | ||
| ->executeQuery() | ||
| ->fetchAllAssociative(); | ||
|
|
||
| foreach ($rows as $row) { | ||
| printf(" %-45s %d★ %s\n", $row['title'], $row['stars'], $row['comment']); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * This file is part of the Artemeon Core - Web Application Framework. | ||
| * | ||
| * (c) Artemeon <www.artemeon.de> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Artemeon\Database\Doctrine; | ||
|
|
||
| use Artemeon\Database\Connection as ArtemeonConnection; | ||
| use Doctrine\DBAL\Driver\Connection as DriverConnection; | ||
| use Doctrine\DBAL\Driver\Exception\NoIdentityValue; | ||
| use Doctrine\DBAL\Driver\Result as DriverResult; | ||
| use Doctrine\DBAL\Driver\Statement as DriverStatement; | ||
|
|
||
| /** | ||
| * DBAL driver connection that delegates to an Artemeon Connection. | ||
| * | ||
| * Platform selection happens in the {@see Driver} subclasses; this class only | ||
| * wires DBAL's per-query primitives to the matching Artemeon Connection calls. | ||
| * The server-version string is supplied by the Driver and used by DBAL solely | ||
| * to pick a platform variant (MySQL 8.4 vs 8.0, MariaDB 11.7 vs 10.6, etc.). | ||
| */ | ||
| final class Connection implements DriverConnection | ||
| { | ||
| public function __construct( | ||
| private readonly ArtemeonConnection $connection, | ||
| private readonly string $serverVersion, | ||
| ) { | ||
| } | ||
|
|
||
| public function prepare(string $sql): DriverStatement | ||
| { | ||
| return new Statement($this->connection, $sql); | ||
| } | ||
|
|
||
| public function query(string $sql): DriverResult | ||
| { | ||
| return $this->prepare($sql)->execute(); | ||
| } | ||
|
|
||
| public function quote(string $value): string | ||
| { | ||
| return "'" . str_replace(['\\', "'"], ['\\\\', "''"], $value) . "'"; | ||
| } | ||
|
|
||
| public function exec(string $sql): int | ||
| { | ||
| return $this->connection->executeStatement($sql); | ||
| } | ||
|
|
||
| public function lastInsertId(): int | string | ||
| { | ||
| throw NoIdentityValue::new(); | ||
| } | ||
|
|
||
| public function beginTransaction(): void | ||
| { | ||
| $this->connection->beginTransaction(); | ||
| } | ||
|
|
||
| public function commit(): void | ||
| { | ||
| $this->connection->commit(); | ||
| } | ||
|
|
||
| public function rollBack(): void | ||
| { | ||
| $this->connection->rollBack(); | ||
| } | ||
|
|
||
| public function getNativeConnection(): object | ||
| { | ||
| return $this->connection; | ||
| } | ||
|
|
||
| public function getServerVersion(): string | ||
| { | ||
| return $this->serverVersion; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * This file is part of the Artemeon Core - Web Application Framework. | ||
| * | ||
| * (c) Artemeon <www.artemeon.de> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Artemeon\Database\Doctrine\Driver; | ||
|
|
||
| use Artemeon\Database\Connection as ArtemeonConnection; | ||
| use Artemeon\Database\Doctrine\Connection as DriverConnectionAdapter; | ||
| use Doctrine\DBAL\Driver\AbstractMySQLDriver; | ||
| use Doctrine\DBAL\Driver\Connection as DriverConnection; | ||
| use SensitiveParameter; | ||
|
|
||
| /** | ||
| * DBAL driver façade for an Artemeon Mysqli driver. | ||
| */ | ||
| final class MysqlDriver extends AbstractMySQLDriver | ||
| { | ||
| public function __construct(private readonly ArtemeonConnection $connection) | ||
| { | ||
| } | ||
|
|
||
| public function connect(#[SensitiveParameter] array $params): DriverConnection | ||
| { | ||
| $version = $this->connection->fetchOne('SELECT VERSION()'); | ||
|
|
||
| return new DriverConnectionAdapter($this->connection, is_string($version) ? $version : '8.0.0'); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(?)