Skip to content

Commit

Permalink
feat: add generic type to normalizer and use external formatter
Browse files Browse the repository at this point in the history
  • Loading branch information
romm committed Dec 22, 2023
1 parent 3f90c98 commit a0f89e5
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 33 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ providing precise and human-readable error messages.
The mapper can handle native PHP types as well as other advanced types supported
by [PHPStan] and [Psalm] like shaped arrays, generics, integer ranges and more.

The library also provides a normalization mechanism that can help transform any
input into a data format (JSON, CSV, …), while preserving the original
structure.

## Installation

```bash
Expand Down
20 changes: 16 additions & 4 deletions src/Library/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
use CuyZ\Valinor\Mapper\TreeMapper;
use CuyZ\Valinor\Mapper\TypeArgumentsMapper;
use CuyZ\Valinor\Mapper\TypeTreeMapper;
use CuyZ\Valinor\Normalizer\FormatNormalizer;
use CuyZ\Valinor\Normalizer\Formatter\Formatter;
use CuyZ\Valinor\Normalizer\Formatter\FormatterFactory;
use CuyZ\Valinor\Normalizer\Normalizer;
use CuyZ\Valinor\Normalizer\RecursiveNormalizer;
use CuyZ\Valinor\Normalizer\Transformer\KeyTransformersHandler;
Expand Down Expand Up @@ -181,7 +184,7 @@ public function __construct(Settings $settings)
return new CacheObjectBuilderFactory($factory, $cache);
},

Normalizer::class => fn () => new RecursiveNormalizer(
RecursiveNormalizer::class => fn () => new RecursiveNormalizer(
$this->get(ClassDefinitionRepository::class),
new ValueTransformersHandler(
$this->get(FunctionDefinitionRepository::class),
Expand All @@ -198,7 +201,7 @@ public function __construct(Settings $settings)
$this->get(TypeParserFactory::class),
$this->get(AttributesRepository::class),
),
$this->get(CacheInterface::class)
$this->get(CacheInterface::class),
),

FunctionDefinitionRepository::class => fn () => new CacheFunctionDefinitionRepository(
Expand Down Expand Up @@ -245,9 +248,18 @@ public function argumentsMapper(): ArgumentsMapper
return $this->get(ArgumentsMapper::class);
}

public function normalizer(): Normalizer
/**
* @template T
*
* @param FormatterFactory<Formatter<T>> $formatterFactory
* @return Normalizer<T>
*/
public function normalizer(FormatterFactory $formatterFactory): Normalizer
{
return $this->get(Normalizer::class);
return new FormatNormalizer(
$formatterFactory,
$this->get(RecursiveNormalizer::class),
);
}

public function cacheWarmupService(): RecursiveCacheWarmupService
Expand Down
13 changes: 10 additions & 3 deletions src/MapperBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
use CuyZ\Valinor\Mapper\ArgumentsMapper;
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
use CuyZ\Valinor\Mapper\TreeMapper;
use CuyZ\Valinor\Normalizer\Format;
use CuyZ\Valinor\Normalizer\Formatter\Formatter;
use CuyZ\Valinor\Normalizer\Formatter\FormatterFactory;
use CuyZ\Valinor\Normalizer\Normalizer;
use Psr\SimpleCache\CacheInterface;
use Throwable;
Expand Down Expand Up @@ -550,9 +551,15 @@ public function argumentsMapper(): ArgumentsMapper
return $this->container()->argumentsMapper();
}

public function normalizer(Format $format): Normalizer
/**
* @template T
*
* @param FormatterFactory<Formatter<T>> $formatterFactory
* @return Normalizer<T>
*/
public function normalizer(FormatterFactory $formatterFactory): Normalizer
{
return $this->container()->normalizer();
return $this->container()->normalizer($formatterFactory);
}

public function __clone()
Expand Down
9 changes: 7 additions & 2 deletions src/Normalizer/Format.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

namespace CuyZ\Valinor\Normalizer;

use CuyZ\Valinor\Normalizer\Formatter\ArrayFormatterFactory;

/** @api */
final class Format
{
public static function array(): self
/**
* @todo doc
*/
public static function array(): ArrayFormatterFactory
{
return new self();
return new ArrayFormatterFactory();
}
}
30 changes: 30 additions & 0 deletions src/Normalizer/FormatNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Normalizer;

use CuyZ\Valinor\Normalizer\Formatter\Formatter;
use CuyZ\Valinor\Normalizer\Formatter\FormatterFactory;

/**
* @internal
*
* @template T
* @implements Normalizer<T>
*/
final class FormatNormalizer implements Normalizer
{
/**
* @param FormatterFactory<Formatter<T>> $formatterFactory
*/
public function __construct(
private FormatterFactory $formatterFactory,
private RecursiveNormalizer $recursiveNormalizer,
) {}

public function normalize(mixed $value): mixed
{
return $this->recursiveNormalizer->normalize($value, $this->formatterFactory->new());
}
}
34 changes: 34 additions & 0 deletions src/Normalizer/Formatter/ArrayFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Normalizer\Formatter;

use function is_array;
use function is_iterable;
use function iterator_to_array;

/**
* @internal
*
* @implements Formatter<array<mixed>|scalar|null>
*/
final class ArrayFormatter implements Formatter
{
/** @var iterable<mixed>|scalar|null */
private mixed $value;

public function push(mixed $value): void
{
$this->value = $value;
}

public function value(): mixed
{
if (is_iterable($this->value) && ! is_array($this->value)) {
$this->value = iterator_to_array($this->value);
}

return $this->value;
}
}
18 changes: 18 additions & 0 deletions src/Normalizer/Formatter/ArrayFormatterFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Normalizer\Formatter;

/**
* @internal
*
* @implements FormatterFactory<ArrayFormatter>
*/
final class ArrayFormatterFactory implements FormatterFactory
{
public function new(): ArrayFormatter
{
return new ArrayFormatter();
}
}
23 changes: 23 additions & 0 deletions src/Normalizer/Formatter/Formatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Normalizer\Formatter;

/**
* @internal
*
* @template-covariant T
*/
interface Formatter
{
/**
* @param iterable<mixed>|scalar|null $value
*/
public function push(mixed $value): void;

/**
* @return T
*/
public function value(): mixed;
}
18 changes: 18 additions & 0 deletions src/Normalizer/Formatter/FormatterFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Normalizer\Formatter;

/**
* @internal
*
* @template-covariant T of Formatter
*/
interface FormatterFactory
{
/**
* @return T
*/
public function new(): Formatter;
}
15 changes: 13 additions & 2 deletions src/Normalizer/Normalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@

namespace CuyZ\Valinor\Normalizer;

/** @api */
/**
* @api
*
* @template-covariant T
*/
interface Normalizer
{
/**
* @todo doc
* A normalizer is a service that transforms a given input into scalar and
* array values, while preserving the original structure.
*
* This feature can be used to share information with other systems that use
* a data format (JSON, CSV, XML, etc.). The normalizer will take care of
* recursively transforming the data into a format that can be serialized.
*
* @return T
*/
public function normalize(mixed $value): mixed;
}
53 changes: 31 additions & 22 deletions src/Normalizer/RecursiveNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Normalizer\Exception\CircularReferenceFoundDuringNormalization;
use CuyZ\Valinor\Normalizer\Exception\TypeUnhandledByNormalizer;
use CuyZ\Valinor\Normalizer\Formatter\Formatter;
use CuyZ\Valinor\Normalizer\Transformer\KeyTransformersHandler;
use CuyZ\Valinor\Normalizer\Transformer\ValueTransformersHandler;
use CuyZ\Valinor\Type\Types\NativeClassType;
Expand All @@ -21,8 +22,8 @@

use function array_map;

/** @internal */
final class RecursiveNormalizer implements Normalizer
/** @internal */
final class RecursiveNormalizer
{
public function __construct(
private ClassDefinitionRepository $classDefinitionRepository,
Expand All @@ -34,16 +35,23 @@ public function __construct(
private array $transformerAttributes,
) {}

public function normalize(mixed $value): mixed
/**
* @param Formatter<mixed> $formatter
*/
public function normalize(mixed $value, Formatter $formatter): mixed
{
return $this->doNormalize($value, new WeakMap()); // @phpstan-ignore-line
$this->doNormalize($value, $formatter, new WeakMap()); // @phpstan-ignore-line

return $formatter->value();
}

/**
* @param Formatter<mixed> $formatter
* @param WeakMap<object, true> $references
* @param list<object> $attributes
* @return iterable<mixed>|scalar|null
*/
private function doNormalize(mixed $value, WeakMap $references, array $attributes = []): mixed
private function doNormalize(mixed $value, Formatter $formatter, WeakMap $references, array $attributes = []): mixed
{
if (is_object($value)) {
if (isset($references[$value])) {
Expand All @@ -55,14 +63,9 @@ private function doNormalize(mixed $value, WeakMap $references, array $attribute
}

if ($this->transformers === [] && $this->transformerAttributes === []) {
$value = $this->defaultTransformer($value, $references);
$value = $this->defaultTransformer($value, $formatter, $references);

if (is_array($value)) {
$value = array_map(
fn (mixed $value) => $this->doNormalize($value, $references),
$value,
);
}
$formatter->push($value);

return $value;
}
Expand All @@ -73,18 +76,24 @@ private function doNormalize(mixed $value, WeakMap $references, array $attribute
$attributes = [...$attributes, ...$classAttributes];
}

return $this->valueTransformers->transform(
$value = $this->valueTransformers->transform(
$value,
$attributes,
$this->transformers,
fn (mixed $value) => $this->defaultTransformer($value, $references),
fn (mixed $value) => $this->defaultTransformer($value, $formatter, $references),
);

$formatter->push($value);

return $value;
}

/**
* @param Formatter<mixed> $formatter
* @param WeakMap<object, true> $references
* @return iterable<mixed>|scalar|null
*/
private function defaultTransformer(mixed $value, WeakMap $references): mixed
private function defaultTransformer(mixed $value, Formatter $formatter, WeakMap $references): mixed
{
if ($value === null) {
return null;
Expand All @@ -105,7 +114,7 @@ private function defaultTransformer(mixed $value, WeakMap $references): mixed

if ($value::class === stdClass::class) {
return array_map(
fn (mixed $value) => $this->doNormalize($value, $references),
fn (mixed $value) => $this->doNormalize($value, $formatter, $references),
(array)$value
);
}
Expand All @@ -120,18 +129,18 @@ private function defaultTransformer(mixed $value, WeakMap $references): mixed

$key = $this->keyTransformers->transformKey($key, $attributes);

$transformed[$key] = $this->doNormalize($subValue, $references, $attributes);
$transformed[$key] = $this->doNormalize($subValue, $formatter, $references, $attributes);
}

return $transformed;
}

if (is_iterable($value)) {
if (! is_array($value)) {
$value = iterator_to_array($value);
}

return $value;
return (function () use ($value, $formatter, $references) {
foreach ($value as $key => $item) {
yield $key => $this->doNormalize($item, $formatter, $references);
}
})();
}

throw new TypeUnhandledByNormalizer($value);
Expand Down
1 change: 1 addition & 0 deletions src/Normalizer/Transformer/ValueTransformersHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function __construct(
/**
* @param array<object> $attributes
* @param list<callable> $transformers
* @return array<mixed>|scalar|null
*/
public function transform(mixed $value, array $attributes, array $transformers, callable $defaultTransformer): mixed
{
Expand Down
Loading

0 comments on commit a0f89e5

Please sign in to comment.