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
98 changes: 98 additions & 0 deletions src/Encoder/ObjectAccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Encoder;

use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\TypeMeta;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Reflecta\Lens\Lens;
use function Psl\Vec\sort_by;
use function VeeWee\Reflecta\Lens\index;
use function VeeWee\Reflecta\Lens\optional;
use function VeeWee\Reflecta\Lens\property;

final class ObjectAccess
{
/**
* @param array<string, Property> $properties
* @param array<string, Lens<object, mixed>> $encoderLenses
* @param array<string, Lens<array, mixed>> $decoderLenses
* @param array<string, Iso<mixed, string>> $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<mixed, string>
*/
private static function grabIsoForProperty(Context $context, Property $property): Iso
{
$propertyContext = $context->withType($property->getType());

return $context->registry->detectEncoderForContext($propertyContext)
->iso($propertyContext);
}
}
124 changes: 27 additions & 97 deletions src/Encoder/ObjectEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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))
);
}
Expand All @@ -78,19 +68,14 @@ function (string|Element $value) use ($context, $properties) : object {

/**
* @param TObj|array $data
* @param array<string, Property> $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())(
Expand All @@ -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
};
}
)
Expand All @@ -135,100 +119,46 @@ function (string $normalizePropertyName, Property $property) use ($context, $dat
}

/**
* @param array<string, Property> $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<TObj, array<string, mixed>> $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<mixed, string>
*/
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<S, A> $lens
*
* @return Lens<S, A>
*/
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<string, Property>
*/
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()),
);
}
}
2 changes: 2 additions & 0 deletions tests/Unit/Encoder/ObjectEncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down