Skip to content

Commit

Permalink
feat: handle interface constructor registration
Browse files Browse the repository at this point in the history
By default, the mapper cannot instantiate an interface, as it does not
know which implementation to use. To do so, the `MapperBuilder::infer()`
method can be used, but it is cumbersome in most cases.

It is now also possible to register a constructor for an interface, in
the same way as for a class.

Because the mapper cannot automatically guess which implementation can
be used for an interface, it is not possible to use the `Constructor`
attribute, the `MapperBuilder::registerConstructor()` method must be
used instead.

In the example below, the mapper is taught how to instantiate an
implementation of `UuidInterface` from package `ramsey/uuid`:

```php
(new \CuyZ\Valinor\MapperBuilder())
    ->registerConstructor(
        // The static method below has return type `UuidInterface`;
        // therefore, the mapper will build an instance of `Uuid` when
        // it needs to instantiate an implementation of `UuidInterface`.
        Ramsey\Uuid\Uuid::fromString(...)
    )
    ->mapper()
    ->map(
        Ramsey\Uuid\UuidInterface::class,
        '663bafbf-c3b5-4336-b27f-1796be8554e0'
    );
```
  • Loading branch information
romm committed Apr 1, 2024
1 parent f797bf0 commit 13f69a9
Show file tree
Hide file tree
Showing 22 changed files with 206 additions and 49 deletions.
8 changes: 6 additions & 2 deletions docs/pages/how-to/infer-interfaces.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Inferring interfaces

When the mapper meets an interface, it needs to understand which implementation
(a class that implements this interface) will be used — this information must be
provided in the mapper builder, using the method `infer()`.
will be used. This can be done by [registering a constructor for the interface],
but it can have limitations. A more powerful way to handle this is to infer the
implementation based on the data provided to the mapper. This can be done using
the `infer()` method.

The callback given to this method must return the name of a class that
implements the interface. Any arguments can be required by the callback; they
Expand Down Expand Up @@ -124,3 +126,5 @@ assert($result->foo === 'foo');
assert($result->bar === 'bar');
assert($result->baz === 'baz');
```

[registering a constructor for the interface]: use-custom-object-constructors.md#interface-implementation-constructor
31 changes: 31 additions & 0 deletions docs/pages/how-to/use-custom-object-constructors.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,37 @@ final class Color
}
```

## Interface implementation constructor

By default, the mapper cannot instantiate an interface, as it does not know
which implementation to use. To do so, it is possible to register a constructor
for an interface, in the same way as for a class.

!!! note

Because the mapper cannot automatically guess which implementation can be
used for an interface, it is not possible to use the `Constructor`
attribute, the `MapperBuilder::registerConstructor()` method must be used
instead.

