Skip to content

Commit d30088a

Browse files
committed
Add support for native MySQL driver (mysqli)
1 parent ba7466c commit d30088a

File tree

9 files changed

+133
-47
lines changed

9 files changed

+133
-47
lines changed

.github/workflows/test-unit.yml

+22-4
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,41 @@ jobs:
163163
php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
164164
if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-sqlite.cov; fi
165165
166-
- name: "Run tests: MySQL"
166+
- name: "Run tests: MySQL - PDO"
167167
env:
168168
DB_DSN: "mysql:host=mysql;dbname=atk4_test"
169169
DB_USER: atk4_test_user
170170
DB_PASSWORD: atk4_pass
171171
run: |
172172
php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
173-
if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mysql.cov; fi
173+
if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mysql-pdo.cov; fi
174174
175-
- name: "Run tests: MariaDB"
175+
- name: "Run tests: MySQL - mysqli"
176+
env:
177+
DB_DSN: "mysqli:host=mysql;dbname=atk4_test"
178+
DB_USER: atk4_test_user
179+
DB_PASSWORD: atk4_pass
180+
run: |
181+
php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
182+
if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mysql-mysqli.cov; fi
183+
184+
- name: "Run tests: MariaDB - PDO"
176185
env:
177186
DB_DSN: "mysql:host=mariadb;dbname=atk4_test"
178187
DB_USER: atk4_test_user
179188
DB_PASSWORD: atk4_pass
180189
run: |
181190
php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
182-
if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mariadb.cov; fi
191+
if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mariadb-pdo.cov; fi
192+
193+
- name: "Run tests: MariaDB - mysqli"
194+
env:
195+
DB_DSN: "mysqli:host=mariadb;dbname=atk4_test"
196+
DB_USER: atk4_test_user
197+
DB_PASSWORD: atk4_pass
198+
run: |
199+
php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
200+
if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-mariadb-mysqli.cov; fi
183201
184202
- name: "Run tests: PostgreSQL"
185203
env:

