Skip to content

Commit

Permalink
added ObjectTranslator (dg#420)
Browse files Browse the repository at this point in the history
Object translator allows to translate any object passed to Translator into Dibi\Expression.
  • Loading branch information
milo committed Nov 22, 2022
1 parent 5d11aeb commit cfb0688
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 0 deletions.
20 changes: 20 additions & 0 deletions src/Dibi/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class Connection implements IConnection

private ?Translator $translator = null;

private ?ObjectTranslator $objectTranslator = null;

private HashMap $substitutes;

private int $transactionDepth = 0;
Expand Down Expand Up @@ -126,6 +128,7 @@ final public function connect(): void
if ($this->config['driver'] instanceof Driver) {
$this->driver = $this->config['driver'];
$this->translator = new Translator($this);
$this->translator->setObjectTranslator($this->objectTranslator);
return;

} elseif (is_subclass_of($this->config['driver'], Driver::class)) {
Expand All @@ -143,6 +146,7 @@ final public function connect(): void
try {
$this->driver = new $class($this->config);
$this->translator = new Translator($this);
$this->translator->setObjectTranslator($this->objectTranslator);

if ($event) {
$this->onEvent($event->done());
Expand Down Expand Up @@ -522,6 +526,22 @@ public function substitute(string $value): string
}


/********************* value objects translation ****************d*g**/


public function setObjectTranslator(?ObjectTranslator $translator): void
{
$this->objectTranslator = $translator;
$this->translator?->setObjectTranslator($translator);
}


public function getObjectTranslator(): ?ObjectTranslator
{
return $this->objectTranslator;
}


/********************* shortcuts ****************d*g**/


Expand Down
17 changes: 17 additions & 0 deletions src/Dibi/Translator.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ final class Translator

private Driver $driver;

private ?ObjectTranslator $objectTranslator = null;

private int $cursor = 0;

private array $args;
Expand Down Expand Up @@ -49,6 +51,12 @@ public function __construct(Connection $connection)
}


public function setObjectTranslator(?ObjectTranslator $translator): void
{
$this->objectTranslator = $translator;
}


/**
* Generates SQL. Can be called only once.
* @throws Exception
Expand Down Expand Up @@ -314,6 +322,15 @@ public function formatValue(mixed $value, ?string $modifier): string
}
}

if ($this->objectTranslator
&& is_object($value)
&& $modifier === null
&& !$value instanceof Literal
&& !$value instanceof Expression
) {
$value = $this->objectTranslator->translateObject($value) ?? $value;
}

// object-to-scalar procession
if ($value instanceof \BackedEnum && is_scalar($value->value)) {
$value = $value->value;
Expand Down
12 changes: 12 additions & 0 deletions src/Dibi/interfaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,15 @@ function commit(?string $savepoint = null): void;
*/
function rollback(?string $savepoint = null): void;
}


/**
* Value object to SQL expression translator.
*/
interface ObjectTranslator
{
/**
* Translate object to expression, return NULL when cannot handle such object type.
*/
function translateObject(object $object): ?Expression;
}
161 changes: 161 additions & 0 deletions tests/dibi/Connection.objectTranslator.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

/**
* @dataProvider ../databases.ini
*/

declare(strict_types=1);

use Tester\Assert;

require __DIR__ . '/bootstrap.php';

$conn = new Dibi\Connection($config + ['formatDateTime' => "'Y-m-d H:i:s.u'", 'formatDate' => "'Y-m-d'"]);


class Email
{
public $address = 'address@example.com';
}

class Time extends DateTimeImmutable
{
}


class EmailTranslator implements Dibi\ObjectTranslator
{
public function translateObject(object $object): ?Dibi\Expression
{
return match (true) {
$object instanceof Email => new Dibi\Expression('?', $object->address),
default => null,
};
}
}

class TimeTranslator implements Dibi\ObjectTranslator
{
public function translateObject(object $object): ?Dibi\Expression
{
return match (true) {
$object instanceof Time => new Dibi\Expression('OwnTime(?)', $object->format('H:i:s')),
default => null,
};
}
}

class DateTimeTranslator implements Dibi\ObjectTranslator
{
public function translateObject(object $object): ?Dibi\Expression
{
return match (true) {
$object instanceof DateTimeInterface => new Dibi\Expression('OwnDateTime'),
default => null,
};
}
}


test('Without object translator', function () use ($conn) {
Assert::exception(function () use ($conn) {
$conn->translate('?', new Email);
}, Dibi\Exception::class, 'SQL translate error: Unexpected Email');
});


test('Basics', function () use ($conn) {
$conn->setObjectTranslator(new EmailTranslator);
Assert::same(
reformat([
'sqlsrv' => "N'address@example.com'",
"'address@example.com'",
]),
$conn->translate('?', new Email),
);
});


test('DateTime', function () use ($conn) {
$stamp = Time::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');

// Without object translator, DateTime child is translated by driver
$conn->setObjectTranslator(null);
Assert::same(
$conn->getDriver()->escapeDateTime($stamp),
$conn->translate('?', $stamp),
);


// With object translator
$conn->setObjectTranslator(new TimeTranslator);
Assert::same(
reformat([
'sqlsrv' => "OwnTime(N'12:13:14')",
"OwnTime('12:13:14')",
]),
$conn->translate('?', $stamp),
);


// With modifier, it is still translated by driver
Assert::same(
$conn->getDriver()->escapeDateTime($stamp),
$conn->translate('%dt', $stamp),
);
Assert::same(
$conn->getDriver()->escapeDateTime($stamp),
$conn->translate('%t', $stamp),
);
Assert::same(
$conn->getDriver()->escapeDate($stamp),
$conn->translate('%d', $stamp),
);


// DateTimeImmutable as a Time parent is not affected and still translated by driver
$dt = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');
Assert::same(
$conn->getDriver()->escapeDateTime($dt),
$conn->translate('?', $dt),
);

// But DateTime translation can be overloaded
$conn->setObjectTranslator(new DateTimeTranslator);
Assert::same(
'OwnDateTime',
$conn->translate('?', $dt),
);
});


test('Complex structures', function () use ($conn) {
$conn->setObjectTranslator(new class implements Dibi\ObjectTranslator {
public function translateObject(object $object): ?Dibi\Expression
{
return match (true) {
$object instanceof Email => (new EmailTranslator)->translateObject($object),
$object instanceof Time => (new TimeTranslator)->translateObject($object),
$object instanceof DateTimeInterface => (new DateTimeTranslator)->translateObject($object),
default => null,
};
}
});

$time = Time::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');
Assert::same(
reformat([
'sqlsrv' => "([a], [b], [c], [d], [e], [f], [g]) VALUES (OwnTime(N'12:13:14'), '2022-11-22', CONVERT(DATETIME2(7), '2022-11-22 12:13:14.000000'), CONVERT(DATETIME2(7), '2022-11-22 12:13:14.000000'), N'address@example.com', OwnDateTime, OwnDateTime)",
"([a], [b], [c], [d], [e], [f], [g]) VALUES (OwnTime('12:13:14'), '2022-11-22', '2022-11-22 12:13:14.000000', '2022-11-22 12:13:14.000000', 'address@example.com', OwnDateTime, OwnDateTime)",
]),
$conn->translate('%v', [
'a' => $time,
'b%d' => $time,
'c%t' => $time,
'd%dt' => $time,
'e' => new Email,
'f' => new DateTime,
'g' => new DateTimeImmutable,
]),
);
});

0 comments on commit cfb0688

Please sign in to comment.