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
86 changes: 32 additions & 54 deletions src/Encoder/ObjectAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Soap\Encoding\Encoder;

use Closure;
use Soap\Encoding\Cache\ScopedCache;
use Soap\Encoding\EncoderRegistry;
use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
Expand All @@ -15,10 +16,8 @@
use Soap\Engine\Metadata\Model\TypeMeta;
use Soap\Engine\Metadata\Model\XsdType;
use VeeWee\Reflecta\Iso\IsoInterface;
use VeeWee\Reflecta\Lens\Lens;
use VeeWee\Reflecta\Lens\LensInterface;
use function Psl\Vec\sort_by;
use function VeeWee\Reflecta\Lens\optional;
use function VeeWee\Reflecta\Lens\optional_property;

final class ObjectAccess
Expand All @@ -28,8 +27,8 @@ final class ObjectAccess
* Every array is keyed by the normalized PHP property name.
*
* @param array<string, Property> $properties The metadata properties, attributes first.
* @param array<string, LensInterface<object|null, object|null, mixed, mixed>> $encoderLenses Reads the property value from the PHP object on encode.
* @param array<string, LensInterface<ElementLookup|null, ElementLookup|null, mixed, mixed>> $decoderLenses Reads the raw XML value from the element on decode.
* @param array<string, Closure(object): mixed> $encodeReaders Reads the property value from the PHP object on encode.
* @param array<string, Closure(ElementLookup): mixed> $decodeReaders Reads the raw XML value from the element on decode.
* @param array<string, IsoInterface<mixed, mixed, string|null, Element|ElementList|string|null>> $isos Converts between the PHP value and its XML form.
* @param bool $isAnyPropertyQualified Whether at least one property is namespace qualified.
* @param array<string, bool> $isAttribute Whether the property is an XML attribute instead of a child element.
Expand All @@ -38,8 +37,8 @@ final class ObjectAccess
*/
public function __construct(
public readonly array $properties,
public readonly array $encoderLenses,
public readonly array $decoderLenses,
public readonly array $encodeReaders,
public readonly array $decodeReaders,
public readonly array $isos,
public readonly bool $isAnyPropertyQualified,
public readonly array $isAttribute,
Expand Down Expand Up @@ -85,8 +84,8 @@ private static function build(Context $context): self
);

$normalizedProperties = [];
$encoderLenses = [];
$decoderLenses = [];
$encodeReaders = [];
$decodeReaders = [];
$isos = [];
$isAttribute = [];
$decodeDefaultValues = [];
Expand All @@ -101,13 +100,22 @@ private static function build(Context $context): self
$normalizedName = PhpPropertyNameNormalizer::normalize($name);

$encoder = $context->registry->detectEncoderForContext($propertyContext);
$shouldLensBeOptional = self::shouldLensBeOptional($propertyTypeMeta);
$normalizedProperties[$normalizedName] = $property;

$customEncoderLens = self::createCustomEncoderLens($shouldLensBeOptional, $encoder, $type, $property);
$encoderLenses[$normalizedName] = $customEncoderLens ?? optional_property($normalizedName);
$decoderLenses[$normalizedName] = self::createCustomDecoderLens($shouldLensBeOptional, $encoder, $type, $property)
?? self::createDecoderLens($name, $propertyType);
$customEncoderLens = self::createCustomEncoderLens($encoder, $type, $property);
/** @var Closure(object): mixed $encodeReader */
$encodeReader = $customEncoderLens !== null
? $customEncoderLens->get(...)
: optional_property($normalizedName)->get(...);
$encodeReaders[$normalizedName] = $encodeReader;

$customDecoderLens = self::createCustomDecoderLens($encoder, $type, $property);
/** @var Closure(ElementLookup): mixed $decodeReader */
$decodeReader = $customDecoderLens !== null
? $customDecoderLens->get(...)
: self::createDecoderReader($name, $propertyType);
$decodeReaders[$normalizedName] = $decodeReader;

$isos[$normalizedName] = $encoder->iso($propertyContext);
$isAttribute[$normalizedName] = $propertyTypeMeta->isAttribute()->unwrapOr(false);
$decodeDefaultValues[$normalizedName] = $propertyTypeMeta->isList()->unwrapOr(false) ? [] : null;
Expand All @@ -118,8 +126,8 @@ private static function build(Context $context): self

