Skip to content

Commit

Permalink
feat: improve object constructors parameters types inferring
Browse files Browse the repository at this point in the history
The collision system that checks object constructors parameters types is
now way more clever, as it no longer checks for parameters' names only.
Types are now also checked, and only true collision will be detected,
for instance when two constructors share a parameter with the same name
and type.

Note that when two parameters share the same name, the following type
priority operates:

1. Non-scalar type
2. Integer type
3. Float type
4. String type
5. Boolean type

With this change, the code below is now valid:

```php
final readonly class Money
{
    private function __construct(
        public int $value,
    ) {}

    #[\CuyZ\Valinor\Mapper\Object\Constructor]
    public static function fromInt(int $value): self
    {
        return new self($value);
    }

    #[\CuyZ\Valinor\Mapper\Object\Constructor]
    public static function fromString(string $value): self
    {
        if (! preg_match('/^\d+€$/', $value)) {
            throw new \InvalidArgumentException('Invalid money format');
        }

        return new self((int)rtrim($value, '€'));
    }
}

$mapper = (new \CuyZ\Valinor\MapperBuilder())->mapper();

$mapper->map(Money::class, 42); // ✅
$mapper->map(Money::class, '42€'); // ✅
```
  • Loading branch information
romm committed Sep 2, 2024
1 parent 0479532 commit 2150dca
Show file tree
Hide file tree
Showing 24 changed files with 582 additions and 524 deletions.
10 changes: 2 additions & 8 deletions src/Library/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use CuyZ\Valinor\Definition\Repository\Reflection\ReflectionFunctionDefinitionRepository;
use CuyZ\Valinor\Mapper\ArgumentsMapper;
use CuyZ\Valinor\Mapper\Object\Factory\CacheObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Object\Factory\CollisionObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Object\Factory\SortingObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Object\Factory\ConstructorObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Object\Factory\DateTimeObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Object\Factory\DateTimeZoneObjectBuilderFactory;
Expand All @@ -37,7 +37,6 @@
use CuyZ\Valinor\Mapper\Tree\Builder\NodeBuilder;
use CuyZ\Valinor\Mapper\Tree\Builder\NullNodeBuilder;
use CuyZ\Valinor\Mapper\Tree\Builder\ObjectImplementations;
use CuyZ\Valinor\Mapper\Tree\Builder\FilteredObjectNodeBuilder;
use CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder;
use CuyZ\Valinor\Mapper\Tree\Builder\ScalarNodeBuilder;
use CuyZ\Valinor\Mapper\Tree\Builder\ShapedArrayNodeBuilder;
Expand Down Expand Up @@ -116,7 +115,6 @@ public function __construct(Settings $settings)
ObjectType::class => new ObjectNodeBuilder(
$this->get(ClassDefinitionRepository::class),
$this->get(ObjectBuilderFactory::class),
$this->get(FilteredObjectNodeBuilder::class),
),
]);

Expand All @@ -126,8 +124,6 @@ public function __construct(Settings $settings)
$builder,
$this->get(ObjectImplementations::class),
$this->get(ClassDefinitionRepository::class),
$this->get(ObjectBuilderFactory::class),
$this->get(FilteredObjectNodeBuilder::class),
new FunctionsContainer(
$this->get(FunctionDefinitionRepository::class),
$settings->customConstructors
Expand All @@ -152,8 +148,6 @@ public function __construct(Settings $settings)
return new ErrorCatcherNodeBuilder($builder, $settings->exceptionFilter);
},

FilteredObjectNodeBuilder::class => fn () => new FilteredObjectNodeBuilder(),

