diff --git a/src/Dibi/Connection.php b/src/Dibi/Connection.php index 9882ad76..7d661fcd 100644 --- a/src/Dibi/Connection.php +++ b/src/Dibi/Connection.php @@ -35,6 +35,8 @@ class Connection implements IConnection private ?Translator $translator = null; + private ?ObjectTranslator $objectTranslator = null; + private HashMap $substitutes; private int $transactionDepth = 0; @@ -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)) { @@ -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()); @@ -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**/ diff --git a/src/Dibi/Translator.php b/src/Dibi/Translator.php index e9289e22..e8498917 100644 --- a/src/Dibi/Translator.php +++ b/src/Dibi/Translator.php @@ -21,6 +21,8 @@ final class Translator private Driver $driver; + private ?ObjectTranslator $objectTranslator = null; + private int $cursor = 0; private array $args; @@ -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 @@ -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; diff --git a/src/Dibi/interfaces.php b/src/Dibi/interfaces.php index 3cf33685..429836bb 100644 --- a/src/Dibi/interfaces.php +++ b/src/Dibi/interfaces.php @@ -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; +} diff --git a/tests/dibi/Connection.objectTranslator.phpt b/tests/dibi/Connection.objectTranslator.phpt new file mode 100644 index 00000000..82e5d649 --- /dev/null +++ b/tests/dibi/Connection.objectTranslator.phpt @@ -0,0 +1,162 @@ + "'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)", + 'odbc' => "([a], [b], [c], [d], [e], [f], [g]) VALUES (OwnTime('12:13:14'), #11/22/2022#, #11/22/2022 12:13:14.000000#, #11/22/2022 12:13:14.000000#, '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, + ]), + ); +});