diff --git a/src/Hydrator.php b/src/Hydrator.php new file mode 100644 index 00000000..d0fd5c88 --- /dev/null +++ b/src/Hydrator.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Csv; + +use Iterator; +use League\Csv\Serializer\DenormalizerFactory; +use League\Csv\Serializer\Factory; +use ReflectionException; + +/** + * @template TClass of object + */ +final class Hydrator implements TabularDataHydrator +{ + public function __construct(private readonly DenormalizerFactory $factory = new Factory()) + { + } + + /** + * Denormalizes data back into an object of a given class. + * + * @param class-string $className + * + * @throws Serializer\DenormalizationFailed + * @throws ReflectionException + * @throws Serializer\TypeCastingFailed + * + * @return TClass + */ + public function hydrate(string $className, array $record): object + { + /** @var TClass $instance */ + $instance = $this->factory + ->newDenormalizer($className, array_keys($record)) + ->denormalize($record); + + return $instance; + } + + /** + * Denormalizes data back into an object of a given class. + * + * @param class-string $className + * @param iterable $records + * + * @throws Serializer\DenormalizationFailed + * @throws ReflectionException + * @throws Serializer\TypeCastingFailed + * + * @return Iterator + */ + public function hydrateAll(string $className, iterable $records): Iterator + { + $iterator = MapIterator::toIterator($records); + $iterator->rewind(); + if (!$iterator->valid()) { + return; + } + + /** @var array $current */ + $current = $iterator->current(); + $key = $iterator->key(); + $iterator->next(); + + $denormalizer = $this->factory->newDenormalizer($className, array_keys($current)); + + while ($iterator->valid()) { + /** @var TClass $instance */ + $instance = $denormalizer->denormalize($current); + + yield $key => $instance; + + /** @var array $current */ + $current = $iterator->current(); + $key = $iterator->key(); + $iterator->next(); + } + + /** @var TClass $instance */ + $instance = $denormalizer->denormalize($current); + + yield $key => $instance; + } +} diff --git a/src/JsonConverter.php b/src/JsonConverter.php index b9030f0c..97966c73 100644 --- a/src/JsonConverter.php +++ b/src/JsonConverter.php @@ -233,7 +233,7 @@ public function __call(string $name, array $arguments): self|bool /** * Returns the PHP json flag associated to its method suffix to ease method lookup. */ - private static function methodToFlag(string $method, int $prefixSize): int + private static function methodToFlag(string $methodName, int $prefixSize): int { static $suffix2Flag; @@ -252,8 +252,8 @@ private static function methodToFlag(string $method, int $prefixSize): int } } - return $suffix2Flag[substr($method, $prefixSize)] - ?? throw new BadMethodCallException('The method "'.self::class.'::'.$method.'" does not exist.'); + return $suffix2Flag[substr($methodName, $prefixSize)] + ?? throw new BadMethodCallException('The method "'.self::class.'::'.$methodName.'" does not exist.'); } /** @@ -365,7 +365,7 @@ public function download(iterable $records, ?string $filename = null): int HttpHeaders::forFileDownload($filename, 'application/json; charset=utf-8'); } - return $this->save($records, new SplFileObject('php://output', 'w')); + return $this->save($records, new SplFileObject('php://output', 'wb')); } /** @@ -408,9 +408,9 @@ public function save(iterable $records, mixed $destination, $context = null): in $stream = match (true) { $destination instanceof Stream, $destination instanceof SplFileObject => $destination, - $destination instanceof SplFileInfo => $destination->openFile(mode:'w', context: $context), + $destination instanceof SplFileInfo => $destination->openFile(mode:'wb', context: $context), is_resource($destination) => Stream::createFromResource($destination), - is_string($destination) => Stream::createFromPath($destination, 'w', $context), + is_string($destination) => Stream::createFromPath($destination, 'wb', $context), default => throw new TypeError('The destination path must be a filename, a stream or a SplFileInfo object.'), }; $bytes = 0; diff --git a/src/Reader.php b/src/Reader.php index 5168d1c8..051bc97b 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -17,7 +17,6 @@ use Closure; use Iterator; use JsonSerializable; -use League\Csv\Serializer\Denormalizer; use League\Csv\Serializer\MappingFailed; use League\Csv\Serializer\TypeCastingFailed; use SplFileObject; @@ -484,18 +483,14 @@ public function getRecords(array $header = []): Iterator * @throws MappingFailed * @throws TypeCastingFailed * - * @return iterator + * @return Iterator */ public function getRecordsAsObject(string $className, array $header = []): Iterator { - /** @var array $header */ - $header = $this->prepareHeader($header); + /** @var Iterator $records */ + $records = (new Hydrator())->hydrateAll($className, $this->getRecords($header)); - return Denormalizer::assignAll( - $className, - $this->combineHeader($this->prepareRecords(), $header), - $header - ); + return $records; } /** diff --git a/src/ResultSet.php b/src/ResultSet.php index 013d0424..30776f53 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -19,10 +19,10 @@ use Generator; use Iterator; use JsonSerializable; -use League\Csv\Serializer\Denormalizer; use League\Csv\Serializer\MappingFailed; use League\Csv\Serializer\TypeCastingFailed; use LimitIterator; +use ReflectionException; use function array_filter; use function array_flip; @@ -316,24 +316,24 @@ public function getRecords(array $header = []): Iterator } /** - * @template T of object - * @param class-string $className + * @template TClass of object + * + * @param class-string $className * @param array $header * * @throws Exception * @throws MappingFailed + * @throws ReflectionException * @throws TypeCastingFailed - * @return iterator + * + * @return Iterator */ public function getRecordsAsObject(string $className, array $header = []): Iterator { - $header = $this->prepareHeader($header); + /** @var Iterator $data */ + $data = (new Hydrator())->hydrateAll($className, $this->getRecords($header)); - return Denormalizer::assignAll( - $className, - $this->combineHeader($header), - $header - ); + return $data; } /** @@ -424,8 +424,9 @@ public function nthAsObject(int $nth, string $className, array $header = []): ?o return null; } + $mapper = new Hydrator(); if ([] === $header || $this->header === $header) { - return Denormalizer::assign($className, $record); + return $mapper->hydrate($className, $record); } $row = array_values($record); @@ -434,7 +435,7 @@ public function nthAsObject(int $nth, string $className, array $header = []): ?o $record[$headerName] = $row[$offset] ?? null; } - return Denormalizer::assign($className, $record); + return $mapper->hydrate($className, $record); } /** diff --git a/src/Serializer/Denormalizer.php b/src/Serializer/Denormalizer.php index 8b9d6798..661edc1c 100644 --- a/src/Serializer/Denormalizer.php +++ b/src/Serializer/Denormalizer.php @@ -29,18 +29,18 @@ use function count; use function is_int; -final class Denormalizer +final class Denormalizer implements TabularDataDenormalizer { private static bool $convertEmptyStringToNull = true; - private readonly ReflectionClass $class; + private ReflectionClass $class; /** @var array */ - private readonly array $properties; + private array $properties; /** @var array */ - private readonly array $propertySetters; + private array $propertySetters; /** @var array */ - private readonly array $afterMappingCalls; - private readonly ?MapRecord $mapRecord; + private array $afterMappingCalls; + private ?MapRecord $mapRecord; /** * @param class-string $className @@ -49,6 +49,17 @@ final class Denormalizer * @throws MappingFailed */ public function __construct(string $className, array $propertyNames = []) + { + $this->initialize($className, $propertyNames); + } + + /** + * @param class-string $className + * @param array $propertyNames + * + * @return void + */ + public function initialize(string $className, array $propertyNames = []): void { $this->class = $this->setClass($className); $this->properties = $this->class->getProperties(); @@ -58,29 +69,60 @@ public function __construct(string $className, array $propertyNames = []) } /** - * @deprecated since version 9.17.0 - * - * @see MapRecord::$convertEmptyStringToNull - * @see MapCell::$convertEmptyStringToNull + * @param class-string $className * - * Enables converting empty string to the null value. + * @throws MappingFailed */ - public static function allowEmptyStringAsNull(): void + private function setClass(string $className): ReflectionClass { - self::$convertEmptyStringToNull = true; + class_exists($className) || throw new MappingFailed('The class `'.$className.'` can not be denormalized; The class does not exist or could not be found.'); + + $class = new ReflectionClass($className); + if ($class->isInternal() && $class->isFinal()) { + throw new MappingFailed('The class `'.$className.'` can not be denormalized; PHP internal class marked as final can not be instantiated without using the constructor.'); + } + + return $class; } /** - * @deprecated since version 9.17.0 + * @param array $propertyNames * - * @see MapRecord::$convertEmptyStringToNull - * @see MapCell::$convertEmptyStringToNull + * @throws MappingFailed * - * Disables converting empty string to the null value. + * @return array */ - public static function disallowEmptyStringAsNull(): void + private function setPropertySetters(array $propertyNames): array { - self::$convertEmptyStringToNull = false; + $propertySetters = []; + $methodNames = array_map(fn (string $propertyName) => 'set'.ucfirst($propertyName), $propertyNames); + + foreach ([...$this->properties, ...$this->class->getMethods()] as $accessor) { + $attributes = $accessor->getAttributes(MapCell::class, ReflectionAttribute::IS_INSTANCEOF); + $propertySetter = match (count($attributes)) { + 0 => $this->autoDiscoverPropertySetter($accessor, $propertyNames, $methodNames), + 1 => $this->findPropertySetter($attributes[0]->newInstance(), $accessor, $propertyNames), + default => throw new MappingFailed('Using more than one `'.MapCell::class.'` attribute on a class property or method is not supported.'), + }; + if (null !== $propertySetter) { + $propertySetters[] = $propertySetter; + } + } + + return match ([]) { + $propertySetters => throw new MappingFailed('No property or method from `'.$this->class->getName().'` could be used for denormalization.'), + default => $propertySetters, + }; + } + + /** + * @return array + */ + private function setAfterMappingCalls(): array + { + return $this->mapRecord?->afterMappingMethods($this->class) + ?? AfterMapping::from($this->class)?->mapRecord->afterMappingMethods($this->class) /* @phpstan-ignore-line */ + ?? []; } /** @@ -95,8 +137,6 @@ public static function registerType(string $type, Closure $callback): void /** * Unregister a global type conversion callback to convert a field into a specific type. - * - * */ public static function unregisterType(string $type): bool { @@ -156,37 +196,6 @@ public static function supportsAlias(string $alias): bool return CallbackCasting::supportsAlias($alias); } - /** - * @param class-string $className - * @param array $record - * - * @throws DenormalizationFailed - * @throws MappingFailed - * @throws ReflectionException - * @throws TypeCastingFailed - */ - public static function assign(string $className, array $record): object - { - return (new self($className, array_keys($record)))->denormalize($record); - } - - /** - * @param class-string $className - * @param array $propertyNames - * - * @throws MappingFailed - * @throws TypeCastingFailed - */ - public static function assignAll(string $className, iterable $records, array $propertyNames = []): Iterator - { - return (new self($className, $propertyNames))->denormalizeAll($records); - } - - public function denormalizeAll(iterable $records): Iterator - { - return MapIterator::fromIterable($records, $this->denormalize(...)); - } - /** * @throws DenormalizationFailed * @throws ReflectionException @@ -212,62 +221,6 @@ public function denormalize(array $record): object return $object; } - /** - * @param class-string $className - * - * @throws MappingFailed - */ - private function setClass(string $className): ReflectionClass - { - class_exists($className) || throw new MappingFailed('The class `'.$className.'` can not be denormalized; The class does not exist or could not be found.'); - - $class = new ReflectionClass($className); - if ($class->isInternal() && $class->isFinal()) { - throw new MappingFailed('The class `'.$className.'` can not be denormalized; PHP internal class marked as final can not be instantiated without using the constructor.'); - } - - return $class; - } - - /** - * @param array $propertyNames - * - * @throws MappingFailed - * - * @return array - */ - private function setPropertySetters(array $propertyNames): array - { - $propertySetters = []; - $methodNames = array_map(fn (string $propertyName) => 'set'.ucfirst($propertyName), $propertyNames); - - foreach ([...$this->properties, ...$this->class->getMethods()] as $accessor) { - $attributes = $accessor->getAttributes(MapCell::class, ReflectionAttribute::IS_INSTANCEOF); - $propertySetter = match (count($attributes)) { - 0 => $this->autoDiscoverPropertySetter($accessor, $propertyNames, $methodNames), - 1 => $this->findPropertySetter($attributes[0]->newInstance(), $accessor, $propertyNames), - default => throw new MappingFailed('Using more than one `'.MapCell::class.'` attribute on a class property or method is not supported.'), - }; - if (null !== $propertySetter) { - $propertySetters[] = $propertySetter; - } - } - - return match ([]) { - $propertySetters => throw new MappingFailed('No property or method from `'.$this->class->getName().'` could be used for denormalization.'), - default => $propertySetters, - }; - } - /** - * @return array - */ - private function setAfterMappingCalls(): array - { - return $this->mapRecord?->afterMappingMethods($this->class) - ?? AfterMapping::from($this->class)?->mapRecord->afterMappingMethods($this->class) /* @phpstan-ignore-line */ - ?? []; - } - /** * @param array $propertyNames * @param array $methodNames @@ -429,7 +382,7 @@ private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $refl } } - public function resolveTypeCaster(MapCell $mapCell, ReflectionMethod|ReflectionProperty $accessor): ?string + private function resolveTypeCaster(MapCell $mapCell, ReflectionMethod|ReflectionProperty $accessor): ?string { /** @var ?class-string $typeCaster */ $typeCaster = $mapCell->cast; @@ -455,4 +408,96 @@ public function resolveTypeCaster(MapCell $mapCell, ReflectionMethod|ReflectionP return CallbackCasting::class.$typeCaster; } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @codeCoverageIgnore + * + * @deprecated since version 9.17.0 + * + * @see MapRecord::$convertEmptyStringToNull + * @see MapCell::$convertEmptyStringToNull + * + * Enables converting empty string to the null value. + */ + public static function allowEmptyStringAsNull(): void + { + self::$convertEmptyStringToNull = true; + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @codeCoverageIgnore + * + * @deprecated since version 9.17.0 + * + * @see MapRecord::$convertEmptyStringToNull + * @see MapCell::$convertEmptyStringToNull + * + * Disables converting empty string to the null value. + */ + public static function disallowEmptyStringAsNull(): void + { + self::$convertEmptyStringToNull = false; + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @codeCoverageIgnore + * + * @param class-string $className + * @param array $record + * + * @throws DenormalizationFailed + * @throws MappingFailed + * @throws ReflectionException + * @throws TypeCastingFailed + *@deprecated since 9.19 + * @see Hydrator::hydrate() + * + */ + public static function assign(string $className, array $record): object + { + return (new self($className, array_keys($record)))->denormalize($record); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @codeCoverageIgnore + * + * @deprecated since 9.19 + * @see Hydrator::hydrateAll() + * + * @param class-string $className + * @param array $propertyNames + * + * @throws MappingFailed + * @throws TypeCastingFailed + */ + public static function assignAll(string $className, iterable $records, array $propertyNames = []): Iterator + { + $denormalizer = new self($className, $propertyNames); + + return MapIterator::fromIterable($records, $denormalizer->denormalize(...)); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @codeCoverageIgnore + * + * @deprecated since 9.19 + * @see Hydrator::hydrateAll() + * + * @throws MappingFailed + * @throws TypeCastingFailed + */ + public function denormalizeAll(iterable $records): Iterator + { + return MapIterator::fromIterable($records, $this->denormalize(...)); + } } diff --git a/src/Serializer/DenormalizerFactory.php b/src/Serializer/DenormalizerFactory.php new file mode 100644 index 00000000..dae126ea --- /dev/null +++ b/src/Serializer/DenormalizerFactory.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Csv\Serializer; + +interface DenormalizerFactory +{ + /** + * @param class-string $class + * @param array $header + */ + public function newDenormalizer(string $class, array $header = []): TabularDataDenormalizer; +} diff --git a/src/Serializer/DenormalizerTest.php b/src/Serializer/DenormalizerTest.php index b0fb4db6..d9ae3741 100644 --- a/src/Serializer/DenormalizerTest.php +++ b/src/Serializer/DenormalizerTest.php @@ -18,6 +18,7 @@ use DateTimeImmutable; use DateTimeInterface; use DateTimeZone; +use League\Csv\Hydrator; use League\Csv\Reader; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -56,7 +57,7 @@ public function __construct( } }; - $results = [...Denormalizer::assignAll($class::class, $records, ['date', 'temperature', 'place'])]; + $results = [...(new Hydrator())->hydrateAll($class::class, $records)]; self::assertCount(2, $results); foreach ($results as $result) { self::assertInstanceOf($class::class, $result); @@ -84,7 +85,7 @@ public function __construct( } }; - $weather = Denormalizer::assign($class::class, $record); + $weather = (new Hydrator())->hydrate($class::class, $record); self::assertInstanceOf($class::class, $weather); self::assertInstanceOf(DateTimeImmutable::class, $weather->observedOn); @@ -112,7 +113,7 @@ public function __construct( } }; - $weather = Denormalizer::assign($foobar::class, $record); + $weather = (new Hydrator())->hydrate($foobar::class, $record); self::assertInstanceOf($foobar::class, $weather); self::assertInstanceOf(DateTimeImmutable::class, $weather->observedOn); @@ -156,7 +157,7 @@ public function getObservedOn(): DateTime 'place' => 'Abidjan', ]; - $weather = Denormalizer::assign($class::class, $record); + $weather = (new Hydrator())->hydrate($class::class, $record); self::assertInstanceOf($class::class, $weather); self::assertSame('2023-10-30', $weather->getObservedOn()->format('Y-m-d')); @@ -169,7 +170,7 @@ public function testMappingFailBecauseTheRecordAttributeIsMissing(): void $this->expectException(MappingFailed::class); $this->expectExceptionMessage('No property or method from `stdClass` could be used for denormalization.'); - Denormalizer::assign(stdClass::class, ['foo' => 'bar']); + (new Hydrator())->hydrate(stdClass::class, ['foo' => 'bar']); } public function testItWillThrowIfTheHeaderIsMissingAndTheColumnOffsetIsAString(): void @@ -309,7 +310,7 @@ public function __construct( } }; - $instance = Denormalizer::assign($foobar::class, ['temperature' => '1', 'place' => 'Abidjan', 'observedOn' => '2023-10-23']); + $instance = (new Hydrator())->hydrate($foobar::class, ['temperature' => '1', 'place' => 'Abidjan', 'observedOn' => '2023-10-23']); self::assertInstanceOf($foobar::class, $instance); self::assertSame(1.0, $instance->temperature); @@ -350,7 +351,7 @@ private function addOne(): void }; /** @var object{addition: int} $res */ - $res = Denormalizer::assign($usingAfterMapping::class, ['addition' => '1']); + $res = (new Hydrator())->hydrate($usingAfterMapping::class, ['addition' => '1']); self::assertSame(2, $res->addition); } @@ -372,7 +373,7 @@ private function addOne(): void $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The method `addTow` is not defined on the `'.$missingMethodAfterMapping::class.'` class.'); - Denormalizer::assign($missingMethodAfterMapping::class, ['addition' => '1']); + (new Hydrator())->hydrate($missingMethodAfterMapping::class, ['addition' => '1']); } public function testIfFailsToUseAfterMappingWithInvalidArgument(): void @@ -392,7 +393,7 @@ private function addOne(int $add): void $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The method `'.$requiresArgumentAfterMapping::class.'::addOne` has too many required parameters.'); - Denormalizer::assign($requiresArgumentAfterMapping::class, ['addition' => '1']); + (new Hydrator())->hydrate($requiresArgumentAfterMapping::class, ['addition' => '1']); } public function testItWillThrowIfTheClassContainsUninitializedProperties(): void @@ -416,7 +417,7 @@ public function getNombre(): int $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The property type definition for `'.$foobar::class.'::annee` is missing; register it using the `'.Denormalizer::class.'` class.'); - Denormalizer::assign( + (new Hydrator())->hydrate( $foobar::class, ['prenoms' => 'John', 'nombre' => '42', 'sexe' => 'M', 'annee' => '2018'] ); @@ -431,7 +432,7 @@ public function testItCanNotAutodiscoverWithIntersectionType(): void $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The property type definition for `'.$foobar::class.'::traversable` is missing; register it using the `'.Denormalizer::class.'` class.'); - Denormalizer::assign($foobar::class, ['traversable' => '1']); + (new Hydrator())->hydrate($foobar::class, ['traversable' => '1']); } public function testItWillThrowIfThePropertyIsMisMatchWithTheTypeCastingClass(): void @@ -453,7 +454,7 @@ public function getFirstName(): string $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The type for the method `'.$foobar::class.'::setFirstName` first argument `firstName` is invalid; `DateTimeInterface` or `mixed` type must be used with the `League\Csv\Serializer\CastToDate`.'); - Denormalizer::assign($foobar::class, ['firstName' => 'john']); + (new Hydrator())->hydrate($foobar::class, ['firstName' => 'john']); } public function testItCanUseTheClosureRegisteringMechanism(): void @@ -465,11 +466,11 @@ public function testItCanUseTheClosureRegisteringMechanism(): void Denormalizer::registerType('string', fn (?string $value) => 'yolo!'); - self::assertSame('yolo!', Denormalizer::assign($foobar::class, $record)->foo); /* @phpstan-ignore-line */ + self::assertSame('yolo!', (new Hydrator())->hydrate($foobar::class, $record)->foo); /* @phpstan-ignore-line */ Denormalizer::unregisterType('string'); - self::assertSame('toto', Denormalizer::assign($foobar::class, $record)->foo); + self::assertSame('toto', (new Hydrator())->hydrate($foobar::class, $record)->foo); } public function testItFailsToRegisterUnknownType(): void @@ -490,11 +491,11 @@ public function testEmptyStringHandling(): void Denormalizer::disallowEmptyStringAsNull(); /* @phpstan-ignore-line */ - self::assertSame('', Denormalizer::assign($foobar::class, $record)->foo); /* @phpstan-ignore-line */ + self::assertSame('', (new Hydrator())->hydrate($foobar::class, $record)->foo); /* @phpstan-ignore-line */ Denormalizer::allowEmptyStringAsNull(); /* @phpstan-ignore-line */ - self::assertNull(Denormalizer::assign($foobar::class, $record)->foo); + self::assertNull((new Hydrator())->hydrate($foobar::class, $record)->foo); } public function testResolvesMethodWithUntypedParameterToStringByDefaultUsingCell(): void @@ -513,8 +514,8 @@ public function getFoobar(): ?string } }; - $instance = Denormalizer::assign($class::class, ['foobar' => 'barbaz']); - $instance1 = Denormalizer::assign($class::class, ['foobar' => null]); + $instance = (new Hydrator())->hydrate($class::class, ['foobar' => 'barbaz']); + $instance1 = (new Hydrator())->hydrate($class::class, ['foobar' => null]); self::assertInstanceOf($class::class, $instance); self::assertSame('barbaz', $instance->getFoobar()); @@ -539,7 +540,7 @@ public function getDate(): DateTimeInterface } }; - $object = Denormalizer::assign($class::class, ['date' => 'tomorrow']); + $object = (new Hydrator())->hydrate($class::class, ['date' => 'tomorrow']); self::assertInstanceOf($class::class, $object); self::assertEquals(new DateTimeZone('Africa/Abidjan'), $object->getDate()->getTimezone()); } @@ -563,7 +564,7 @@ public function getDate(): DateTimeInterface $this->expectException(MappingFailed::class); $this->expectExceptionMessage('No property or method from `'.$class::class.'` could be used for denormalization.'); - Denormalizer::assign($class::class, ['date' => 'tomorrow']); + (new Hydrator())->hydrate($class::class, ['date' => 'tomorrow']); } #[Test] @@ -585,7 +586,7 @@ public function __construct( } }; - $instance = Denormalizer::assign($class::class, ['str' => 'kinshasa']); + $instance = (new Hydrator())->hydrate($class::class, ['str' => 'kinshasa']); self::assertInstanceOf($class::class, $instance); self::assertSame('KINSHASA', $instance->str); @@ -595,7 +596,7 @@ public function __construct( $this->expectException(MappingFailed::class); $this->expectExceptionMessage('`@strtoupper` must be an resolvable class implementing the `'.TypeCasting::class.'` interface or a supported alias.'); - Denormalizer::assign($class::class, ['str' => 'kinshasa']); + (new Hydrator())->hydrate($class::class, ['str' => 'kinshasa']); } #[Test] @@ -637,7 +638,7 @@ public function __construct( } }; - $instance = Denormalizer::assign($class::class, ['place' => 'YaMouSSokro']); + $instance = (new Hydrator())->hydrate($class::class, ['place' => 'YaMouSSokro']); self::assertInstanceOf($class::class, $instance); self::assertSame('yamoussokro', $instance->str); } @@ -655,7 +656,7 @@ public function setObservedOn(DateTimeInterface $observedOn): void } }; - $instance = Denormalizer::assign($classIgnoreMethod::class, ['observedOn' => '2023-10-01']); + $instance = (new Hydrator())->hydrate($classIgnoreMethod::class, ['observedOn' => '2023-10-01']); self::assertInstanceOf($classIgnoreMethod::class, $instance); self::assertInstanceOf(DateTimeImmutable::class, $instance->observedOn); @@ -670,7 +671,7 @@ public function setObservedOn(DateTimeInterface $observedOn): void } }; - $instance = Denormalizer::assign($classIgnoreProperty::class, ['observedOn' => '2023-10-01']); + $instance = (new Hydrator())->hydrate($classIgnoreProperty::class, ['observedOn' => '2023-10-01']); self::assertInstanceOf($classIgnoreProperty::class, $instance); self::assertInstanceOf(DateTime::class, $instance->observedOn); @@ -703,7 +704,7 @@ public function it_will_fails_if_the_property_is_missing_from_source(): void $this->expectException(DenormalizationFailed::class); $this->expectExceptionMessage('The property '.$class::class.'::bar is not initialized; its value is missing from the source data.'); - Denormalizer::assign($class::class, $data); + (new Hydrator())->hydrate($class::class, $data); } #[Test] diff --git a/src/Serializer/Factory.php b/src/Serializer/Factory.php new file mode 100644 index 00000000..2a624c87 --- /dev/null +++ b/src/Serializer/Factory.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Csv\Serializer; + +final class Factory implements DenormalizerFactory +{ + /** + * @param class-string $class + * @param array $header + */ + public function newDenormalizer(string $class, array $header = []): TabularDataDenormalizer + { + return new Denormalizer($class, $header); + } +} diff --git a/src/Serializer/TabularDataDenormalizer.php b/src/Serializer/TabularDataDenormalizer.php new file mode 100644 index 00000000..a2da6e65 --- /dev/null +++ b/src/Serializer/TabularDataDenormalizer.php @@ -0,0 +1,31 @@ + $propertyNames + * + * @return void + */ + public function initialize(string $className, array $propertyNames = []): void; + + /** + * @throws DenormalizationFailed + * @throws ReflectionException + * @throws TypeCastingFailed + * + * @return T + */ + public function denormalize(array $record): object; +} diff --git a/src/TabularDataHydrator.php b/src/TabularDataHydrator.php new file mode 100644 index 00000000..4d3438e2 --- /dev/null +++ b/src/TabularDataHydrator.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Csv; + +use Iterator; +use League\Csv\Serializer\DenormalizationFailed; +use League\Csv\Serializer\TypeCastingFailed; +use ReflectionException; + +/** + * @template TClass of object + */ +interface TabularDataHydrator +{ + /** + * Hydrates a signle record into an object of a given class. + * + * @param class-string $className + * + * @throws DenormalizationFailed + * @throws ReflectionException + * @throws TypeCastingFailed + * + * @return TClass + */ + public function hydrate(string $className, array $record): object; + + /** + * Hydrates multiple records into objects of a given class. + * + * @param class-string $className + * @param iterable $records + * + * @throws DenormalizationFailed + * @throws ReflectionException + * @throws TypeCastingFailed + * + * @return Iterator + */ + public function hydrateAll(string $className, iterable $records): Iterator; +}