ObjectImplementations::class => fn () => new ObjectImplementations(
new FunctionsContainer(
$this->get(FunctionDefinitionRepository::class),
Expand All @@ -172,7 +166,7 @@ public function __construct(Settings $settings)
$factory = new ConstructorObjectBuilderFactory($factory, $settings->nativeConstructors, $constructors);
$factory = new DateTimeZoneObjectBuilderFactory($factory, $this->get(FunctionDefinitionRepository::class));
$factory = new DateTimeObjectBuilderFactory($factory, $settings->supportedDateFormats, $this->get(FunctionDefinitionRepository::class));
$factory = new CollisionObjectBuilderFactory($factory);
$factory = new SortingObjectBuilderFactory($factory);

if (! $settings->allowPermissiveTypes) {
$factory = new StrictTypesObjectBuilderFactory($factory);
Expand Down
35 changes: 22 additions & 13 deletions src/Mapper/Object/Arguments.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
use IteratorAggregate;
use Traversable;

use function array_keys;
use function array_map;
use function array_values;
use function count;

/**
* @internal
Expand All @@ -22,44 +24,51 @@
*/
final class Arguments implements IteratorAggregate, Countable
{
/** @var Argument[] */
private array $arguments;
/** @var array<string, Argument> */
private array $arguments = [];

public function __construct(Argument ...$arguments)
{
$this->arguments = $arguments;
foreach ($arguments as $argument) {
$this->arguments[$argument->name()] = $argument;
}
}

public static function fromParameters(Parameters $parameters): self
{
return new self(...array_map(
fn (ParameterDefinition $parameter) => Argument::fromParameter($parameter),
array_values([...$parameters])
[...$parameters],
));
}

public static function fromProperties(Properties $properties): self
{
return new self(...array_map(
fn (PropertyDefinition $property) => Argument::fromProperty($property),
array_values([...$properties])
[...$properties],
));
}

public function at(int $index): Argument
{
return $this->arguments[$index];
return array_values($this->arguments)[$index];
}

public function has(string $name): bool
/**
* @return list<string>
*/
public function names(): array
{
foreach ($this->arguments as $argument) {
if ($argument->name() === $name) {
return true;
}
}
return array_keys($this->arguments);
}

return false;
/**
* @return array<string, Argument>
*/
public function toArray(): array
{
return $this->arguments;
}

public function count(): int
Expand Down
26 changes: 15 additions & 11 deletions src/Mapper/Object/ArgumentsValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace CuyZ\Valinor\Mapper\Object;

use CuyZ\Valinor\Mapper\Object\Exception\InvalidSource;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
Expand All @@ -27,6 +26,8 @@ final class ArgumentsValues implements IteratorAggregate

private Arguments $arguments;

private bool $hasInvalidValue = false;

private bool $forInterface = false;

private bool $hadSingleArgument = false;
Expand All @@ -42,7 +43,7 @@ public static function forInterface(Arguments $arguments, Shell $shell): self
$self->forInterface = true;

Check warning on line 43 in src/Mapper/Object/ArgumentsValues.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ public static function forInterface(Arguments $arguments, Shell $shell) : self { $self = new self($arguments); - $self->forInterface = true; + $self->forInterface = false; if (count($arguments) > 0) { $self->transform($shell); }

if (count($arguments) > 0) {
$self = $self->transform($shell);
$self->transform($shell);
}

return $self;
Expand All @@ -51,11 +52,16 @@ public static function forInterface(Arguments $arguments, Shell $shell): self
public static function forClass(Arguments $arguments, Shell $shell): self
{
$self = new self($arguments);
$self = $self->transform($shell);
$self->transform($shell);

return $self;
}

public function hasInvalidValue(): bool
{
return $this->hasInvalidValue;
}

public function hasValue(string $name): bool
{
return array_key_exists($name, $this->value);
Expand All @@ -71,18 +77,18 @@ public function hadSingleArgument(): bool
return $this->hadSingleArgument;
}

private function transform(Shell $shell): self
private function transform(Shell $shell): void
{
$clone = clone $this;

$transformedValue = $this->transformValueForSingleArgument($shell);

if (! is_array($transformedValue)) {
throw new InvalidSource($transformedValue, $this->arguments);
$this->hasInvalidValue = true;

return;
}

if ($transformedValue !== $shell->value()) {
$clone->hadSingleArgument = true;
$this->hadSingleArgument = true;
}

foreach ($this->arguments as $argument) {
Expand All @@ -93,9 +99,7 @@ private function transform(Shell $shell): self
}
}

$clone->value = $transformedValue;

return $clone;
$this->value = $transformedValue;
}

private function transformValueForSingleArgument(Shell $shell): mixed
Expand Down
11 changes: 2 additions & 9 deletions src/Mapper/Object/Exception/ObjectBuildersCollision.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,16 @@

namespace CuyZ\Valinor\Mapper\Object\Exception;

use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
use RuntimeException;

use function array_map;
use function implode;

/** @internal */
final class ObjectBuildersCollision extends RuntimeException
{
public function __construct(ClassDefinition $class, ObjectBuilder ...$builders)
public function __construct(ObjectBuilder $builderA, ObjectBuilder $builderB)
{
$constructors = array_map(fn (ObjectBuilder $builder) => $builder->signature(), $builders);
$constructors = implode('`, `', $constructors);

parent::__construct(
"A collision was detected between the following constructors of the class `{$class->type->toString()}`: `$constructors`.",
"A type collision was detected between the constructors `{$builderA->signature()}` and `{$builderB->signature()}`.",
1654955787
);
}
Expand Down
24 changes: 0 additions & 24 deletions src/Mapper/Object/Exception/SeveralObjectBuildersFound.php

This file was deleted.

69 changes: 0 additions & 69 deletions src/Mapper/Object/Factory/CollisionObjectBuilderFactory.php

This file was deleted.

1 change: 1 addition & 0 deletions src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function for(ClassDefinition $class): array
$builders[] = $this->internalDateTimeBuilder($class->type);
}

/** @var non-empty-list<ObjectBuilder> */
return $builders;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public function for(ClassDefinition $class): array
$builders[] = $this->defaultBuilder($class->type);
}

/** @var non-empty-list<ObjectBuilder> */
return $builders;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Mapper/Object/Factory/ObjectBuilderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
interface ObjectBuilderFactory
{
/**
* @return list<ObjectBuilder>
* @return non-empty-list<ObjectBuilder>
*/
public function for(ClassDefinition $class): array;
}
5 changes: 0 additions & 5 deletions src/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,12 @@

use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Mapper\Object\ReflectionObjectBuilder;
use CuyZ\Valinor\Utility\Reflection\Reflection;

/** @internal */
final class ReflectionObjectBuilderFactory implements ObjectBuilderFactory
{
public function for(ClassDefinition $class): array
{
if (Reflection::enumExists($class->name)) {
return [];
}

return [new ReflectionObjectBuilder($class)];
}
}
Loading

0 comments on commit 2150dca

Please sign in to comment.