Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 83 additions & 35 deletions src/Attribute/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@
use Membrane\Builder\Builder as BuilderInterface;
use Membrane\Builder\Specification;
use Membrane\Exception\CannotProcessProperty;
use Membrane\Filter;
use Membrane\Processor;
use Membrane\Processor\AfterSet;
use Membrane\Processor\BeforeSet;
use Membrane\Processor\Collection;
use Membrane\Processor\Field;
use Membrane\Processor\FieldSet;
use Membrane\Processor\ProcessorType;
use Membrane\Validator;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionProperty;

use function array_map;
Expand Down Expand Up @@ -53,30 +54,84 @@ private function fromClass(string $class, string $processes = ''): Processor
continue;
}

$type = $property->getType();
$processors[] = $this->getProcessorFromProperty($property);
}

if ($type === null) {
throw CannotProcessProperty::noTypeHint($property->getName());
}
return new FieldSet($processes, ...$processors);
}

if (!($type instanceof ReflectionNamedType)) {
throw CannotProcessProperty::compoundPropertyType($property->getName());
}
private function getProcessorFromProperty(ReflectionProperty $property): Processor
{
$type = $property->getType();

if ($type === null) {
throw CannotProcessProperty::noTypeHint($property->getName());
}

$processorType = $this->getProcessorTypeFromPropertyType($type->getName());
$processorTypeAttribute = current($property->getAttributes(OverrideProcessorType::class));
if ($processorTypeAttribute !== false) {
$processorType = $processorTypeAttribute->newInstance()->type;
if ($type instanceof \ReflectionIntersectionType) {
throw CannotProcessProperty::intersectionTypeHint($property->getName());
}

if ($type instanceof \ReflectionUnionType) {
$processors = [];

foreach ($type->getTypes() as $subType) {
if ($subType instanceof \ReflectionIntersectionType) {
throw CannotProcessProperty::intersectionTypeHint($property->getName());
}

if (!in_array($subType->getName(), ['bool', 'float', 'int', 'string', 'null', 'true', 'false'])) {
throw CannotProcessProperty::compoundPropertyType($property->getName());
}

$processors [] = $this->makeField($property->getName(), ...$this
->getFiltersOrValidators($property, $subType->getName()));
}

$processors[] = match ($processorType) {
ProcessorType::Field => $this->makeField($property),
ProcessorType::Fieldset => $this->fromClass($type->getName(), $property->getName()),
ProcessorType::Collection => $this->makeCollection($property),
};
// AnyOf is faster than OneOf since it performs less checks
return new Processor\AnyOf($property->getName(), ...$processors);
}

return new FieldSet($processes, ...$processors);
assert($type instanceof \ReflectionNamedType); // proof by exhaustion (all alternatives have been checked above)

$processorType = $this->getProcessorTypeFromPropertyType($type->getName());
$processorTypeAttribute = current($property->getAttributes(OverrideProcessorType::class));
if ($processorTypeAttribute !== false) {
$processorType = $processorTypeAttribute->newInstance()->type;
}

return match ($processorType) {
ProcessorType::Field => $this->makeField($property->getName(), ...$this
->getFiltersOrValidators($property, $type->getName())),
ProcessorType::Fieldset => $this->fromClass($type->getName(), $property->getName()),
ProcessorType::Collection => $this->makeCollection($property),
};
}

/** @return array<Filter|Validator> */
private function getFiltersOrValidators(
ReflectionProperty $property,
string $type
): array {
$reflectionAttributes = $property->getAttributes();

$result = [];
foreach ($reflectionAttributes as $reflectionAttribute) {
$attribute = $reflectionAttribute->newInstance();

switch (true) {
case $attribute instanceof When:
if ($attribute->typeIs === $type) {
$result [] = $attribute->filterOrValidator->class;
}
break;
case $attribute instanceof FilterOrValidator:
$result [] = $attribute->class;
break;
}
}

return $result;
}

private function getProcessorTypeFromPropertyType(string $type): ProcessorType
Expand All @@ -96,28 +151,20 @@ enum_exists($type)
};
}

private function makeField(ReflectionProperty $property): Field
{
$attributes = $property->getAttributes(
FilterOrValidator::class,
ReflectionAttribute::IS_INSTANCEOF
);

return new Field(
$property->getName(),
...array_map(fn($reflectionAttribute) => $reflectionAttribute->newInstance()->class, $attributes)
);
private function makeField(
string $propertyName,
Filter|Validator ...$filtersOrValidators
): Field {
return new Field($propertyName, ...$filtersOrValidators);
}

private function makeCollection(ReflectionProperty $property): Processor
{
$subtype = (current($property->getAttributes(Subtype::class)) ?: null)
?->newInstance()
?->type;

if ($subtype === null) {
$subtype = current($property->getAttributes(Subtype::class));
if ($subtype === false) {
throw CannotProcessProperty::noSubtypeHint($property->getName());
}
$subtype = $subtype->newInstance()->type;

$subProcessorType = $this->getProcessorTypeFromPropertyType($subtype);

Expand All @@ -130,7 +177,8 @@ private function makeCollection(ReflectionProperty $property): Processor

$processors[] = match ($subProcessorType) {
ProcessorType::Fieldset => $this->fromClass($subtype, $property->getName()),
ProcessorType::Field => $this->makeField($property),
ProcessorType::Field => $this->makeField($property->getName(), ...$this
->getFiltersOrValidators($property, $subtype)),
ProcessorType::Collection =>
throw CannotProcessProperty::nestedCollection($property->getName())
};
Expand Down
15 changes: 15 additions & 0 deletions src/Attribute/When.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Membrane\Attribute;

#[\Attribute(\Attribute::IS_REPEATABLE + \Attribute::TARGET_PROPERTY)]
final class When
{
public function __construct(
public string $typeIs,
public FilterOrValidator $filterOrValidator,
) {
}
}
12 changes: 11 additions & 1 deletion src/Exception/CannotProcessProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,19 @@ public static function noTypeHint(string $propertyName): self
return new self($message);
}

public static function intersectionTypeHint(string $propertyName): self
{
$message = sprintf('Property %s uses an intersection type, these are not supported', $propertyName);
return new self($message);
}

public static function compoundPropertyType(string $propertyName): self
{
$message = sprintf('Property %s uses a compound type hint, these are not currently supported', $propertyName);
$message = sprintf(
'Property %s uses a compound type hint'
. ', currently these are only supported for scalar types',
$propertyName
);
return new self($message);
}

Expand Down
1 change: 0 additions & 1 deletion src/OpenAPI/Exception/CannotProcessOpenAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public static function invalidOpenAPI(string $fileName, string ...$errors): self
return new self($message, self::INVALID_OPEN_API);
}

/** @param $mediaTypes */
public static function unsupportedMediaTypes(string ...$mediaTypes): self
{
$supportedContentTypes = [
Expand Down
Loading