1
0
mirror of https://github.com/dg/dibi.git synced 2025-09-01 02:01:48 +02:00

Compare commits

..

28 Commits

Author SHA1 Message Date
David Grudl
86a71dde28 Released version 5.0.1 2023-11-25 14:08:47 +01:00
David Grudl
bb1f7d4b93 SqliteDriver: disables exceptions (is enabled since PHP 8.3) 2023-11-25 14:08:47 +01:00
David Grudl
680026747e added DibiExtension3 2023-11-25 14:08:47 +01:00
Jan Rössler
7ca47508cb PostgreReflector: detect IDENTITY columns as autoincrement 2023-11-05 20:38:26 +01:00
Jan Rössler
beba7b3592 PostgreReflector: fix autoincrement column detection 2023-11-05 20:38:26 +01:00
David Grudl
6cc7ce8e44 used PhpStorm Language attribute 2023-11-05 20:38:25 +01:00
Petr Hubík
92b8e6077e fix: PDO::errorInfo() can return NULL as a code, but Exception does not accept NULL code 2023-09-29 15:55:05 +02:00
David Grudl
e45638eab4 cs 2023-09-29 15:55:05 +02:00
Marek Bartoš
8564217bc1 Fluent: execute() has conditional return type 2023-09-05 12:40:51 +02:00
David Grudl
82c45c3076 Released version 5.0.0 2023-08-09 16:38:02 +02:00
Miloslav Hůla
df45bd3553 added object translators (#420)
The Translator is now capable to translate objects into Expression via object translators registered by the Connection.
2023-08-09 16:35:02 +02:00
Miloslav Hůla
fe22e230ce tests: remove dependency on dibi_test database name 2023-08-09 16:35:02 +02:00
David Grudl
8257532630 used native PHP 8 functions 2023-08-09 16:35:02 +02:00
David Grudl
08dfc37492 added PHP 8 typehints 2023-08-09 16:33:28 +02:00
David Grudl
7acef0c34b added property typehints 2023-08-09 16:33:28 +02:00
David Grudl
b01d97ac86 removed Dibi\Strict 2023-08-09 16:33:28 +02:00
David Grudl
8915b0343c removed support for PHP 7 2023-08-09 16:33:28 +02:00
David Grudl
d1a3362321 coding style 2023-08-09 16:33:28 +02:00
David Grudl
b6ead80202 composer: updated dependencies 2023-08-09 16:15:31 +02:00
David Grudl
a640ac2a8f requires PHP 8.0 2023-08-09 16:15:31 +02:00
David Grudl
87e702d1fc opened 5.0-dev 2023-08-09 16:15:31 +02:00
David Grudl
cb0cf4ba2f Released version 4.2.8 2023-08-09 16:15:07 +02:00
David Grudl
8e7df8374b drivers: removed auto-free feature 2023-08-09 16:15:07 +02:00
Marek Bartoš
848ac76fed Fluent: improved phpDoc 2023-08-09 16:15:07 +02:00
David Grudl
a0f2ca2fca typo 2023-08-09 16:15:04 +02:00
David Grudl
cf14987b42 cs 2023-08-05 19:56:38 +02:00
David Grudl
01c7ab63e3 tested in PHP 8.3 2023-08-05 19:50:57 +02:00
David Grudl
520119740d updated .gitattributes 2022-12-21 02:06:10 +01:00
71 changed files with 530 additions and 792 deletions

4
.gitattributes vendored
View File

@@ -1,8 +1,8 @@
.gitattributes export-ignore
.gitignore export-ignore
.github export-ignore
.travis.yml export-ignore
ecs.php export-ignore
appveyor.yml export-ignore
ncs.* export-ignore
phpstan.neon export-ignore
tests/ export-ignore

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.0', '8.1', '8.2']
php: ['8.0', '8.1', '8.2', '8.3']
fail-fast: false

View File

@@ -11,13 +11,14 @@
}
],
"require": {
"php": ">=8.0 <8.3"
"php": "8.0 - 8.3"
},
"require-dev": {
"tracy/tracy": "^2.8",
"nette/tester": "^2.4",
"nette/di": "^3.0",
"phpstan/phpstan": "^0.12"
"tracy/tracy": "^2.9",
"nette/tester": "^2.5",
"nette/di": "^3.1",
"phpstan/phpstan": "^1.0",
"jetbrains/phpstorm-attributes": "^1.0"
},
"replace": {
"dg/dibi": "*"

View File

@@ -34,7 +34,7 @@ Install Dibi via Composer:
composer require dibi/dibi
```
The Dibi 5.0 requires PHP version 8.0 and supports PHP up to 8.2.
The Dibi 5.0 requires PHP version 8.0 and supports PHP up to 8.3.
Usage
@@ -341,7 +341,7 @@ $database->query('INSERT INTO users', [
There are three methods for dealing with transactions:
```php
$database->beginTransaction();
$database->begin();
$database->commit();
@@ -639,7 +639,7 @@ In the configuration file, we will register the DI extensions and add the `dibi`
```neon
extensions:
dibi: Dibi\Bridges\Nette\DibiExtension22
dibi: Dibi\Bridges\Nette\DibiExtension3
dibi:
host: localhost

View File

@@ -20,7 +20,6 @@ use Tracy;
class DibiExtension22 extends Nette\DI\CompilerExtension
{
private ?bool $debugMode;
private ?bool $cliMode;

View File

@@ -0,0 +1,96 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Bridges\Nette;
use Dibi;
use Nette;
use Nette\Schema\Expect;
use Tracy;
/**
* Dibi extension for Nette Framework 3. Creates 'connection' & 'panel' services.
*/
class DibiExtension3 extends Nette\DI\CompilerExtension
{
private ?bool $debugMode;
private ?bool $cliMode;
public function __construct(?bool $debugMode = null, ?bool $cliMode = null)
{
$this->debugMode = $debugMode;
$this->cliMode = $cliMode;
}
public function getConfigSchema(): Nette\Schema\Schema
{
return Expect::structure([
'autowired' => Expect::bool(true),
'flags' => Expect::anyOf(Expect::arrayOf('string'), Expect::type('dynamic')),
'profiler' => Expect::bool(),
'explain' => Expect::bool(true),
'filter' => Expect::bool(true),
'driver' => Expect::string()->dynamic(),
'name' => Expect::string()->dynamic(),
'lazy' => Expect::bool(false)->dynamic(),
'onConnect' => Expect::array()->dynamic(),
'substitutes' => Expect::arrayOf('string')->dynamic(),
'result' => Expect::structure([
'normalize' => Expect::bool(true),
'formatDateTime' => Expect::string(),
'formatTimeInterval' => Expect::string(),
'formatJson' => Expect::string(),
]),
])->otherItems(Expect::type('mixed'))
->castTo('array');
}
public function loadConfiguration()
{
$container = $this->getContainerBuilder();
$config = $this->getConfig();
$this->debugMode ??= $container->parameters['debugMode'];
$this->cliMode ??= $container->parameters['consoleMode'];
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode && !$this->cliMode);
unset($config['profiler']);
if (is_array($config['flags'])) {
$flags = 0;
foreach ((array) $config['flags'] as $flag) {
$flags |= constant($flag);
}
$config['flags'] = $flags;
}
$connection = $container->addDefinition($this->prefix('connection'))
->setCreator(Dibi\Connection::class, [$config])
->setAutowired($config['autowired']);
if (class_exists(Tracy\Debugger::class)) {
$connection->addSetup(
[new Nette\DI\Definitions\Statement('Tracy\Debugger::getBlueScreen'), 'addPanel'],
[[Dibi\Bridges\Tracy\Panel::class, 'renderException']],
);
}
if ($useProfiler) {
$panel = $container->addDefinition($this->prefix('panel'))
->setCreator(Dibi\Bridges\Tracy\Panel::class, [
$config['explain'],
$config['filter'] ? Dibi\Event::QUERY : Dibi\Event::ALL,
]);
$connection->addSetup([$panel, 'register'], [$connection]);
}
}
}

View File

@@ -1,8 +1,8 @@
# This will create service named 'dibi.connection'.
# Requires Nette Framework 2.2 or later
# Requires Nette Framework 3 or later
extensions:
dibi: Dibi\Bridges\Nette\DibiExtension22
dibi: Dibi\Bridges\Nette\DibiExtension3
dibi:
host: localhost

View File

@@ -20,15 +20,9 @@ use Tracy;
*/
class Panel implements Tracy\IBarPanel
{
use Dibi\Strict;
/** maximum SQL length */
public static int $maxLength = 1000;
public bool|string $explain;
public int $filter;
private array $events = [];
@@ -68,7 +62,7 @@ class Panel implements Tracy\IBarPanel
if ($e instanceof Dibi\Exception && $e->getSql()) {
return [
'tab' => 'SQL',
'panel' => Helpers::dump($e->getSql(), true),
'panel' => Helpers::dump($e->getSql(), return: true),
];
}
@@ -124,7 +118,7 @@ class Panel implements Tracy\IBarPanel
? $this->explain
: ($connection->getConfig('driver') === 'oracle' ? 'EXPLAIN PLAN FOR' : 'EXPLAIN');
try {
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), true);
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), return: true);
} catch (Dibi\Exception $e) {
}
@@ -138,7 +132,7 @@ class Panel implements Tracy\IBarPanel
$s .= "<br /><a href='#tracy-debug-DibiProfiler-row-$counter' class='tracy-toggle tracy-collapsed' rel='#tracy-debug-DibiProfiler-row-$counter'>explain</a>";
}
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, true);
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, return: true);
if ($explain) {
$s .= "<div id='tracy-debug-DibiProfiler-row-$counter' class='tracy-collapsed'>{$explain}</div>";
}
@@ -171,7 +165,7 @@ class Panel implements Tracy\IBarPanel
private function getConnectionName(Dibi\Connection $connection): string
{
$driver = $connection->getConfig('driver');
return (is_object($driver) ? $driver::class : $driver)
return get_debug_type($driver)
. ($connection->getConfig('name') ? '/' . $connection->getConfig('name') : '')
. ($connection->getConfig('host') ? "\u{202f}@\u{202f}" . $connection->getConfig('host') : '');
}

View File

@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace Dibi;
use JetBrains\PhpStorm\Language;
use Traversable;
@@ -20,26 +21,19 @@ use Traversable;
*/
class Connection implements IConnection
{
use Strict;
/** function (Event $event); Occurs after query is executed */
public ?array $onEvent = [];
/** Current connection configuration */
private array $config;
/** @var string[] resultset formats */
private array $formats;
private ?Driver $driver = null;
private ?Translator $translator = null;
/** @var array<string, callable(object): Expression> */
/** @var array<string, callable(object): Expression | null> */
private array $translators = [];
private bool $sortTranslators = false;
private HashMap $substitutes;
private int $transactionDepth = 0;
@@ -216,7 +210,7 @@ class Connection implements IConnection
* Generates (translates) and executes SQL query.
* @throws Exception
*/
final public function query(mixed ...$args): Result
final public function query(#[Language('GenericSQL')] mixed ...$args): Result
{
return $this->nativeQuery($this->translate(...$args));
}
@@ -226,7 +220,7 @@ class Connection implements IConnection
* Generates SQL query.
* @throws Exception
*/
final public function translate(mixed ...$args): string
final public function translate(#[Language('GenericSQL')] mixed ...$args): string
{
if (!$this->driver) {
$this->connect();
@@ -239,7 +233,7 @@ class Connection implements IConnection
/**
* Generates and prints SQL query.
*/
final public function test(mixed ...$args): bool
final public function test(#[Language('GenericSQL')] mixed ...$args): bool
{
try {
Helpers::dump($this->translate(...$args));
@@ -261,7 +255,7 @@ class Connection implements IConnection
* Generates (translates) and returns SQL query as DataSource.
* @throws Exception
*/
final public function dataSource(mixed ...$args): DataSource
final public function dataSource(#[Language('GenericSQL')] mixed ...$args): DataSource
{
return new DataSource($this->translate(...$args), $this);
}
@@ -271,7 +265,7 @@ class Connection implements IConnection
* Executes the SQL query.
* @throws Exception
*/
final public function nativeQuery(string $sql): Result
final public function nativeQuery(#[Language('SQL')] string $sql): Result
{
if (!$this->driver) {
$this->connect();
@@ -528,19 +522,68 @@ class Connection implements IConnection
/********************* value objects translation ****************d*g**/
/** @param callable(object): Expression $translator */
public function addObjectTranslator(string $class, callable $translator): self
/**
* @param callable(object): Expression $translator
*/
public function setObjectTranslator(callable $translator): void
{
$this->translators[$class] = $translator;
uksort($this->translators, fn($a, $b) => class_exists($a, false) && is_subclass_of($a, $b) ? -1 : 1);
return $this;
if (!$translator instanceof \Closure) {
$translator = \Closure::fromCallable($translator);
}
$param = (new \ReflectionFunction($translator))->getParameters()[0] ?? null;
$type = $param?->getType();
$types = match (true) {
$type instanceof \ReflectionNamedType => [$type],
$type instanceof \ReflectionUnionType => $type->getTypes(),
default => throw new Exception('Object translator must have exactly one parameter with class typehint.'),
};
foreach ($types as $type) {
if ($type->isBuiltin() || $type->allowsNull()) {
throw new Exception("Object translator must have exactly one parameter with non-nullable class typehint, got '$type'.");
}
$this->translators[$type->getName()] = $translator;
}
$this->sortTranslators = true;
}
/** @return array<string, callable(object): Expression> */
public function getObjectTranslators(): array
public function translateObject(object $object): ?Expression
{
return $this->translators;
if ($this->sortTranslators) {
$this->translators = array_filter($this->translators);
uksort($this->translators, fn($a, $b) => is_subclass_of($a, $b) ? -1 : 1);
$this->sortTranslators = false;
}
if (!array_key_exists($object::class, $this->translators)) {
$translator = null;
foreach ($this->translators as $class => $t) {
if ($object instanceof $class) {
$translator = $t;
break;
}
}
$this->translators[$object::class] = $translator;
}
$translator = $this->translators[$object::class];
if ($translator === null) {
return null;
}
$result = $translator($object);
if (!$result instanceof Expression) {
throw new Exception(sprintf(
"Object translator for class '%s' returned '%s' but %s expected.",
$object::class,
get_debug_type($result),
Expression::class,
));
}
return $result;
}
@@ -551,7 +594,7 @@ class Connection implements IConnection
* Executes SQL query and fetch result - shortcut for query() & fetch().
* @throws Exception
*/
public function fetch(mixed ...$args): ?Row
public function fetch(#[Language('GenericSQL')] mixed ...$args): ?Row
{
return $this->query($args)->fetch();
}
@@ -562,7 +605,7 @@ class Connection implements IConnection
* @return Row[]|array[]
* @throws Exception
*/
public function fetchAll(mixed ...$args): array
public function fetchAll(#[Language('GenericSQL')] mixed ...$args): array
{
return $this->query($args)->fetchAll();
}
@@ -572,7 +615,7 @@ class Connection implements IConnection
* Executes SQL query and fetch first column - shortcut for query() & fetchSingle().
* @throws Exception
*/
public function fetchSingle(mixed ...$args): mixed
public function fetchSingle(#[Language('GenericSQL')] mixed ...$args): mixed
{
return $this->query($args)->fetchSingle();
}
@@ -582,7 +625,7 @@ class Connection implements IConnection
* Executes SQL query and fetch pairs - shortcut for query() & fetchPairs().
* @throws Exception
*/
public function fetchPairs(mixed ...$args): array
public function fetchPairs(#[Language('GenericSQL')] mixed ...$args): array
{
return $this->query($args)->fetchPairs();
}

View File

@@ -15,26 +15,15 @@ namespace Dibi;
*/
class DataSource implements IDataSource
{
use Strict;
private Connection $connection;
private string $sql;
private ?Result $result = null;
private ?int $count = null;
private ?int $totalCount = null;
private array $cols = [];
private array $sorting = [];
private array $conds = [];
private ?int $offset = null;
private ?int $limit = null;

View File

@@ -15,8 +15,6 @@ namespace Dibi;
*/
class DateTime extends \DateTimeImmutable
{
use Strict;
public function __construct(string|int $time = 'now', ?\DateTimeZone $timezone = null)
{
$timezone = $timezone ?: new \DateTimeZone(date_default_timezone_get());

View File

@@ -17,8 +17,6 @@ use Dibi;
*/
class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
{
use Dibi\Strict;
public function disconnect(): void
{
}

View File

@@ -26,8 +26,6 @@ use Dibi\Helpers;
*/
class FirebirdDriver implements Dibi\Driver
{
use Dibi\Strict;
public const ERROR_EXCEPTION_THROWN = -836;
/** @var resource */
@@ -35,7 +33,6 @@ class FirebirdDriver implements Dibi\Driver
/** @var ?resource */
private $transaction;
private bool $inTransaction = false;

View File

@@ -17,8 +17,6 @@ use Dibi;
*/
class FirebirdReflector implements Dibi\Reflector
{
use Dibi\Strict;
private Dibi\Driver $driver;

View File

@@ -18,13 +18,9 @@ use Dibi\Helpers;
*/
class FirebirdResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
private $resultSet;
private bool $autoFree = true;
/**
* @param resource $resultSet
@@ -35,17 +31,6 @@ class FirebirdResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -104,7 +89,6 @@ class FirebirdResult implements Dibi\ResultDriver
*/
public function getResultResource(): mixed
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}

View File

@@ -18,8 +18,6 @@ use Dibi;
*/
class MySqlReflector implements Dibi\Reflector
{
use Dibi\Strict;
private Dibi\Driver $driver;

View File

@@ -32,17 +32,11 @@ use Dibi;
*/
class MySqliDriver implements Dibi\Driver
{
use Dibi\Strict;
public const ERROR_ACCESS_DENIED = 1045;
public const ERROR_DUPLICATE_ENTRY = 1062;
public const ERROR_DATA_TRUNCATED = 1265;
private \mysqli $connection;
/** Is buffered (seekable and countable)? */
private bool $buffered = false;

View File

@@ -17,13 +17,7 @@ use Dibi;
*/
class MySqliResult implements Dibi\ResultDriver
{
use Dibi\Strict;
private \mysqli_result $resultSet;
private bool $autoFree = true;
/** Is buffered (seekable and countable)? */
private bool $buffered;
@@ -34,17 +28,6 @@ class MySqliResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
@$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -134,7 +117,6 @@ class MySqliResult implements Dibi\ResultDriver
*/
public function getResultResource(): \mysqli_result
{
$this->autoFree = false;
return $this->resultSet;
}

View File

@@ -17,8 +17,6 @@ use Dibi;
*/
class NoDataResult implements Dibi\ResultDriver
{
use Dibi\Strict;
private int $rows;

View File

@@ -25,13 +25,9 @@ use Dibi;
*/
class OdbcDriver implements Dibi\Driver
{
use Dibi\Strict;
/** @var resource */
private $connection;
private ?int $affectedRows;
private bool $microseconds = true;
@@ -123,7 +119,7 @@ class OdbcDriver implements Dibi\Driver
*/
public function begin(?string $savepoint = null): void
{
if (!odbc_autocommit($this->connection, false)) {
if (!odbc_autocommit($this->connection)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
}

View File

@@ -17,8 +17,6 @@ use Dibi;
*/
class OdbcReflector implements Dibi\Reflector
{
use Dibi\Strict;
private Dibi\Driver $driver;

View File

@@ -17,13 +17,8 @@ use Dibi;
*/
class OdbcResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
private $resultSet;
private bool $autoFree = true;
private int $row = 0;
@@ -36,17 +31,6 @@ class OdbcResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -127,7 +111,6 @@ class OdbcResult implements Dibi\ResultDriver
*/
public function getResultResource(): mixed
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}

View File

@@ -27,16 +27,10 @@ use Dibi;
*/
class OracleDriver implements Dibi\Driver
{
use Dibi\Strict;
/** @var resource */
private $connection;
private bool $autocommit = true;
/** use native datetime format */
private bool $nativeDate;
private ?int $affectedRows;

View File

@@ -17,8 +17,6 @@ use Dibi;
*/
class OracleReflector implements Dibi\Reflector
{
use Dibi\Strict;
private Dibi\Driver $driver;

View File

@@ -17,13 +17,9 @@ use Dibi;
*/
class OracleResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
private $resultSet;
private bool $autoFree = true;
/**
* @param resource $resultSet
@@ -34,17 +30,6 @@ class OracleResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -110,7 +95,6 @@ class OracleResult implements Dibi\ResultDriver
*/
public function getResultResource(): mixed
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}

View File

@@ -27,14 +27,9 @@ use PDO;
*/
class PdoDriver implements Dibi\Driver
{
use Dibi\Strict;
private ?PDO $connection;
private ?int $affectedRows;
private string $driverName;
private string $serverVersion = '';
@@ -98,23 +93,15 @@ class PdoDriver implements Dibi\Driver
$this->affectedRows = null;
[$sqlState, $code, $message] = $this->connection->errorInfo();
$code ??= 0;
$message = "SQLSTATE[$sqlState]: $message";
switch ($this->driverName) {
case 'mysql':
throw MySqliDriver::createException($message, $code, $sql);
case 'oci':
throw OracleDriver::createException($message, $code, $sql);
case 'pgsql':
throw PostgreDriver::createException($message, $sqlState, $sql);
case 'sqlite':
throw SqliteDriver::createException($message, $code, $sql);
default:
throw new Dibi\DriverException($message, $code, $sql);
}
throw match ($this->driverName) {
'mysql' => MySqliDriver::createException($message, $code, $sql),
'oci' => OracleDriver::createException($message, $code, $sql),
'pgsql' => PostgreDriver::createException($message, $sqlState, $sql),
'sqlite' => SqliteDriver::createException($message, $code, $sql),
default => new Dibi\DriverException($message, $code, $sql),
};
}
@@ -144,7 +131,7 @@ class PdoDriver implements Dibi\Driver
{
if (!$this->connection->beginTransaction()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
}
}
@@ -157,7 +144,7 @@ class PdoDriver implements Dibi\Driver
{
if (!$this->connection->commit()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
}
}
@@ -170,7 +157,7 @@ class PdoDriver implements Dibi\Driver
{
if (!$this->connection->rollBack()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
}
}

View File

@@ -19,10 +19,7 @@ use PDO;
*/
class PdoResult implements Dibi\ResultDriver
{
use Dibi\Strict;
private ?\PDOStatement $resultSet;
private string $driverName;

View File

@@ -28,11 +28,8 @@ use PgSql;
*/
class PostgreDriver implements Dibi\Driver
{
use Dibi\Strict;
/** @var resource|PgSql\Connection */
private $connection;
private ?int $affectedRows;

View File

@@ -17,10 +17,7 @@ use Dibi;
*/
class PostgreReflector implements Dibi\Reflector
{
use Dibi\Strict;
private Dibi\Driver $driver;
private string $version;
@@ -75,13 +72,6 @@ class PostgreReflector implements Dibi\Reflector
public function getColumns(string $table): array
{
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
$res = $this->driver->query("
SELECT indkey
FROM pg_class
LEFT JOIN pg_index on pg_class.oid = pg_index.indrelid AND pg_index.indisprimary
WHERE pg_class.oid = $_table::regclass
");
$primary = (int) $res->fetch(true)['indkey'];
$res = $this->driver->query("
SELECT *
@@ -101,7 +91,8 @@ class PostgreReflector implements Dibi\Reflector
a.atttypmod-4 AS character_maximum_length,
NOT a.attnotnull AS is_nullable,
a.attnum AS ordinal_position,
pg_get_expr(adef.adbin, adef.adrelid) AS column_default
pg_get_expr(adef.adbin, adef.adrelid) AS column_default,
CASE WHEN a.attidentity IN ('a', 'd') THEN 'YES' ELSE 'NO' END AS is_identity
FROM
pg_attribute a
JOIN pg_type ON a.atttypid = pg_type.oid
@@ -126,7 +117,7 @@ class PostgreReflector implements Dibi\Reflector
'size' => $size > 0 ? $size : null,
'nullable' => $row['is_nullable'] === 'YES' || $row['is_nullable'] === 't' || $row['is_nullable'] === true,
'default' => $row['column_default'],
'autoincrement' => (int) $row['ordinal_position'] === $primary && str_starts_with($row['column_default'] ?? '', 'nextval'),
'autoincrement' => $row['is_identity'] === 'YES' || str_starts_with($row['column_default'] ?? '', 'nextval('),
'vendor' => $row,
];
}

View File

@@ -19,13 +19,9 @@ use PgSql;
*/
class PostgreResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource|PgSql\Result */
private $resultSet;
private bool $autoFree = true;
/**
* @param resource|PgSql\Result $resultSet
@@ -36,17 +32,6 @@ class PostgreResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -113,7 +98,6 @@ class PostgreResult implements Dibi\ResultDriver
*/
public function getResultResource(): mixed
{
$this->autoFree = false;
return is_resource($this->resultSet) || $this->resultSet instanceof PgSql\Result
? $this->resultSet
: null;

View File

@@ -25,12 +25,8 @@ use SQLite3;
*/
class SqliteDriver implements Dibi\Driver
{
use Dibi\Strict;
private SQLite3 $connection;
private string $fmtDate;
private string $fmtDateTime;
@@ -61,6 +57,7 @@ class SqliteDriver implements Dibi\Driver
// enable foreign keys support (defaultly disabled; if disabled then foreign key constraints are not enforced)
$version = SQLite3::version();
$this->connection->enableExceptions(false);
if ($version['versionNumber'] >= '3006019') {
$this->query('PRAGMA foreign_keys = ON');
}

View File

@@ -17,8 +17,6 @@ use Dibi;
*/
class SqliteReflector implements Dibi\Reflector
{
use Dibi\Strict;
private Dibi\Driver $driver;

View File

@@ -18,12 +18,8 @@ use Dibi\Helpers;
*/
class SqliteResult implements Dibi\ResultDriver
{
use Dibi\Strict;
private \SQLite3Result $resultSet;
private bool $autoFree = true;
public function __construct(\SQLite3Result $resultSet)
{
@@ -31,17 +27,6 @@ class SqliteResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
@$this->free();
}
}
/**
* Returns the number of rows in a result set.
* @throws Dibi\NotSupportedException
@@ -107,7 +92,6 @@ class SqliteResult implements Dibi\ResultDriver
*/
public function getResultResource(): \SQLite3Result
{
$this->autoFree = false;
return $this->resultSet;
}

View File

@@ -27,13 +27,9 @@ use Dibi\Helpers;
*/
class SqlsrvDriver implements Dibi\Driver
{
use Dibi\Strict;
/** @var resource */
private $connection;
private ?int $affectedRows;
private string $version = '';

View File

@@ -17,8 +17,6 @@ use Dibi;
*/
class SqlsrvReflector implements Dibi\Reflector
{
use Dibi\Strict;
private Dibi\Driver $driver;

View File

@@ -17,13 +17,9 @@ use Dibi;
*/
class SqlsrvResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
private $resultSet;
private bool $autoFree = true;
/**
* @param resource $resultSet
@@ -34,17 +30,6 @@ class SqlsrvResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -106,7 +91,6 @@ class SqlsrvResult implements Dibi\ResultDriver
*/
public function getResultResource(): mixed
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}

View File

@@ -15,8 +15,6 @@ namespace Dibi;
*/
class Event
{
use Strict;
/** event type */
public const
CONNECT = 1,
@@ -32,17 +30,11 @@ class Event
ALL = 1023;
public Connection $connection;
public int $type;
public string $sql;
public Result|DriverException|null $result;
public float $time;
public ?int $count = null;
public ?array $source = null;

View File

@@ -15,8 +15,6 @@ namespace Dibi;
*/
class Expression
{
use Strict;
private array $values;

View File

@@ -27,6 +27,8 @@ namespace Dibi;
* @method Fluent innerJoin(...$table)
* @method Fluent rightJoin(...$table)
* @method Fluent outerJoin(...$table)
* @method Fluent union(Fluent $fluent)
* @method Fluent unionAll(Fluent $fluent)
* @method Fluent as(...$field)
* @method Fluent on(...$cond)
* @method Fluent and(...$cond)
@@ -43,8 +45,6 @@ namespace Dibi;
*/
class Fluent implements IDataSource
{
use Strict;
public const REMOVE = false;
public static array $masks = [
@@ -92,15 +92,10 @@ class Fluent implements IDataSource
];
private Connection $connection;
private array $setups = [];
private ?string $command = null;
private array $clauses = [];
private array $flags = [];
private $cursor;
/** normalized clauses */
@@ -284,19 +279,17 @@ class Fluent implements IDataSource
/**
* Generates and executes SQL query.
* Returns result set or number of affected rows
* @return ($return is \dibi::IDENTIFIER|\dibi::AFFECTED_ROWS ? int : Result)
* @throws Exception
*/
public function execute(?string $return = null): Result|int|null
{
$res = $this->query($this->_export());
switch ($return) {
case \dibi::IDENTIFIER:
return $this->connection->getInsertId();
case \dibi::AFFECTED_ROWS:
return $this->connection->getAffectedRows();
default:
return $res;
}
return match ($return) {
\dibi::IDENTIFIER => $this->connection->getInsertId(),
\dibi::AFFECTED_ROWS => $this->connection->getAffectedRows(),
default => $res,
};
}

View File

@@ -46,7 +46,7 @@ abstract class HashMapBase
*/
final class HashMap extends HashMapBase
{
public function __set(string $nm, $val)
public function __set(string $nm, mixed $val): void
{
if ($nm === '') {
$nm = "\xFF";
@@ -56,7 +56,7 @@ final class HashMap extends HashMapBase
}
public function __get(string $nm)
public function __get(string $nm): mixed
{
if ($nm === '') {
$nm = "\xFF";

View File

@@ -12,8 +12,6 @@ namespace Dibi;
class Helpers
{
use Strict;
private static HashMap $types;

View File

@@ -15,8 +15,6 @@ namespace Dibi;
*/
class Literal
{
use Strict;
private string $value;

View File

@@ -17,13 +17,9 @@ use Dibi;
*/
class FileLogger
{
use Dibi\Strict;
/** Name of the file where SQL errors should be logged */
public string $file;
public int $filter;
private bool $errorsOnly;
@@ -74,7 +70,7 @@ class FileLogger
{
$driver = $event->connection->getConfig('driver');
$message .=
"\n-- driver: " . (is_object($driver) ? $driver::class : $driver) . '/' . $event->connection->getConfig('name')
"\n-- driver: " . get_debug_type($driver) . '/' . $event->connection->getConfig('name')
. "\n-- " . date('Y-m-d H:i:s')
. "\n\n";
file_put_contents($this->file, $message, FILE_APPEND | LOCK_EX);

View File

@@ -27,8 +27,6 @@ use Dibi;
*/
class Column
{
use Dibi\Strict;
/** when created by Result */
private ?Dibi\Reflector $reflector;

View File

@@ -21,10 +21,7 @@ use Dibi;
*/
class Database
{
use Dibi\Strict;
private Dibi\Reflector $reflector;
private ?string $name;
/** @var Table[] */

View File

@@ -9,7 +9,6 @@ declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
/**
@@ -20,8 +19,6 @@ use Dibi;
*/
class ForeignKey
{
use Dibi\Strict;
private string $name;
/** @var array of [local, foreign, onDelete, onUpdate] */

View File

@@ -9,7 +9,6 @@ declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
/**
@@ -22,8 +21,6 @@ use Dibi;
*/
class Index
{
use Dibi\Strict;
/** @var array (name, columns, [unique], [primary]) */
private array $info;

View File

@@ -20,8 +20,6 @@ use Dibi;
*/
class Result
{
use Dibi\Strict;
private Dibi\ResultDriver $driver;
/** @var Column[]|null */

View File

@@ -25,12 +25,8 @@ use Dibi;
*/
class Table
{
use Dibi\Strict;
private Dibi\Reflector $reflector;
private string $name;
private bool $view;
/** @var Column[] */
@@ -41,7 +37,6 @@ class Table
/** @var Index[] */
private array $indexes;
private ?Index $primaryKey;

View File

@@ -17,13 +17,10 @@ namespace Dibi;
*/
class Result implements IDataSource
{
use Strict;
private ?ResultDriver $driver;
/** Translate table */
private array $types = [];
private ?Reflection\Result $meta;
/** Already fetched? Used for allowance for first seek(0) */
@@ -34,7 +31,6 @@ class Result implements IDataSource
/** @var callable|null returned object factory */
private $rowFactory;
private array $formats = [];

View File

@@ -15,12 +15,8 @@ namespace Dibi;
*/
class ResultIterator implements \Iterator, \Countable
{
use Strict;
private Result $result;
private mixed $row;
private int $pointer = 0;
@@ -44,6 +40,7 @@ class ResultIterator implements \Iterator, \Countable
/**
* Returns the key of the current element.
*/
#[\ReturnTypeWillChange]
public function key(): mixed
{
return $this->pointer;
@@ -53,6 +50,7 @@ class ResultIterator implements \Iterator, \Countable
/**
* Returns the current element.
*/
#[\ReturnTypeWillChange]
public function current(): mixed
{
return $this->row;

View File

@@ -48,10 +48,11 @@ class Row implements \ArrayAccess, \IteratorAggregate, \Countable
}
public function __get(string $key)
public function __get(string $key): mixed
{
$hint = Helpers::getSuggestion(array_keys((array) $this), $key);
trigger_error("Attempt to read missing column '$key'" . ($hint ? ", did you mean '$hint'?" : '.'), E_USER_NOTICE);
return null;
}

View File

@@ -1,112 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
/**
* Better OOP experience.
*/
trait Strict
{
/** @var array [method => [type => callback]] */
private static array $extMethods;
/**
* Call to undefined method.
* @throws \LogicException
*/
public function __call(string $name, array $args)
{
$class = method_exists($this, $name) ? 'parent' : static::class;
$items = (new ReflectionClass($this))->getMethods(ReflectionMethod::IS_PUBLIC);
$items = array_map(fn($item) => $item->getName(), $items);
$hint = ($t = Helpers::getSuggestion($items, $name))
? ", did you mean $t()?"
: '.';
throw new \LogicException("Call to undefined method $class::$name()$hint");
}
/**
* Call to undefined static method.
* @throws \LogicException
*/
public static function __callStatic(string $name, array $args)
{
$rc = new ReflectionClass(static::class);
$items = array_filter($rc->getMethods(\ReflectionMethod::IS_STATIC), fn($m) => $m->isPublic());
$items = array_map(fn($item) => $item->getName(), $items);
$hint = ($t = Helpers::getSuggestion($items, $name))
? ", did you mean $t()?"
: '.';
throw new \LogicException("Call to undefined static method {$rc->getName()}::$name()$hint");
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function &__get(string $name)
{
if ((method_exists($this, $m = 'get' . $name) || method_exists($this, $m = 'is' . $name))
&& (new ReflectionMethod($this, $m))->isPublic()
) { // back compatiblity
$ret = $this->$m();
return $ret;
}
$rc = new ReflectionClass($this);
$items = array_filter($rc->getProperties(ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic());
$items = array_map(fn($item) => $item->getName(), $items);
$hint = ($t = Helpers::getSuggestion($items, $name))
? ", did you mean $$t?"
: '.';
throw new \LogicException("Attempt to read undeclared property {$rc->getName()}::$$name$hint");
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function __set(string $name, $value)
{
$rc = new ReflectionClass($this);
$items = array_filter($rc->getProperties(ReflectionProperty::IS_PUBLIC), fn($p) => !$p->isStatic());
$items = array_map(fn($item) => $item->getName(), $items);
$hint = ($t = Helpers::getSuggestion($items, $name))
? ", did you mean $$t?"
: '.';
throw new \LogicException("Attempt to write to undeclared property {$rc->getName()}::$$name$hint");
}
public function __isset(string $name): bool
{
return false;
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function __unset(string $name)
{
$class = static::class;
throw new \LogicException("Attempt to unset undeclared property $class::$$name.");
}
}

View File

@@ -15,29 +15,18 @@ namespace Dibi;
*/
final class Translator
{
use Strict;
private Connection $connection;
private Driver $driver;
private int $cursor = 0;
private array $args;
/** @var string[] */
private array $errors;
private bool $comment = false;
private int $ifLevel = 0;
private int $ifLevelStart = 0;
private ?int $limit = null;
private ?int $offset = null;
private HashMap $identifiers;
@@ -268,7 +257,7 @@ final class Translator
$proto = array_keys($v);
}
} else {
return $this->errors[] = '**Unexpected type ' . (is_object($v) ? $v::class : gettype($v)) . '**';
return $this->errors[] = '**Unexpected type ' . get_debug_type($v) . '**';
}
$pair = explode('%', $k, 2); // split into identifier & modifier
@@ -318,13 +307,9 @@ final class Translator
&& $modifier === null
&& !$value instanceof Literal
&& !$value instanceof Expression
&& $result = $this->connection->translateObject($value)
) {
foreach ($this->connection->getObjectTranslators() as $class => $translator) {
if ($value instanceof $class) {
$value = $translator($value);
return $this->connection->translate(...$value->getValues());
}
}
return $this->connection->translate(...$result->getValues());
}
// object-to-scalar procession
@@ -350,7 +335,7 @@ final class Translator
) {
// continue
} else {
$type = is_object($value) ? $value::class : gettype($value);
$type = get_debug_type($value);
return $this->errors[] = "**Invalid combination of type $type and modifier %$modifier**";
}
}
@@ -438,18 +423,17 @@ final class Translator
$value = substr($value, 0, $toSkip)
. preg_replace_callback(
<<<'XX'
/
(?=[`['":])
(?:
`(.+?)`|
\[(.+?)\]|
(')((?:''|[^'])*)'|
(")((?:""|[^"])*)"|
('|")|
:(\S*?:)([a-zA-Z0-9._]?)
)/sx
XX
,
/
(?=[`['":])
(?:
`(.+?)`|
\[(.+?)]|
(')((?:''|[^'])*)'|
(")((?:""|[^"])*)"|
(['"])|
:(\S*?:)([a-zA-Z0-9._]?)
)/sx
XX,
[$this, 'cb'],
substr($value, $toSkip),
);
@@ -517,7 +501,7 @@ final class Translator
return $this->connection->translate(...$value->getValues());
} else {
$type = is_object($value) ? $value::class : gettype($value);
$type = get_debug_type($value);
return $this->errors[] = "**Unexpected $type**";
}
}

View File

@@ -37,14 +37,12 @@ declare(strict_types=1);
*/
class dibi
{
use Dibi\Strict;
public const
AFFECTED_ROWS = 'a',
IDENTIFIER = 'n';
/** version */
public const VERSION = '5.0-dev';
public const VERSION = '5.0.1';
/** sorting order */
public const

View File

@@ -73,24 +73,29 @@ test('', function () use ($config) {
test('', function () use ($config) {
Assert::exception(function () use ($config) {
new Connection($config + ['onConnect' => '']);
}, InvalidArgumentException::class, "Configuration option 'onConnect' must be array.");
Assert::exception(
fn() => new Connection($config + ['onConnect' => '']),
InvalidArgumentException::class,
"Configuration option 'onConnect' must be array.",
);
$e = Assert::exception(function () use ($config) {
new Connection($config + ['onConnect' => ['STOP']]);
}, Dibi\DriverException::class);
$e = Assert::exception(
fn() => new Connection($config + ['onConnect' => ['STOP']]),
Dibi\DriverException::class,
);
Assert::same('STOP', $e->getSql());
$e = Assert::exception(function () use ($config) {
new Connection($config + ['onConnect' => [['STOP %i', 123]]]);
}, Dibi\DriverException::class);
$e = Assert::exception(
fn() => new Connection($config + ['onConnect' => [['STOP %i', 123]]]),
Dibi\DriverException::class,
);
Assert::same('STOP 123', $e->getSql());
// lazy
$conn = new Connection($config + ['lazy' => true, 'onConnect' => ['STOP']]);
$e = Assert::exception(function () use ($conn) {
$conn->query('SELECT 1');
}, Dibi\DriverException::class);
$e = Assert::exception(
fn() => $conn->query('SELECT 1'),
Dibi\DriverException::class,
);
Assert::same('STOP', $e->getSql());
});

View File

@@ -24,14 +24,16 @@ class Time extends DateTimeImmutable
test('Without object translator', function () use ($conn) {
Assert::exception(function () use ($conn) {
$conn->translate('?', new Email);
}, Dibi\Exception::class, 'SQL translate error: Unexpected Email');
Assert::exception(
fn() => $conn->translate('?', new Email),
Dibi\Exception::class,
'SQL translate error: Unexpected Email',
);
});
test('Basics', function () use ($conn) {
$conn->addObjectTranslator(Email::class, fn($object) => new Dibi\Expression('?', $object->address));
$conn->setObjectTranslator(fn(Email $email) => new Dibi\Expression('?', $email->address));
Assert::same(
reformat([
'sqlsrv' => "N'address@example.com'",
@@ -53,7 +55,7 @@ test('DateTime', function () use ($conn) {
// With object translator
$conn->addObjectTranslator(Time::class, fn($object) => new Dibi\Expression('OwnTime(?)', $object->format('H:i:s')));
$conn->setObjectTranslator(fn(Time $time) => new Dibi\Expression('OwnTime(?)', $time->format('H:i:s')));
Assert::same(
reformat([
'sqlsrv' => "OwnTime(N'12:13:14')",
@@ -86,7 +88,7 @@ test('DateTime', function () use ($conn) {
);
// But DateTime translation can be overloaded
$conn->addObjectTranslator(DateTimeInterface::class, fn() => new Dibi\Expression('OwnDateTime'));
$conn->setObjectTranslator(fn(DateTimeInterface $dt) => new Dibi\Expression('OwnDateTime'));
Assert::same(
'OwnDateTime',
$conn->translate('?', $dt),
@@ -95,9 +97,9 @@ test('DateTime', function () use ($conn) {
test('Complex structures', function () use ($conn) {
$conn->addObjectTranslator(Email::class, fn($object) => new Dibi\Expression('?', $object->address));
$conn->addObjectTranslator(Time::class, fn($object) => new Dibi\Expression('OwnTime(?)', $object->format('H:i:s')));
$conn->addObjectTranslator(Time::class, fn($object) => new Dibi\Expression('OwnTime(?)', $object->format('H:i:s')));
$conn->setObjectTranslator(fn(Email $email) => new Dibi\Expression('?', $email->address));
$conn->setObjectTranslator(fn(Time $time) => new Dibi\Expression('OwnTime(?)', $time->format('H:i:s')));
$conn->setObjectTranslator(fn(DateTimeInterface $dt) => new Dibi\Expression('OwnDateTime'));
$time = Time::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');
Assert::same(
@@ -117,3 +119,37 @@ test('Complex structures', function () use ($conn) {
]),
);
});
test('Invalid translator', function () use ($conn) {
Assert::exception(
fn() => $conn->setObjectTranslator(fn($email) => 'foo'),
Dibi\Exception::class,
'Object translator must have exactly one parameter with class typehint.',
);
Assert::exception(
fn() => $conn->setObjectTranslator(fn(string $email) => 'foo'),
Dibi\Exception::class,
"Object translator must have exactly one parameter with non-nullable class typehint, got 'string'.",
);
Assert::exception(
fn() => $conn->setObjectTranslator(fn(Email|bool $email) => 'foo'),
Dibi\Exception::class,
"Object translator must have exactly one parameter with non-nullable class typehint, got 'bool'.",
);
Assert::exception(
fn() => $conn->setObjectTranslator(fn(Email|null $email) => 'foo'),
Dibi\Exception::class,
"Object translator must have exactly one parameter with non-nullable class typehint, got '?Email'.",
);
$conn->setObjectTranslator(fn(Email $email) => 'foo');
Assert::exception(
fn() => $conn->translate('?', new Email),
Dibi\Exception::class,
"Object translator for class 'Email' returned 'string' but Dibi\\Expression expected.",
);
});

View File

@@ -15,18 +15,22 @@ $conn = new Dibi\Connection($config);
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
/*Assert::exception(function () use ($conn) {
$conn->rollback();
}, Dibi\Exception::class);
/*
Assert::exception(
fn() => $conn->rollback(),
Dibi\Exception::class,
);
Assert::exception(function () use ($conn) {
$conn->commit();
}, Dibi\Exception::class);
Assert::exception(
fn() => $conn->commit(),
Dibi\Exception::class,
);
$conn->begin();
Assert::exception(function () use ($conn) {
$conn->begin();
}, Dibi\Exception::class);
Assert::exception(
fn() => $conn->begin(),
Dibi\Exception::class,
);
*/
@@ -53,14 +57,16 @@ test('begin() & commit()', function () use ($conn) {
test('transaction() fail', function () use ($conn) {
Assert::exception(function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
throw new Exception('my exception');
});
}, Throwable::class, 'my exception');
}),
Throwable::class,
'my exception',
);
Assert::same(4, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
});
@@ -76,8 +82,8 @@ test('transaction() success', function () use ($conn) {
test('nested transaction() call fail', function () use ($conn) {
Assert::exception(function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
@@ -88,8 +94,10 @@ test('nested transaction() call fail', function () use ($conn) {
]);
throw new Exception('my exception');
});
});
}, Throwable::class, 'my exception');
}),
Throwable::class,
'my exception',
);
Assert::same(5, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
});
@@ -111,21 +119,27 @@ test('nested transaction() call success', function () use ($conn) {
test('begin(), commit() & rollback() calls are forbidden in transaction()', function () use ($conn) {
Assert::exception(function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->begin();
});
}, LogicException::class, Dibi\Connection::class . '::begin() call is forbidden inside a transaction() callback');
}),
LogicException::class,
Dibi\Connection::class . '::begin() call is forbidden inside a transaction() callback',
);
Assert::exception(function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->commit();
});
}, LogicException::class, Dibi\Connection::class . '::commit() call is forbidden inside a transaction() callback');
}),
LogicException::class,
Dibi\Connection::class . '::commit() call is forbidden inside a transaction() callback',
);
Assert::exception(function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->rollback();
});
}, LogicException::class, Dibi\Connection::class . '::rollback() call is forbidden inside a transaction() callback');
}),
LogicException::class,
Dibi\Connection::class . '::rollback() call is forbidden inside a transaction() callback',
);
});

View File

@@ -147,9 +147,11 @@ if ($config['system'] === 'mysql') {
->limit(' 1; DROP TABLE users')
->offset(' 1; DROP TABLE users');
Assert::exception(function () use ($fluent) {
(string) $fluent;
}, Dibi\Exception::class, "Expected number, ' 1; DROP TABLE users' given.");
Assert::exception(
fn() => (string) $fluent,
Dibi\Exception::class,
"Expected number, ' 1; DROP TABLE users' given.",
);
}

View File

@@ -12,22 +12,32 @@ Assert::same(0, Helpers::intVal(0));
Assert::same(0, Helpers::intVal('0'));
Assert::same(-10, Helpers::intVal('-10'));
Assert::exception(function () {
Helpers::intVal('12345678901234567890123456879');
}, Dibi\Exception::class, 'Number 12345678901234567890123456879 is greater than integer.');
Assert::exception(
fn() => Helpers::intVal('12345678901234567890123456879'),
Dibi\Exception::class,
'Number 12345678901234567890123456879 is greater than integer.',
);
Assert::exception(function () {
Helpers::intVal('-12345678901234567890123456879');
}, Dibi\Exception::class, 'Number -12345678901234567890123456879 is greater than integer.');
Assert::exception(
fn() => Helpers::intVal('-12345678901234567890123456879'),
Dibi\Exception::class,
'Number -12345678901234567890123456879 is greater than integer.',
);
Assert::exception(function () {
Helpers::intVal('');
}, Dibi\Exception::class, "Expected number, '' given.");
Assert::exception(
fn() => Helpers::intVal(''),
Dibi\Exception::class,
"Expected number, '' given.",
);
Assert::exception(function () {
Helpers::intVal('not number');
}, Dibi\Exception::class, "Expected number, 'not number' given.");
Assert::exception(
fn() => Helpers::intVal('not number'),
Dibi\Exception::class,
"Expected number, 'not number' given.",
);
Assert::exception(function () {
Helpers::intVal(null);
}, Dibi\Exception::class, "Expected number, '' given.");
Assert::exception(
fn() => Helpers::intVal(null),
Dibi\Exception::class,
"Expected number, '' given.",
);

View File

@@ -19,17 +19,22 @@ function buildPdoDriver(?int $errorMode)
// PDO error mode: exception
Assert::exception(function () {
buildPdoDriver(PDO::ERRMODE_EXCEPTION);
}, Dibi\DriverException::class, 'PDO connection in exception or warning error mode is not supported.');
Assert::exception(
fn() => buildPdoDriver(PDO::ERRMODE_EXCEPTION),
Dibi\DriverException::class,
'PDO connection in exception or warning error mode is not supported.',
);
// PDO error mode: warning
Assert::exception(function () {
buildPdoDriver(PDO::ERRMODE_WARNING);
}, Dibi\DriverException::class, 'PDO connection in exception or warning error mode is not supported.');
Assert::exception(
fn() => buildPdoDriver(PDO::ERRMODE_WARNING),
Dibi\DriverException::class,
'PDO connection in exception or warning error mode is not supported.',
);
test('PDO error mode: explicitly set silent', function () {
buildPdoDriver(PDO::ERRMODE_SILENT);
});
test(
'PDO error mode: explicitly set silent',
fn() => buildPdoDriver(PDO::ERRMODE_SILENT)
);

View File

@@ -162,9 +162,10 @@ test('', function () {
if (PHP_VERSION_ID < 80000) {
Assert::same(['col' => 0], @$result->test(['col' => ''])); // triggers warning since PHP 7.1
} else {
Assert::exception(function () use ($result) {
Assert::same(['col' => 0], $result->test(['col' => '']));
}, TypeError::class);
Assert::exception(
fn() => Assert::same(['col' => 0], $result->test(['col' => ''])),
TypeError::class,
);
}
Assert::same(['col' => 0], $result->test(['col' => '0']));
@@ -189,9 +190,10 @@ test('', function () {
$result->setType('col', Type::DATETIME);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(function () use ($result) {
$result->test(['col' => true]);
}, TypeError::class);
Assert::exception(
fn() => $result->test(['col' => true]),
TypeError::class,
);
Assert::same(['col' => null], $result->test(['col' => false]));
Assert::same(['col' => null], $result->test(['col' => '']));
@@ -208,9 +210,10 @@ test('', function () {
$result->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(function () use ($result) {
$result->test(['col' => true]);
}, TypeError::class);
Assert::exception(
fn() => $result->test(['col' => true]),
TypeError::class,
);
Assert::same(['col' => null], $result->test(['col' => false]));
Assert::same(['col' => null], $result->test(['col' => '']));
@@ -226,9 +229,10 @@ test('', function () {
$result->setType('col', Type::DATE);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(function () use ($result) {
$result->test(['col' => true]);
}, TypeError::class);
Assert::exception(
fn() => $result->test(['col' => true]),
TypeError::class,
);
Assert::same(['col' => null], $result->test(['col' => false]));
Assert::same(['col' => null], $result->test(['col' => '']));
@@ -242,9 +246,10 @@ test('', function () {
$result->setType('col', Type::TIME);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(function () use ($result) {
$result->test(['col' => true]);
}, TypeError::class);
Assert::exception(
fn() => $result->test(['col' => true]),
TypeError::class,
);
Assert::same(['col' => null], $result->test(['col' => false]));
Assert::same(['col' => null], $result->test(['col' => '']));

View File

@@ -24,13 +24,17 @@ Assert::true(isset($row['title']));
// missing
Assert::error(function () use ($row) {
$x = $row->missing;
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
Assert::error(
fn() => $x = $row->missing,
E_USER_NOTICE,
"Attempt to read missing column 'missing'.",
);
Assert::error(function () use ($row) {
$x = $row['missing'];
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
Assert::error(
fn() => $x = $row['missing'],
E_USER_NOTICE,
"Attempt to read missing column 'missing'.",
);
Assert::false(isset($row->missing));
Assert::false(isset($row['missing']));
@@ -41,13 +45,17 @@ Assert::same(123, $row['missing'] ?? 123);
// suggestions
Assert::error(function () use ($row) {
$x = $row->tilte;
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
Assert::error(
fn() => $x = $row->tilte,
E_USER_NOTICE,
"Attempt to read missing column 'tilte', did you mean 'title'?",
);
Assert::error(function () use ($row) {
$x = $row['tilte'];
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
Assert::error(
fn() => $x = $row['tilte'],
E_USER_NOTICE,
"Attempt to read missing column 'tilte', did you mean 'title'?",
);
// to array

View File

@@ -83,7 +83,7 @@ $tests = function ($conn) {
);
Assert::exception(
$conn->translate('SELECT 1 %ofs %lmt', 10, 10),
fn() => $conn->translate('SELECT 1 %ofs %lmt', 10, 10),
Dibi\NotSupportedException::class,
);
}

View File

@@ -1,151 +0,0 @@
<?php
declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
class TestClass
{
use Dibi\Strict;
public $public;
public static $publicStatic;
protected $protected;
public function publicMethod()
{
}
public static function publicMethodStatic()
{
}
protected function protectedMethod()
{
}
protected static function protectedMethodS()
{
}
public function getBar()
{
return 123;
}
public function isFoo()
{
return 456;
}
}
class TestChild extends TestClass
{
public function callParent()
{
parent::callParent();
}
}
// calling
Assert::exception(function () {
$obj = new TestClass;
$obj->undeclared();
}, LogicException::class, 'Call to undefined method TestClass::undeclared().');
Assert::exception(function () {
TestClass::undeclared();
}, LogicException::class, 'Call to undefined static method TestClass::undeclared().');
Assert::exception(function () {
$obj = new TestChild;
$obj->callParent();
}, LogicException::class, 'Call to undefined method parent::callParent().');
Assert::exception(function () {
$obj = new TestClass;
$obj->publicMethodX();
}, LogicException::class, 'Call to undefined method TestClass::publicMethodX(), did you mean publicMethod()?');
Assert::exception(function () { // suggest static method
$obj = new TestClass;
$obj->publicMethodStaticX();
}, LogicException::class, 'Call to undefined method TestClass::publicMethodStaticX(), did you mean publicMethodStatic()?');
Assert::exception(function () { // suggest only public method
$obj = new TestClass;
$obj->protectedMethodX();
}, LogicException::class, 'Call to undefined method TestClass::protectedMethodX().');
// writing
Assert::exception(function () {
$obj = new TestClass;
$obj->undeclared = 'value';
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$undeclared.');
Assert::exception(function () {
$obj = new TestClass;
$obj->publicX = 'value';
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$publicX, did you mean $public?');
Assert::exception(function () { // suggest only non-static property
$obj = new TestClass;
$obj->publicStaticX = 'value';
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$publicStaticX.');
Assert::exception(function () { // suggest only public property
$obj = new TestClass;
$obj->protectedX = 'value';
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$protectedX.');
// property getter
$obj = new TestClass;
Assert::false(isset($obj->bar));
Assert::same(123, $obj->bar);
Assert::false(isset($obj->foo));
Assert::same(456, $obj->foo);
// reading
Assert::exception(function () {
$obj = new TestClass;
$val = $obj->undeclared;
}, LogicException::class, 'Attempt to read undeclared property TestClass::$undeclared.');
Assert::exception(function () {
$obj = new TestClass;
$val = $obj->publicX;
}, LogicException::class, 'Attempt to read undeclared property TestClass::$publicX, did you mean $public?');
Assert::exception(function () { // suggest only non-static property
$obj = new TestClass;
$val = $obj->publicStaticX;
}, LogicException::class, 'Attempt to read undeclared property TestClass::$publicStaticX.');
Assert::exception(function () { // suggest only public property
$obj = new TestClass;
$val = $obj->protectedX;
}, LogicException::class, 'Attempt to read undeclared property TestClass::$protectedX.');
// unset/isset
Assert::exception(function () {
$obj = new TestClass;
unset($obj->undeclared);
}, LogicException::class, 'Attempt to unset undeclared property TestClass::$undeclared.');
Assert::false(isset($obj->undeclared));

View File

@@ -13,13 +13,16 @@ switch ($config['system']) {
case 'mysql':
Assert::equal('10:20:30.0', $translator->formatValue(new DateInterval('PT10H20M30S'), null));
Assert::equal('-1:00:00.0', $translator->formatValue(DateInterval::createFromDateString('-1 hour'), null));
Assert::exception(function () use ($translator) {
$translator->formatValue(new DateInterval('P2Y4DT6H8M'), null);
}, Dibi\NotSupportedException::class, 'Only time interval is supported.');
Assert::exception(
fn() => $translator->formatValue(new DateInterval('P2Y4DT6H8M'), null),
Dibi\NotSupportedException::class,
'Only time interval is supported.',
);
break;
default:
Assert::exception(function () use ($translator) {
$translator->formatValue(new DateInterval('PT10H20M30S'), null);
}, Dibi\Exception::class);
Assert::exception(
fn() => $translator->formatValue(new DateInterval('PT10H20M30S'), null),
Dibi\Exception::class,
);
}

View File

@@ -91,9 +91,11 @@ Assert::same(
);
// invalid input
$e = Assert::exception(function () use ($conn) {
$conn->translate('SELECT %s', (object) [123], ', %m', 123);
}, Dibi\Exception::class, 'SQL translate error: Invalid combination of type stdClass and modifier %s');
$e = Assert::exception(
fn() => $conn->translate('SELECT %s', (object) [123], ', %m', 123),
Dibi\Exception::class,
'SQL translate error: Invalid combination of type stdClass and modifier %s',
);
Assert::same('SELECT **Invalid combination of type stdClass and modifier %s** , **Unknown or unexpected modifier %m**', $e->getSql());
Assert::same(
@@ -176,9 +178,10 @@ Assert::same(
);
if ($config['system'] === 'odbc') {
Assert::exception(function () use ($conn) {
$conn->translate('SELECT * FROM [products] %lmt %ofs', 2, 1);
}, Dibi\Exception::class);
Assert::exception(
fn() => $conn->translate('SELECT * FROM [products] %lmt %ofs', 2, 1),
Dibi\Exception::class,
);
} else {
// with limit = 2, offset = 1
Assert::same(
@@ -226,9 +229,11 @@ Assert::same(
]),
);
Assert::exception(function () use ($conn) {
$conn->translate('SELECT %s', new DateTime('1212-09-26'));
}, Dibi\Exception::class, 'SQL translate error: Invalid combination of type Dibi\DateTime and modifier %s');
Assert::exception(
fn() => $conn->translate('SELECT %s', new DateTime('1212-09-26')),
Dibi\Exception::class,
'SQL translate error: Invalid combination of type Dibi\DateTime and modifier %s',
);
@@ -268,9 +273,11 @@ if ($config['system'] === 'postgre') {
}
$e = Assert::exception(function () use ($conn) {
$conn->translate("SELECT '");
}, Dibi\Exception::class, 'SQL translate error: Alone quote');
$e = Assert::exception(
fn() => $conn->translate("SELECT '"),
Dibi\Exception::class,
'SQL translate error: Alone quote',
);
Assert::same('SELECT **Alone quote**', $e->getSql());
Assert::match(
@@ -647,13 +654,17 @@ Assert::same(
$conn->translate('INSERT INTO [test.*]'),
);
Assert::exception(function () use ($conn) {
$conn->translate('INSERT INTO %i', 'ahoj');
}, Dibi\Exception::class, "Expected number, 'ahoj' given.");
Assert::exception(
fn() => $conn->translate('INSERT INTO %i', 'ahoj'),
Dibi\Exception::class,
"Expected number, 'ahoj' given.",
);
Assert::exception(function () use ($conn) {
$conn->translate('INSERT INTO %f', 'ahoj');
}, Dibi\Exception::class, "Expected number, 'ahoj' given.");
Assert::exception(
fn() => $conn->translate('INSERT INTO %f', 'ahoj'),
Dibi\Exception::class,
"Expected number, 'ahoj' given.",
);
Assert::same(

View File

@@ -20,7 +20,7 @@ date_default_timezone_set('Europe/Prague');
try {
$config = Tester\Environment::loadData();
} catch (Throwable $e) {
$config = parse_ini_file(__DIR__ . '/../databases.ini', true);
$config = parse_ini_file(__DIR__ . '/../databases.ini', process_sections: true);
$config = reset($config);
}

View File

@@ -15,41 +15,54 @@ $conn = new Dibi\Connection($config);
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
$e = Assert::exception(function () use ($conn) {
new Dibi\Connection([
$e = Assert::exception(
fn() => new Dibi\Connection([
'driver' => 'mysqli',
'host' => 'localhost',
'username' => 'unknown',
'password' => 'unknown',
]);
}, Dibi\DriverException::class);
]),
Dibi\DriverException::class,
);
Assert::null($e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('SELECT');
}, Dibi\DriverException::class, '%a% error in your SQL syntax;%a%', 1064);
$e = Assert::exception(
fn() => $conn->query('SELECT'),
Dibi\DriverException::class,
'%a% error in your SQL syntax;%a%',
1064,
);
Assert::same('SELECT', $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")');
}, Dibi\UniqueConstraintViolationException::class, "%a?%Duplicate entry '1' for key '%a?%PRIMARY'", 1062);
$e = Assert::exception(
fn() => $conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")'),
Dibi\UniqueConstraintViolationException::class,
"%a?%Duplicate entry '1' for key '%a?%PRIMARY'",
1062,
);
Assert::same("INSERT INTO products (product_id, title) VALUES (1, 'New')", $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO products (title) VALUES (NULL)');
}, Dibi\NotNullConstraintViolationException::class, "%a?%Column 'title' cannot be null", 1048);
$e = Assert::exception(
fn() => $conn->query('INSERT INTO products (title) VALUES (NULL)'),
Dibi\NotNullConstraintViolationException::class,
"%a?%Column 'title' cannot be null",
1048,
);
Assert::same('INSERT INTO products (title) VALUES (NULL)', $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)');
}, Dibi\ForeignKeyConstraintViolationException::class, '%a% a foreign key constraint fails %a%', 1452);
$e = Assert::exception(
fn() => $conn->query('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)'),
Dibi\ForeignKeyConstraintViolationException::class,
'%a% a foreign key constraint fails %a%',
1452,
);
Assert::same('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)', $e->getSql());

View File

@@ -15,29 +15,40 @@ $conn = new Dibi\Connection($config);
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
$e = Assert::exception(function () use ($conn) {
$conn->query('SELECT INTO');
}, Dibi\DriverException::class, '%a?%syntax error %A%');
$e = Assert::exception(
fn() => $conn->query('SELECT INTO'),
Dibi\DriverException::class,
'%a?%syntax error %A%',
);
Assert::same('SELECT INTO', $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")');
}, Dibi\UniqueConstraintViolationException::class, '%a% violates unique constraint %A%', '23505');
$e = Assert::exception(
fn() => $conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")'),
Dibi\UniqueConstraintViolationException::class,
'%a% violates unique constraint %A%',
'23505',
);
Assert::same("INSERT INTO products (product_id, title) VALUES (1, 'New')", $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO products (title) VALUES (NULL)');
}, Dibi\NotNullConstraintViolationException::class, '%a?%null value in column "title"%a%violates not-null constraint%A?%', '23502');
$e = Assert::exception(
fn() => $conn->query('INSERT INTO products (title) VALUES (NULL)'),
Dibi\NotNullConstraintViolationException::class,
'%a?%null value in column "title"%a%violates not-null constraint%A?%',
'23502',
);
Assert::same('INSERT INTO products (title) VALUES (NULL)', $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)');
}, Dibi\ForeignKeyConstraintViolationException::class, '%a% violates foreign key constraint %A%', '23503');
$e = Assert::exception(
fn() => $conn->query('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)'),
Dibi\ForeignKeyConstraintViolationException::class,
'%a% violates foreign key constraint %A%',
'23503',
);
Assert::same('INSERT INTO orders (customer_id, product_id, amount) VALUES (100, 1, 1)', $e->getSql());

View File

@@ -15,23 +15,32 @@ $conn = new Dibi\Connection($config);
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
$e = Assert::exception(function () use ($conn) {
$conn->query('SELECT');
}, Dibi\DriverException::class, '%a%', 1);
$e = Assert::exception(
fn() => $conn->query('SELECT'),
Dibi\DriverException::class,
'%a%',
1,
);
Assert::same('SELECT', $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")');
}, Dibi\UniqueConstraintViolationException::class, null, 19);
$e = Assert::exception(
fn() => $conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")'),
Dibi\UniqueConstraintViolationException::class,
null,
19,
);
Assert::same("INSERT INTO products (product_id, title) VALUES (1, 'New')", $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO products (title) VALUES (NULL)');
}, Dibi\NotNullConstraintViolationException::class, null, 19);
$e = Assert::exception(
fn() => $conn->query('INSERT INTO products (title) VALUES (NULL)'),
Dibi\NotNullConstraintViolationException::class,
null,
19,
);
Assert::same('INSERT INTO products (title) VALUES (NULL)', $e->getSql());