In the example below, the mapper is taught how to instantiate an implementation
of `UuidInterface` from package [`ramsey/uuid`](https://github.com/ramsey/uuid):

```php
(new \CuyZ\Valinor\MapperBuilder())
->registerConstructor(
// The static method below has return type `UuidInterface`; therefore,
// the mapper will build an instance of `Uuid` when it needs to
// instantiate an implementation of `UuidInterface`.
Ramsey\Uuid\Uuid::fromString(...)
)
->mapper()
->map(
Ramsey\Uuid\UuidInterface::class,
'663bafbf-c3b5-4336-b27f-1796be8554e0'
);
```

## Custom enum constructor

Registering a constructor for an enum works the same way as for a class, as
Expand Down
4 changes: 2 additions & 2 deletions src/Definition/ClassDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

namespace CuyZ\Valinor\Definition;

use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;

/** @internal */
final class ClassDefinition
{
public function __construct(
/** @var class-string */
public readonly string $name,
public readonly ClassType $type,
public readonly ObjectType $type,
public readonly Attributes $attributes,
public readonly Properties $properties,
public readonly Methods $methods,
Expand Down
4 changes: 2 additions & 2 deletions src/Definition/Exception/InvalidTypeAliasImportClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

namespace CuyZ\Valinor\Definition\Exception;

use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;
use LogicException;

/** @internal */
final class InvalidTypeAliasImportClass extends LogicException
{
public function __construct(ClassType $type, string $className)
public function __construct(ObjectType $type, string $className)
{
parent::__construct(
"Cannot import a type alias from unknown class `$className` in class `{$type->className()}`.",
Expand Down
4 changes: 2 additions & 2 deletions src/Definition/Exception/InvalidTypeAliasImportClassType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

namespace CuyZ\Valinor\Definition\Exception;

use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\ClassType;
use LogicException;

/** @internal */
final class InvalidTypeAliasImportClassType extends LogicException
{
public function __construct(ClassType $classType, Type $type)
public function __construct(ObjectType $classType, Type $type)
{
parent::__construct(
"Importing a type alias can only be done with classes, `{$type->toString()}` was given in class `{$classType->className()}`.",
Expand Down
4 changes: 2 additions & 2 deletions src/Definition/Exception/UnknownTypeAliasImport.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace CuyZ\Valinor\Definition\Exception;

use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;
use LogicException;

/** @internal */
Expand All @@ -13,7 +13,7 @@ final class UnknownTypeAliasImport extends LogicException
/**
* @param class-string $importClassName
*/
public function __construct(ClassType $type, string $importClassName, string $alias)
public function __construct(ObjectType $type, string $importClassName, string $alias)
{
parent::__construct(
"Type alias `$alias` imported in `{$type->className()}` could not be found in `$importClassName`",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;
use Psr\SimpleCache\CacheInterface;

use function sha1;
Expand All @@ -20,7 +20,7 @@ public function __construct(
private CacheInterface $cache
) {}

public function for(ClassType $type): ClassDefinition
public function for(ObjectType $type): ClassDefinition
{
// @infection-ignore-all
$key = 'class-definition' . sha1($type->toString());
Expand Down
4 changes: 2 additions & 2 deletions src/Definition/Repository/ClassDefinitionRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
namespace CuyZ\Valinor\Definition\Repository;

use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;

/** @internal */
interface ClassDefinitionRepository
{
public function for(ClassType $type): ClassDefinition;
public function for(ObjectType $type): ClassDefinition;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
use CuyZ\Valinor\Definition\PropertyDefinition;
use CuyZ\Valinor\Definition\Repository\AttributesRepository;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\GenericType;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
Expand Down Expand Up @@ -67,7 +66,7 @@ public function __construct(TypeParserFactory $typeParserFactory)
$this->methodBuilder = new ReflectionMethodDefinitionBuilder($this->attributesRepository);
}

public function for(ClassType $type): ClassDefinition
public function for(ObjectType $type): ClassDefinition
{
$reflection = Reflection::class($type->className());

Expand Down Expand Up @@ -97,7 +96,7 @@ private function attributes(ReflectionClass $reflection): array
/**
* @return list<PropertyDefinition>
*/
private function properties(ClassType $type): array
private function properties(ObjectType $type): array
{
return array_map(
function (ReflectionProperty $property) use ($type) {
Expand All @@ -112,7 +111,7 @@ function (ReflectionProperty $property) use ($type) {
/**
* @return list<MethodDefinition>
*/
private function methods(ClassType $type): array
private function methods(ObjectType $type): array
{
$reflection = Reflection::class($type->className());
$methods = $reflection->getMethods();
Expand All @@ -134,7 +133,7 @@ private function methods(ClassType $type): array
/**
* @param ReflectionClass<object> $target
*/
private function typeResolver(ClassType $type, ReflectionClass $target): ReflectionTypeResolver
private function typeResolver(ObjectType $type, ReflectionClass $target): ReflectionTypeResolver
{
$typeKey = $target->isInterface()
? "{$type->toString()}/{$type->className()}"
Expand Down Expand Up @@ -209,7 +208,7 @@ private function localTypeAliases(ObjectType $type): array
/**
* @return array<string, Type>
*/
private function importedTypeAliases(ClassType $type): array
private function importedTypeAliases(ObjectType $type): array
{
$reflection = Reflection::class($type->className());
$importedTypesRaw = DocParser::importedTypeAliases($reflection);
Expand Down Expand Up @@ -258,7 +257,7 @@ private function typeParser(ObjectType $type): TypeParser
return $this->typeParserFactory->get(...$specs);
}

private function parentType(ClassType $type): NativeClassType
private function parentType(ObjectType $type): NativeClassType
{
$reflection = Reflection::class($type->className());

Expand Down
16 changes: 10 additions & 6 deletions src/Library/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
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\ObjectNodeBuilder;
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 All @@ -54,7 +54,7 @@
use CuyZ\Valinor\Normalizer\Transformer\KeyTransformersHandler;
use CuyZ\Valinor\Normalizer\Transformer\RecursiveTransformer;
use CuyZ\Valinor\Normalizer\Transformer\ValueTransformersHandler;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\Parser\Factory\LexingTypeParserFactory;
use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
use CuyZ\Valinor\Type\Parser\TypeParser;
Expand Down Expand Up @@ -111,10 +111,10 @@ public function __construct(Settings $settings)
ShapedArrayType::class => new ShapedArrayNodeBuilder($settings->allowSuperfluousKeys),
ScalarType::class => new ScalarNodeBuilder($settings->enableFlexibleCasting),
NullType::class => new NullNodeBuilder(),
ClassType::class => new NativeClassNodeBuilder(
ObjectType::class => new NativeClassNodeBuilder(
$this->get(ClassDefinitionRepository::class),
$this->get(ObjectBuilderFactory::class),
$this->get(ObjectNodeBuilder::class),
$this->get(FilteredObjectNodeBuilder::class),
$settings->enableFlexibleCasting,
),
]);
Expand All @@ -126,7 +126,11 @@ public function __construct(Settings $settings)
$this->get(ObjectImplementations::class),
$this->get(ClassDefinitionRepository::class),
$this->get(ObjectBuilderFactory::class),
$this->get(ObjectNodeBuilder::class),
$this->get(FilteredObjectNodeBuilder::class),
new FunctionsContainer(
$this->get(FunctionDefinitionRepository::class),
$settings->customConstructors
),
$settings->enableFlexibleCasting,
$settings->allowSuperfluousKeys,
);
Expand All @@ -149,7 +153,7 @@ public function __construct(Settings $settings)
return new ErrorCatcherNodeBuilder($builder, $settings->exceptionFilter);
},

ObjectNodeBuilder::class => fn () => new ObjectNodeBuilder($settings->allowSuperfluousKeys),
FilteredObjectNodeBuilder::class => fn () => new FilteredObjectNodeBuilder($settings->allowSuperfluousKeys),

ObjectImplementations::class => fn () => new ObjectImplementations(
new FunctionsContainer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ private function builders(ClassDefinition $class): array
return array_values($builders);
}

private function constructorMatches(FunctionObject $function, ClassType $classType): bool
private function constructorMatches(FunctionObject $function, ObjectType $classType): bool
{
$definition = $function->definition;

Expand Down
4 changes: 2 additions & 2 deletions src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
use CuyZ\Valinor\Mapper\Object\NativeConstructorObjectBuilder;
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;
use DateTime;
use DateTimeImmutable;

Expand Down Expand Up @@ -51,7 +51,7 @@ public function for(ClassDefinition $class): array
return $builders;
}

private function internalDateTimeBuilder(ClassType $type): FunctionObjectBuilder
private function internalDateTimeBuilder(ObjectType $type): FunctionObjectBuilder
{
$constructor = new DateTimeFormatConstructor(...$this->supportedDateFormats);
$function = new FunctionObject($this->functionDefinitionRepository->for($constructor), $constructor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use CuyZ\Valinor\Mapper\Object\NativeConstructorObjectBuilder;
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;
use DateTimeZone;
use Exception;

Expand Down Expand Up @@ -60,7 +60,7 @@ public function for(ClassDefinition $class): array
return $builders;
}

private function defaultBuilder(ClassType $type): FunctionObjectBuilder
private function defaultBuilder(ObjectType $type): FunctionObjectBuilder
{
$constructor = function (string $timezone) {
try {
Expand Down
4 changes: 2 additions & 2 deletions src/Mapper/Object/FunctionObjectBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use CuyZ\Valinor\Definition\FunctionObject;
use CuyZ\Valinor\Definition\ParameterDefinition;
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\ObjectType;
use Exception;

use function array_map;
Expand All @@ -24,7 +24,7 @@ final class FunctionObjectBuilder implements ObjectBuilder

private bool $isDynamicConstructor;

public function __construct(FunctionObject $function, ClassType $type)
public function __construct(FunctionObject $function, ObjectType $type)
{
$definition = $function->definition;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use function count;

/** @internal */
final class ObjectNodeBuilder
final class FilteredObjectNodeBuilder
{
public function __construct(private bool $allowSuperfluousKeys) {}

Expand Down
Loading

0 comments on commit 13f69a9

Please sign in to comment.