From 1deb36c0d35784ea42090f02dd0cf395885cbac1 Mon Sep 17 00:00:00 2001 From: Victor GREBOT Date: Sat, 26 Jul 2025 01:08:21 +0200 Subject: [PATCH] feature(hydrator): Hydrate DTO without any metadata required --- CHANGELOG | 1 + src/Ting/Driver/CacheResult.php | 13 +++ src/Ting/Driver/Mysqli/Result.php | 20 +++- src/Ting/Driver/Pgsql/Result.php | 21 +++- src/Ting/Driver/ResultInterface.php | 5 + src/Ting/Repository/HydratorValueObject.php | 97 ++++++++++++++++++ tests/fixtures/FakeDriver/MysqliResult.php | 7 ++ tests/fixtures/ValueObject/Bouh.php | 32 ++++++ .../Ting/Repository/HydratorValueObject.php | 99 +++++++++++++++++++ 9 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 src/Ting/Repository/HydratorValueObject.php create mode 100644 tests/fixtures/ValueObject/Bouh.php create mode 100644 tests/units/Ting/Repository/HydratorValueObject.php diff --git a/CHANGELOG b/CHANGELOG index ae5d54f8..e7f601c1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ * BC deleted Metadata setter & getter * BC deleted deprecated methods on Mysqli and Pgsql drivers * BC deleted CCMBenchmark\Ting\Query\Generator::getByCriteriaWithOrderAndLimit, use CCMBenchmark\Ting\Query\Generator::getByCriteria + * New feature: HydratorValueObject allows hydrating simple objects without metadata using native fetch_object() methods 3.12.0 (2025-04-17): * add __debuginfo() in NotifyInterface to reduce context when dumping an entity diff --git a/src/Ting/Driver/CacheResult.php b/src/Ting/Driver/CacheResult.php index 71c52b04..99a671d7 100644 --- a/src/Ting/Driver/CacheResult.php +++ b/src/Ting/Driver/CacheResult.php @@ -35,6 +35,10 @@ class CacheResult implements ResultInterface protected ?Iterator $result = null; + /** + * @var class-string|null + */ + protected ?string $objectToFetch = null; /** * @param string $connectionName @@ -66,6 +70,15 @@ public function setResult($result): static return $this; } + /** + * @param class-string $objectToFetch + */ + public function setObjectToFetch(string $objectToFetch): static + { + $this->objectToFetch = $objectToFetch; + return $this; + } + public function getConnectionName(): ?string { return $this->connectionName; diff --git a/src/Ting/Driver/Mysqli/Result.php b/src/Ting/Driver/Mysqli/Result.php index 493acfd8..ef717512 100644 --- a/src/Ting/Driver/Mysqli/Result.php +++ b/src/Ting/Driver/Mysqli/Result.php @@ -36,7 +36,10 @@ class Result implements ResultInterface /** @var array $fields */ protected array $fields = []; protected int $iteratorOffset = 0; - protected ?array $iteratorCurrent = null; + /** @var array|false|object|null */ + protected $iteratorCurrent = null; + /** @var class-string|null */ + protected ?string $objectToFetch = null; public function setConnectionName(string $connectionName): static { @@ -61,6 +64,15 @@ public function setResult($result): static return $this; } + /** + * @param class-string $objectToFetch + */ + public function setObjectToFetch(string $objectToFetch): static + { + $this->objectToFetch = $objectToFetch; + return $this; + } + public function getConnectionName(): ?string { return $this->connectionName; @@ -193,7 +205,11 @@ public function key(): mixed public function next(): void { if ($this->result !== null) { - $this->iteratorCurrent = $this->format($this->result->fetch_array(MYSQLI_NUM)); + if ($this->objectToFetch !== null) { + $this->iteratorCurrent = $this->result->fetch_object($this->objectToFetch); + } else { + $this->iteratorCurrent = $this->format($this->result->fetch_array(MYSQLI_NUM)); + } $this->iteratorOffset++; } diff --git a/src/Ting/Driver/Pgsql/Result.php b/src/Ting/Driver/Pgsql/Result.php index d92b3a50..4a693772 100644 --- a/src/Ting/Driver/Pgsql/Result.php +++ b/src/Ting/Driver/Pgsql/Result.php @@ -41,7 +41,10 @@ class Result implements ResultInterface protected $result = null; protected array $fields = []; protected int $iteratorOffset = 0; - protected ?array $iteratorCurrent = null; + /** @var array|object|false|null */ + protected $iteratorCurrent = null; + /** @var class-string|null */ + protected ?string $objectToFetch = null; /** * @param string $connectionName @@ -73,6 +76,15 @@ public function setResult($result): static return $this; } + /** + * @param class-string $objectToFetch + */ + public function setObjectToFetch(string $objectToFetch): static + { + $this->objectToFetch = $objectToFetch; + return $this; + } + public function getConnectionName(): ?string { return $this->connectionName; @@ -335,7 +347,12 @@ public function key(): mixed public function next(): void { - $this->iteratorCurrent = $this->format(pg_fetch_array($this->result, null, \PGSQL_NUM)); + if ($this->objectToFetch !== null) { + $this->iteratorCurrent = pg_fetch_object($this->result, null, $this->objectToFetch); + } else { + $this->iteratorCurrent = $this->format(pg_fetch_array($this->result, null, \PGSQL_NUM)); + } + $this->iteratorOffset++; } diff --git a/src/Ting/Driver/ResultInterface.php b/src/Ting/Driver/ResultInterface.php index f2d354b4..5e1e8a17 100644 --- a/src/Ting/Driver/ResultInterface.php +++ b/src/Ting/Driver/ResultInterface.php @@ -43,6 +43,11 @@ public function setDatabase(string $database): static; */ public function setResult($result): static; + /** + * @param class-string $objectToFetch + */ + public function setObjectToFetch(string $objectToFetch): static; + public function getConnectionName(): ?string; public function getDatabase(): ?string; diff --git a/src/Ting/Repository/HydratorValueObject.php b/src/Ting/Repository/HydratorValueObject.php new file mode 100644 index 00000000..68bbec35 --- /dev/null +++ b/src/Ting/Repository/HydratorValueObject.php @@ -0,0 +1,97 @@ + + */ +class HydratorValueObject implements HydratorInterface +{ + /** + * @var class-string + */ + protected $objectToHydrate; + /** + * @var ResultInterface + */ + protected $result = null; + + + /** + * @param class-string $objectToHydrate + */ + public function __construct(string $objectToHydrate) + { + $this->objectToHydrate = $objectToHydrate; + } + + /** + * @return \Generator + */ + public function getIterator(): \Generator + { + $this->result->setObjectToFetch($this->objectToHydrate); + + foreach ($this->result as $key => $row) { + yield $key => $row; + } + } + + /** + * @return int + */ + public function count(): int + { + if ($this->result === null) { + return 0; + } + + return $this->result->getNumRows(); + } + + public function setMetadataRepository(MetadataRepository $metadataRepository): void + { + // Useless for this hydrator + } + + public function setUnitOfWork(UnitOfWork $unitOfWork): void + { + // Useless for this hydrator + } + + public function setResult(ResultInterface $result): static + { + $this->result = $result; + return $this; + } +} diff --git a/tests/fixtures/FakeDriver/MysqliResult.php b/tests/fixtures/FakeDriver/MysqliResult.php index 3f753395..af4249c0 100644 --- a/tests/fixtures/FakeDriver/MysqliResult.php +++ b/tests/fixtures/FakeDriver/MysqliResult.php @@ -119,6 +119,13 @@ public function getConnectionName(): ?string public function getDatabase(): ?string { + } + public function setObjectToFetch(string $objectToFetch): static + { + } + + public function fetch_object($class_name = null) + { } } diff --git a/tests/fixtures/ValueObject/Bouh.php b/tests/fixtures/ValueObject/Bouh.php new file mode 100644 index 00000000..fa84f5a0 --- /dev/null +++ b/tests/fixtures/ValueObject/Bouh.php @@ -0,0 +1,32 @@ +firstname = $firstname; + $this->name = $name; + } + + /** + * @return mixed + */ + public function getFirstname() + { + return $this->firstname; + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } +} diff --git a/tests/units/Ting/Repository/HydratorValueObject.php b/tests/units/Ting/Repository/HydratorValueObject.php new file mode 100644 index 00000000..0f88abe5 --- /dev/null +++ b/tests/units/Ting/Repository/HydratorValueObject.php @@ -0,0 +1,99 @@ +calling($mockMysqliResult)->fetch_fields = function () { + $fields = []; + $stdClass = new \stdClass(); + $stdClass->name = 'firstname'; + $stdClass->orgname = 'boo_firstname'; + $stdClass->table = 'bouh'; + $stdClass->orgtable = 'T_BOUH_BOO'; + $stdClass->type = MYSQLI_TYPE_VAR_STRING; + $fields[] = $stdClass; + + $stdClass = new \stdClass(); + $stdClass->name = 'name'; + $stdClass->orgname = 'boo_name'; + $stdClass->table = 'bouh'; + $stdClass->orgtable = 'T_BOUH_BOO'; + $stdClass->type = MYSQLI_TYPE_VAR_STRING; + $fields[] = $stdClass; + return $fields; + }; + + $this->calling($mockMysqliResult)->fetch_object = function () use ($data) { + return new Bouh(...$data); + }; + + $result = new Result(); + $result->setResult($mockMysqliResult); + $result->setConnectionName('connectionName'); + $result->setDatabase('database'); + + $this + ->if($hydrator = new \CCMBenchmark\Ting\Repository\HydratorValueObject(Bouh::class)) + ->then($iterator = $hydrator->setResult($result)->getIterator()) + ->then($bouh = $iterator->current()) + ->mock($mockMysqliResult) + ->call('fetch_object') + ->once() + ->object($bouh) + ->isInstanceOf('tests\fixtures\ValueObject\Bouh') + ->string($bouh->getName()) + ->isIdenticalTo('Robez-Masson') + ->string($bouh->getFirstname()) + ->isIdenticalTo('Sylvain'); + } + + public function testCountShouldReturn2() + { + $result = new \mock\CCMBenchmark\Ting\Driver\Mysqli\Result(); + + $this->calling($result)->getNumRows = 2; + + $this + ->if($hydrator = new \CCMBenchmark\Ting\Repository\HydratorValueObject(Bouh::class)) + ->then($hydrator->setResult($result)) + ->integer($hydrator->count()) + ->isIdenticalTo(2); + } +}