return new self(
$normalizedProperties,
$encoderLenses,
$decoderLenses,
$encodeReaders,
$decodeReaders,
$isos,
$isAnyPropertyQualified,
$isAttribute,
Expand Down Expand Up @@ -152,78 +160,48 @@ private static function onNull(
* @return LensInterface<object|null, object|null, mixed, mixed>|null
*/
private static function createCustomEncoderLens(
bool $shouldLensBeOptional,
XmlEncoder $encoder,
Type $type,
Property $property,
): ?LensInterface {
$lens = match (true) {
$encoder instanceof Feature\DecoratingEncoder => self::createCustomEncoderLens($shouldLensBeOptional, $encoder->decoratedEncoder(), $type, $property),
return match (true) {
$encoder instanceof Feature\DecoratingEncoder => self::createCustomEncoderLens($encoder->decoratedEncoder(), $type, $property),
$encoder instanceof Feature\ProvidesObjectEncoderLens => $encoder::createObjectEncoderLens($type, $property),
default => null,
};

if ($lens === null) {
return null;
}

return $shouldLensBeOptional ? optional($lens) : $lens;
}

/**
* @return LensInterface<ElementLookup, ElementLookup, mixed, mixed>
* @return Closure(ElementLookup): mixed
*/
private static function createDecoderLens(string $name, XsdType $type): LensInterface
private static function createDecoderReader(string $name, XsdType $type): Closure
{
$meta = $type->getMeta();
if ($meta->isAttribute()->unwrapOr(false)) {
$namespace = $meta->isQualified()->unwrapOr(false) ? $type->getXmlTargetNamespace() : '';

return Lens::readonly(static fn (ElementLookup $lookup): ?string => $lookup->attribute($name, $namespace));
return static fn (ElementLookup $lookup): ?string => $lookup->attribute($name, $namespace);
}

if ($name === '_') {
return Lens::readonly(static fn (ElementLookup $lookup): ?string => $lookup->text());
return static fn (ElementLookup $lookup): ?string => $lookup->text();
}

return Lens::readonly(static fn (ElementLookup $lookup): Element|ElementList|null => $lookup->element($name));
return static fn (ElementLookup $lookup): Element|ElementList|null => $lookup->element($name);
}

/**
* @return LensInterface<ElementLookup|null, ElementLookup|null, mixed, mixed>|null
*/
private static function createCustomDecoderLens(
bool $shouldLensBeOptional,
XmlEncoder $encoder,
Type $type,
Property $property,
): ?LensInterface {
$lens = match(true) {
$encoder instanceof Feature\DecoratingEncoder => self::createCustomDecoderLens($shouldLensBeOptional, $encoder->decoratedEncoder(), $type, $property),
return match(true) {
$encoder instanceof Feature\DecoratingEncoder => self::createCustomDecoderLens($encoder->decoratedEncoder(), $type, $property),
$encoder instanceof Feature\ProvidesObjectDecoderLens => $encoder::createObjectDecoderLens($type, $property),
default => null,
};

if ($lens === null) {
return null;
}

return $shouldLensBeOptional ? optional($lens) : $lens;
}

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;
}
}
25 changes: 4 additions & 21 deletions src/Encoder/ObjectEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

namespace Soap\Encoding\Encoder;

use Exception;
use Soap\Encoding\Xml\Node\Element;
use Soap\Encoding\Xml\Node\ElementList;
use Soap\Encoding\Xml\Node\ElementLookup;
use Soap\Encoding\Xml\Writer\AttributeBuilder;
use Soap\Encoding\Xml\Writer\NilAttributeBuilder;
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
use Soap\Encoding\Xml\Writer\XsiAttributeBuilder;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Reflecta\Lens\LensInterface;
use function is_array;
use function Psl\Fun\lazy;
use function VeeWee\Reflecta\Iso\object_data;
Expand Down Expand Up @@ -85,10 +84,7 @@ private function to(Context $context, ObjectAccess $objectAccess, object|array $
];
foreach ($objectAccess->properties as $normalizePropertyName => $property) {
/** @var mixed $value */
$value = self::runLens(
$objectAccess->encoderLenses[$normalizePropertyName],
$data
);
$value = $objectAccess->encodeReaders[$normalizePropertyName]($data);
if ($value === null && match ($objectAccess->onNull[$normalizePropertyName]) {
NullValueHandling::Write => false,
NullValueHandling::Skip => true,
Expand Down Expand Up @@ -128,11 +124,8 @@ private function from(Context $context, ObjectAccess $objectAccess, Element $dat

$result = [];
foreach ($objectAccess->isos as $normalizePropertyName => $iso) {
/** @var string|null $value */
$value = self::runLens(
$objectAccess->decoderLenses[$normalizePropertyName],
$lookup,
);
/** @var Element|ElementList|string|null $value */
$value = $objectAccess->decodeReaders[$normalizePropertyName]($lookup);

/**
* @var mixed
Expand All @@ -147,14 +140,4 @@ private function from(Context $context, ObjectAccess $objectAccess, Element $dat

return $objectData->from($result);
}

private static function runLens(LensInterface $lens, mixed $data, mixed $default = null): mixed
{
try {
/** @var mixed */
return $lens->get($data);
} catch (Exception $e) {
return $default;
}
}
}
6 changes: 4 additions & 2 deletions src/Encoder/OptionalElementEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public function iso(Context $context): IsoInterface
return $elementIso;
}

$xsiNamespace = Xmlns::xsi()->value();

return new Iso(
/**
* @param T|null $raw
Expand All @@ -75,7 +77,7 @@ public function iso(Context $context): IsoInterface
/**
* @return T|null
*/
static function (ElementList|Element|string $xml) use ($elementIso, $isList) : mixed {
static function (ElementList|Element|string $xml) use ($elementIso, $isList, $xsiNamespace) : mixed {
if ($xml === '') {
return null;
}
Expand All @@ -91,7 +93,7 @@ static function (ElementList|Element|string $xml) use ($elementIso, $isList) : m
$parsedXml instanceof Element => $parsedXml->element(),
default => null
};
if ($documentElement && $documentElement->getAttributeNS(Xmlns::xsi()->value(), 'nil') === 'true') {
if ($documentElement && $documentElement->getAttributeNS($xsiNamespace, 'nil') === 'true') {
return null;
}

Expand Down
10 changes: 2 additions & 8 deletions src/Xml/Reader/ElementValueReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
use Soap\Encoding\Encoder\Context;
use Soap\Encoding\Encoder\XmlEncoder;
use VeeWee\Reflecta\Iso\IsoInterface;
use function Psl\Type\string;
use function VeeWee\Xml\Dom\Locator\Node\value as readValue;

final class ElementValueReader
{
Expand All @@ -30,9 +28,7 @@ public function __invoke(
*/
public static function forEncoder(Context $context, XmlEncoder $encoder, Element $element): mixed
{
return $encoder->iso($context)->from(
readValue($element, string())
);
return $encoder->iso($context)->from($element->substitutedNodeValue);
}

/**
Expand All @@ -41,8 +37,6 @@ public static function forEncoder(Context $context, XmlEncoder $encoder, Element
*/
public static function forIso(IsoInterface $iso, Element $element): mixed
{
return $iso->from(
readValue($element, string())
);
return $iso->from($element->substitutedNodeValue);
}
}
24 changes: 0 additions & 24 deletions tests/Support/Throws/allowed-throws.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,12 @@
<?php
declare(strict_types=1);

use Soap\Encoding\Test\Unit\Encoder\ObjectEncoderCustomLensTest;
use Soap\Encoding\Test\Unit\Encoder\ObjectPropertyPresenceTest;
use VeeWee\Reflecta\ArrayAccess\Exception\ArrayAccessException;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;

/**
* Deliberate intermediate throws, per test and site. Each entry needs the reason it cannot be avoided.
*/
return [
[
'test' => ObjectEncoderCustomLensTest::class.'::test_it_decodes_through_custom_lenses_that_throw_on_a_missing_element',
'throw' => ArrayAccessException::class,
'via' => 'src/Encoder/ObjectEncoder.php -> VeeWee\\Reflecta\\Lens\\Lens->get',
'throws' => 1,
'reason' => 'The fixture is a third-party encoder whose decoder lens throws on a missing element, to prove custom lenses still get the optional() and runLens treatment.',
],
[
'test' => ObjectEncoderCustomLensTest::class.'::test_it_encodes_through_custom_lenses_that_throw_on_a_missing_property',
'throw' => UnreflectableException::class,
'via' => 'src/Encoder/ObjectEncoder.php -> VeeWee\\Reflecta\\Lens\\Lens->get',
'throws' => 1,
'reason' => 'The fixture is a third-party encoder whose encoder lens throws on a missing property, to prove custom lenses still get the optional() and runLens treatment.',
],
[
'test' => ObjectPropertyPresenceTest::class.'::custom_lens_reading_null_keeps_writing_nil#lens throws on the absent property',
'throw' => UnreflectableException::class,
'via' => 'src/Encoder/ObjectEncoder.php -> VeeWee\\Reflecta\\Lens\\Lens->get',
'throws' => 1,
'reason' => 'The fixture is a third-party encoder whose encoder lens throws on a missing property, to prove that a failing custom lens keeps writing xsi:nil instead of counting as absent.',
],
[
'test' => ObjectPropertyPresenceTest::class.'::optional_property_preserves_absence_null_and_value#lazy object whose initializer throws',
'throw' => RuntimeException::class,
Expand Down
Loading
Loading