diff --git a/src/Encoder/ObjectAccess.php b/src/Encoder/ObjectAccess.php new file mode 100644 index 0000000..36595fb --- /dev/null +++ b/src/Encoder/ObjectAccess.php @@ -0,0 +1,98 @@ + $properties + * @param array> $encoderLenses + * @param array> $decoderLenses + * @param array> $isos + */ + public function __construct( + public readonly array $properties, + public readonly array $encoderLenses, + public readonly array $decoderLenses, + public readonly array $isos, + public readonly bool $isAnyPropertyQualified + ) { + } + + public static function forContext(Context $context): self + { + $type = ComplexTypeBuilder::default()($context); + + $sortedProperties = sort_by( + $type->getProperties(), + static fn (Property $property): bool => !$property->getType()->getMeta()->isAttribute()->unwrapOr(false), + ); + + $normalizedProperties = []; + $encoderLenses = []; + $decoderLenses = []; + $isos = []; + $isAnyPropertyQualified = false; + + foreach ($sortedProperties as $property) { + $typeMeta = $property->getType()->getMeta(); + $name = $property->getName(); + $normalizedName = PhpPropertyNameNormalizer::normalize($name); + + $shouldLensBeOptional = self::shouldLensBeOptional($typeMeta); + $normalizedProperties[$normalizedName] = $property; + $encoderLenses[$normalizedName] = $shouldLensBeOptional ? optional(property($normalizedName)) : property($normalizedName); + $decoderLenses[$normalizedName] = $shouldLensBeOptional ? optional(index($name)) : index($name); + $isos[$normalizedName] = self::grabIsoForProperty($context, $property); + + $isAnyPropertyQualified = $isAnyPropertyQualified || $typeMeta->isQualified()->unwrapOr(false); + } + + return new self( + $normalizedProperties, + $encoderLenses, + $decoderLenses, + $isos, + $isAnyPropertyQualified + ); + } + + private static function shouldLensBeOptional(TypeMeta $meta): bool + { + if ($meta->isNullable()->unwrapOr(false)) { + return true; + } + + if ( + $meta->isAttribute()->unwrapOr(false) && + $meta->use()->unwrapOr('optional') === 'optional' + ) { + return true; + } + + return false; + } + + /** + * @return Iso + */ + private static function grabIsoForProperty(Context $context, Property $property): Iso + { + $propertyContext = $context->withType($property->getType()); + + return $context->registry->detectEncoderForContext($propertyContext) + ->iso($propertyContext); + } +} diff --git a/src/Encoder/ObjectEncoder.php b/src/Encoder/ObjectEncoder.php index 80ed1b0..f1e0599 100644 --- a/src/Encoder/ObjectEncoder.php +++ b/src/Encoder/ObjectEncoder.php @@ -5,8 +5,6 @@ use Closure; use Exception; -use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer; -use Soap\Encoding\TypeInference\ComplexTypeBuilder; use Soap\Encoding\TypeInference\XsiTypeDetector; use Soap\Encoding\Xml\Node\Element; use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader; @@ -15,19 +13,11 @@ use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter; use Soap\Encoding\Xml\Writer\XsiAttributeBuilder; use Soap\Engine\Metadata\Model\Property; -use Soap\Engine\Metadata\Model\TypeMeta; use VeeWee\Reflecta\Iso\Iso; use VeeWee\Reflecta\Lens\Lens; use function is_array; -use function Psl\Dict\map; use function Psl\Dict\map_with_key; -use function Psl\Dict\reindex; -use function Psl\Iter\any; -use function Psl\Vec\sort_by; use function VeeWee\Reflecta\Iso\object_data; -use function VeeWee\Reflecta\Lens\index; -use function VeeWee\Reflecta\Lens\optional; -use function VeeWee\Reflecta\Lens\property; use function VeeWee\Xml\Writer\Builder\children as writeChildren; use function VeeWee\Xml\Writer\Builder\raw; use function VeeWee\Xml\Writer\Builder\value as buildValue; @@ -52,24 +42,24 @@ public function __construct( */ public function iso(Context $context): Iso { - $properties = $this->detectProperties($context); + $objectAccess = ObjectAccess::forContext($context); return new Iso( /** * @param TObj|array $value * @return non-empty-string */ - function (object|array $value) use ($context, $properties) : string { - return $this->to($context, $properties, $value); + function (object|array $value) use ($context, $objectAccess) : string { + return $this->to($context, $objectAccess, $value); }, /** * @param non-empty-string|Element $value * @return TObj */ - function (string|Element $value) use ($context, $properties) : object { + function (string|Element $value) use ($context, $objectAccess) : object { return $this->from( $context, - $properties, + $objectAccess, ($value instanceof Element ? $value : Element::fromString($value)) ); } @@ -78,19 +68,14 @@ function (string|Element $value) use ($context, $properties) : object { /** * @param TObj|array $data - * @param array $properties * * @return non-empty-string */ - private function to(Context $context, array $properties, object|array $data): string + private function to(Context $context, ObjectAccess $objectAccess, object|array $data): string { if (is_array($data)) { $data = (object) $data; } - $isAnyPropertyQualified = any( - $properties, - static fn (Property $property): bool => $property->getType()->getMeta()->isQualified()->unwrapOr(false) - ); $defaultAction = writeChildren([]); return (new XsdTypeXmlElementWriter())( @@ -100,32 +85,31 @@ private function to(Context $context, array $properties, object|array $data): st (new XsiAttributeBuilder( $context, XsiTypeDetector::detectFromValue($context, []), - includeXsiTargetNamespace: !$isAnyPropertyQualified, + includeXsiTargetNamespace: !$objectAccess->isAnyPropertyQualified, )), ...map_with_key( - $properties, - function (string $normalizePropertyName, Property $property) use ($context, $data, $defaultAction) : Closure { + $objectAccess->properties, + static function (string $normalizePropertyName, Property $property) use ($objectAccess, $data, $defaultAction) : Closure { $type = $property->getType(); $meta = $type->getMeta(); $isAttribute = $meta->isAttribute()->unwrapOr(false); /** @var mixed $value */ - $value = $this->runLens( - property($normalizePropertyName), - $meta, - $data, - null + $value = self::runLens( + $objectAccess->encoderLenses[$normalizePropertyName], + $data ); + $iso = $objectAccess->isos[$normalizePropertyName]; return match(true) { $isAttribute => $value ? (new AttributeBuilder( $type, - $this->grabIsoForProperty($context, $property)->to($value) + $iso->to($value) ))(...) : $defaultAction, $property->getName() === '_' => $value - ? buildValue($this->grabIsoForProperty($context, $property)->to($value)) + ? buildValue($iso->to($value)) : (new NilAttributeBuilder())(...), - default => $value ? raw($this->grabIsoForProperty($context, $property)->to($value)) : $defaultAction + default => $value ? raw($iso->to($value)) : $defaultAction }; } ) @@ -135,100 +119,46 @@ function (string $normalizePropertyName, Property $property) use ($context, $dat } /** - * @param array $properties - * * @return TObj */ - private function from(Context $context, array $properties, Element $data): object + private function from(Context $context, ObjectAccess $objectAccess, Element $data): object { $nodes = (new DocumentToLookupArrayReader())($data); /** @var Iso> $objectData */ $objectData = object_data($this->className); return $objectData->from( - map( - $properties, - function (Property $property) use ($context, $nodes): mixed { + map_with_key( + $objectAccess->properties, + static function (string $normalizePropertyName, Property $property) use ($objectAccess, $nodes): mixed { $type = $property->getType(); $meta = $type->getMeta(); /** @var string|null $value */ - $value = $this->runLens( - index($property->getName()), - $meta, + $value = self::runLens( + $objectAccess->decoderLenses[$normalizePropertyName], $nodes, - null ); + $iso = $objectAccess->isos[$normalizePropertyName]; $defaultValue = $meta->isList()->unwrapOr(false) ? [] : null; /** @psalm-suppress PossiblyNullArgument */ return match(true) { - $meta->isAttribute()->unwrapOr(false) => $this->grabIsoForProperty($context, $property)->from($value), - default => $value !== null ? $this->grabIsoForProperty($context, $property)->from($value) : $defaultValue, + $meta->isAttribute()->unwrapOr(false) => $iso->from($value), + default => $value !== null ? $iso->from($value) : $defaultValue, }; }, ) ); } - /** - * @return Iso - */ - private function grabIsoForProperty(Context $context, Property $property): Iso - { - $propertyContext = $context->withType($property->getType()); - - return $context->registry->detectEncoderForContext($propertyContext) - ->iso($propertyContext); - } - - private function runLens(Lens $lens, TypeMeta $meta, mixed $data, mixed $default): mixed + private static function runLens(Lens $lens, mixed $data, mixed $default = null): mixed { try { /** @var mixed */ - return $this->decorateLensForType($lens, $meta)->get($data); + return $lens->get($data); } catch (Exception $e) { return $default; } } - - /** - * @template S - * @template A - * - * @param Lens $lens - * - * @return Lens - */ - private function decorateLensForType(Lens $lens, TypeMeta $meta): Lens - { - if ($meta->isNullable()->unwrapOr(false)) { - return optional($lens); - } - - if ( - $meta->isAttribute()->unwrapOr(false) && - $meta->use()->unwrapOr('optional') === 'optional' - ) { - return optional($lens); - } - - return $lens; - } - - /** - * @return array - */ - private function detectProperties(Context $context): array - { - $type = ComplexTypeBuilder::default()($context); - - return reindex( - sort_by( - $type->getProperties(), - static fn (Property $property): bool => !$property->getType()->getMeta()->isAttribute()->unwrapOr(false), - ), - static fn (Property $property): string => PhpPropertyNameNormalizer::normalize($property->getName()), - ); - } } diff --git a/tests/Unit/Encoder/ObjectEncoderTest.php b/tests/Unit/Encoder/ObjectEncoderTest.php index 633c3c3..6e2a96a 100644 --- a/tests/Unit/Encoder/ObjectEncoderTest.php +++ b/tests/Unit/Encoder/ObjectEncoderTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use Soap\Encoding\Encoder\Context; +use Soap\Encoding\Encoder\ObjectAccess; use Soap\Encoding\Encoder\ObjectEncoder; use Soap\Encoding\Test\Fixture\Model\Hat; use Soap\Encoding\Test\Fixture\Model\User; @@ -21,6 +22,7 @@ use function Psl\Fun\tap; #[CoversClass(ObjectEncoder::class)] +#[CoversClass(ObjectAccess::class)] final class ObjectEncoderTest extends AbstractEncoderTests { public static function provideIsomorphicCases(): iterable