src/Persistence.php

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public static function connect($dsn, string $user = null, string $password = nul
4747
switch ($dsn['driver']) {
4848
case 'pdo_sqlite':
4949
case 'pdo_mysql':
50+
case 'mysqli':
5051
case 'pdo_pgsql':
5152
case 'pdo_sqlsrv':
5253
case 'pdo_oci':

src/Persistence/Sql/Connection.php

+32-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\Common\EventManager;
99
use Doctrine\DBAL\Connection as DbalConnection;
1010
use Doctrine\DBAL\Driver\Connection as DbalDriverConnection;
11+
use Doctrine\DBAL\Driver\Mysqli\Connection as DbalMysqliConnection;
1112
use Doctrine\DBAL\DriverManager;
1213
use Doctrine\DBAL\Platforms\AbstractPlatform;
1314
use Doctrine\DBAL\Platforms\OraclePlatform;
@@ -35,6 +36,7 @@ abstract class Connection
3536
protected static $connectionClassRegistry = [
3637
'pdo_sqlite' => Sqlite\Connection::class,
3738
'pdo_mysql' => Mysql\Connection::class,
39+
'mysqli' => Mysql\Connection::class,
3840
'pdo_pgsql' => Postgresql\Connection::class,
3941
'pdo_oci' => Oracle\Connection::class,
4042
'pdo_sqlsrv' => Mssql\Connection::class,
@@ -129,7 +131,7 @@ public static function normalizeDsn($dsn, $user = null, $password = null)
129131
$dsn['password'] = $password;
130132
}
131133

132-
if (!str_starts_with($dsn['driver'], 'pdo_')) {
134+
if (!str_starts_with($dsn['driver'], 'pdo_') && !in_array($dsn['driver'], ['mysqli'], true)) {
133135
$dsn['driver'] = 'pdo_' . $dsn['driver'];
134136
}
135137

@@ -197,14 +199,35 @@ final public static function isComposerDbal2x(): bool
197199
return !class_exists(DbalResult::class);
198200
}
199201

200-
private static function getDriverNameFromDbalDriverConnection(DbalDriverConnection $connection): string
202+
private static function getDriverFromDbalDriverConnection(DbalDriverConnection $connection): object
201203
{
202-
while (self::isComposerDbal2x() ? $connection instanceof \PDO : $connection = $connection->getWrappedConnection()) {
203-
if ($connection instanceof \PDO) {
204-
return 'pdo_' . $connection->getAttribute(\PDO::ATTR_DRIVER_NAME);
204+
if (self::isComposerDbal2x()) {
205+
if ($connection instanceof \PDO || $connection instanceof \mysqli) {
206+
return $connection;
205207
}
206208
}
207209

210+
$wrappedConnection = $connection instanceof DbalMysqliConnection
211+
? $connection->getWrappedResourceHandle()
212+
: $connection->getWrappedConnection(); // @phpstan-ignore-line
213+
214+
if ($wrappedConnection instanceof \PDO || $wrappedConnection instanceof \mysqli) {
215+
return $wrappedConnection;
216+
}
217+
218+
return self::getDriverFromDbalDriverConnection($wrappedConnection);
219+
}
220+
221+
private static function getDriverNameFromDbalDriverConnection(DbalDriverConnection $connection): string
222+
{
223+
$driver = self::getDriverFromDbalDriverConnection($connection);
224+
225+
if ($driver instanceof \PDO) {
226+
return 'pdo_' . $driver->getAttribute(\PDO::ATTR_DRIVER_NAME);
227+
} elseif ($driver instanceof \mysqli) {
228+
return 'mysqli';
229+
}
230+
208231
return null; // @phpstan-ignore-line
209232
}
210233

@@ -216,7 +239,7 @@ protected static function createDbalEventManager(): EventManager
216239
protected static function connectFromDsn(array $dsn): DbalDriverConnection
217240
{
218241
$dsn = static::normalizeDsn($dsn);
219-
if ($dsn['driver'] === 'pdo_mysql') {
242+
if ($dsn['driver'] === 'pdo_mysql' || $dsn['driver'] === 'mysqli') {
220243
$dsn['charset'] = 'utf8mb4';
221244
} elseif ($dsn['driver'] === 'pdo_oci') {
222245
$dsn['charset'] = 'AL32UTF8';
@@ -413,7 +436,9 @@ public function rollBack(): void
413436
*/
414437
public function lastInsertId(string $sequence = null): string
415438
{
416-
return $this->connection()->lastInsertId($sequence);
439+
$res = $this->connection()->lastInsertId($sequence);
440+
441+
return is_int($res) ? (string) $res : $res;
417442
}
418443

419444
public function getDatabasePlatform(): AbstractPlatform

src/Persistence/Sql/Expression.php

+42-5
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,41 @@ public function __debugInfo(): array
493493
return $arr;
494494
}
495495

496+
protected function hasNativeNamedParamSupport(): bool
497+
{
498+
return true;
499+
}
500+
501+
/**
502+
* @param array{string, array<string, mixed>} $render
503+
*
504+
* @return array{string, array<string, mixed>}
505+
*
506+
* @internal
507+
*/
508+
protected function updateRenderBeforeExecute(array $render): array
509+
{
510+
[$sql, $params] = $render = $render;
511+
512+
if (!$this->hasNativeNamedParamSupport()) {
513+
$numParams = [];
514+
$i = 0;
515+
$j = 0;
516+
$sql = preg_replace_callback(
517+
'~(?:\'(?:\'\'|\\\\\'|[^\'])*\')?+\K(?:\?|:\w+)~s',
518+
function ($matches) use ($params, &$numParams, &$i, &$j) {
519+
$numParams[++$i] = $params[$matches[0] === '?' ? ++$j : $matches[0]];
520+
521+
return '?';
522+
},
523+
$sql
524+
);
525+
$params = $numParams;
526+
}
527+
528+
return [$sql, $params];
529+
}
530+
496531
/**
497532
* @param DbalConnection|Connection $connection
498533
*
@@ -506,7 +541,7 @@ public function execute(object $connection = null): object
506541

507542
// If it's a DBAL connection, we're cool
508543
if ($connection instanceof DbalConnection) {
509-
[$query, $params] = $this->render();
544+
[$query, $params] = $this->updateRenderBeforeExecute($this->render());
510545

511546
$platform = $this->connection->getDatabasePlatform();
512547
try {
@@ -575,11 +610,13 @@ public function execute(object $connection = null): object
575610
}
576611
$errorInfo = $firstException instanceof \PDOException ? $firstException->errorInfo : null;
577612

578-
$new = (new ExecuteException('Dsql execute error', $errorInfo[1] ?? 0, $e))
579-
->addMoreInfo('error', $errorInfo[2] ?? 'n/a (' . $errorInfo[0] . ')')
580-
->addMoreInfo('query', $this->getDebugQuery());
613+
$eNew = (new ExecuteException('Dsql execute error', $errorInfo[1] ?? $e->getCode(), $e));
614+
if ($errorInfo !== null && $errorInfo !== []) {
615+
$eNew->addMoreInfo('error', $errorInfo[2] ?? 'n/a (' . $errorInfo[0] . ')');
616+
}
617+
$eNew->addMoreInfo('query', $this->getDebugQuery());
581618

582-
throw $new;
619+
throw $eNew;
583620
}
584621
}
585622

src/Persistence/Sql/Mssql/ExpressionTrait.php

+5-31
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,11 @@ public function render(): array
3030
return $matches[1] . (!in_array($matches[1], ['N', '\'', '\\'], true) ? 'N' : '') . $matches[2];
3131
}, $sql);
3232

33-
// MSSQL does not support named parameters, so convert them to numerical when called from execute
34-
$trace = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 10);
35-
$calledFromExecute = false;
36-
foreach ($trace as $frame) {
37-
if (($frame['object'] ?? null) === $this) {
38-
if (($frame['function'] ?? null) === 'render') {
39-
continue;
40-
} elseif (($frame['function'] ?? null) === 'execute') {
41-
$calledFromExecute = true;
42-
}
43-
}
44-
45-
break;
46-
}
47-
48-
if ($calledFromExecute) {
49-
$numParams = [];
50-
$i = 0;
51-
$j = 0;
52-
$sql = preg_replace_callback(
53-
'~(?:\'(?:\'\'|\\\\\'|[^\'])*\')?+\K(?:\?|:\w+)~s',
54-
function ($matches) use ($params, &$numParams, &$i, &$j) {
55-
$numParams[++$i] = $params[$matches[0] === '?' ? ++$j : $matches[0]];
56-
57-
return '?';
58-
},
59-
$sql
60-
);
61-
$params = $numParams;
62-
}
63-
6433
return [$sql, $params];
6534
}
35+
36+
protected function hasNativeNamedParamSupport(): bool
37+
{
38+
return false;
39+
}
6640
}

src/Persistence/Sql/Mysql/Expression.php

+2
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88

99
class Expression extends BaseExpression
1010
{
11+
use ExpressionTrait;
12+
1113
protected $escape_char = '`';
1214
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Atk4\Data\Persistence\Sql\Mysql;
6+
7+
use Atk4\Data\Persistence\Sql\Connection as SqlConnection;
8+
9+
trait ExpressionTrait
10+
{
11+
protected function hasNativeNamedParamSupport(): bool
12+
{
13+
// TODO use Connection::getNativeConnection() once only DBAL 3.3+ is supported
14+
// https://github.com/doctrine/dbal/pull/5037
15+
$dbalConnection = $this->connection->connection();
16+
$nativeConnection = \Closure::bind(function () use ($dbalConnection) {
17+
return SqlConnection::getDriverFromDbalDriverConnection($dbalConnection->getWrappedConnection());
18+
}, null, SqlConnection::class)();
19+
20+
return !$nativeConnection instanceof \mysqli;
21+
}
22+
}

src/Persistence/Sql/Mysql/Query.php

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
class Query extends BaseQuery
1010
{
11+
use ExpressionTrait;
12+
1113
protected $escape_char = '`';
1214

1315
protected $expression_class = Expression::class;

tests/Persistence/Sql/WithDb/SelectTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,11 @@ public function testExecuteException(): void
249249
} catch (ExecuteException $e) {
250250
if ($this->getDatabasePlatform() instanceof MySQLPlatform) {
251251
$expectedErrorCode = 1146; // SQLSTATE[42S02]: Base table or view not found: 1146 Table 'non_existing_table' doesn't exist
252+
253+
$dummyExpr = $this->c->expr();
254+
if (Connection::isComposerDbal2x() && !\Closure::bind(fn () => $dummyExpr->hasNativeNamedParamSupport(), null, \Atk4\Data\Persistence\Sql\Expression::class)()) {
255+
$this->markTestIncomplete('DBAL 2.x with mysqli driver does not set exception code');
256+
}
252257
} elseif ($this->getDatabasePlatform() instanceof PostgreSQLPlatform) {
253258
$expectedErrorCode = 7; // SQLSTATE[42P01]: Undefined table: 7 ERROR: relation "non_existing_table" does not exist
254259
} elseif ($this->getDatabasePlatform() instanceof SQLServerPlatform) {

0 commit comments

Comments
 (0)