From fe5b6a8378642d0b0440c2e2d45a0a80afbff313 Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 26 Mar 2026 10:36:56 +0100 Subject: [PATCH] Cache type iso in ElementEncoder, refactor ElementValueBuilder and ElementValueReader ElementEncoder now captures the type iso once via $typeEncoder->iso($context) and passes it to ElementValueBuilder::fromIso() and ElementValueReader::forIso(), avoiding redundant iso rebuilds on every encode/decode invocation. ElementValueBuilder: - Constructor is now private; use fromEncoder() or fromIso() static factories - Stores a list of builder closures composed at construction time - Value encoding happens lazily via a valueProvider closure during __invoke - Removed deprecated resolveXsiTypeForValue/shouldIncludeXsiTargetNamespace ElementValueReader: - Added forEncoder(context, encoder, element) and forIso(iso, element) static methods for callsites with or without a cached iso - __invoke delegates to forEncoder for backwards compatibility SoapObjectEncoder, ApacheMapEncoder: switched to ElementValueReader::forEncoder(). --- benchmarks/BenchSoapClient.php | 2 +- src/Encoder/ElementEncoder.php | 9 +-- src/Encoder/SoapEnc/ApacheMapEncoder.php | 2 +- src/Encoder/SoapEnc/SoapObjectEncoder.php | 2 +- src/Xml/Reader/ElementValueReader.php | 21 +++++++ src/Xml/Writer/ElementValueBuilder.php | 70 +++++++++++------------ 6 files changed, 62 insertions(+), 44 deletions(-) diff --git a/benchmarks/BenchSoapClient.php b/benchmarks/BenchSoapClient.php index 882b6ca..2172903 100644 --- a/benchmarks/BenchSoapClient.php +++ b/benchmarks/BenchSoapClient.php @@ -14,7 +14,7 @@ class BenchSoapClient extends \SoapClient { public ?string $mockResponse = null; - public function __doRequest(string $request, string $location, string $action, int $version, bool $oneWay = false): ?string + public function __doRequest(string $request, string $location, string $action, int $version, bool $oneWay = false, ?string $uriParserClass = null): ?string { return $this->mockResponse; } diff --git a/src/Encoder/ElementEncoder.php b/src/Encoder/ElementEncoder.php index ea46dae..75e79f4 100644 --- a/src/Encoder/ElementEncoder.php +++ b/src/Encoder/ElementEncoder.php @@ -32,21 +32,22 @@ public function iso(Context $context): Iso ? $this->typeEncoder->enhanceElementContext($context) : $context; + $typeIso = $typeEncoder->iso($context); + return new Iso( /** * @psalm-param mixed $raw */ static fn (mixed $raw): string => (new XsdTypeXmlElementWriter())( $context, - (new ElementValueBuilder($context, $typeEncoder, $raw)) + ElementValueBuilder::fromIso($context, $typeEncoder, $typeIso, $raw) ), /** * @psalm-param non-empty-string|Element $xml * @psalm-return mixed */ - static fn (Element|string $xml): mixed => (new ElementValueReader())( - $context, - $typeEncoder, + static fn (Element|string $xml): mixed => ElementValueReader::forIso( + $typeIso, ($xml instanceof Element ? $xml : Element::fromString($xml))->element() ) ); diff --git a/src/Encoder/SoapEnc/ApacheMapEncoder.php b/src/Encoder/SoapEnc/ApacheMapEncoder.php index 3ed1e76..26b2676 100644 --- a/src/Encoder/SoapEnc/ApacheMapEncoder.php +++ b/src/Encoder/SoapEnc/ApacheMapEncoder.php @@ -87,7 +87,7 @@ private function decodeArray(Context $context, Element $value): array static function (array $map, DOMElement $item) use ($context, $xpath): array { $key = $xpath->evaluate('string(./key)', string(), $item); /** @psalm-var mixed $value */ - $value = (new ElementValueReader())( + $value = ElementValueReader::forEncoder( $context->withType(XsdType::any()), ScalarTypeEncoder::default(), assert_element($xpath->querySingle('./value', $item)) diff --git a/src/Encoder/SoapEnc/SoapObjectEncoder.php b/src/Encoder/SoapEnc/SoapObjectEncoder.php index f617823..775f803 100644 --- a/src/Encoder/SoapEnc/SoapObjectEncoder.php +++ b/src/Encoder/SoapEnc/SoapObjectEncoder.php @@ -78,7 +78,7 @@ private function decodeArray(Context $context, Element $value): object static function (array $map, DOMElement $item) use ($context): array { $key = $item->localName ?? 'unkown'; /** @psalm-var mixed $value */ - $value = (new ElementValueReader())( + $value = ElementValueReader::forEncoder( $context->withType(XsdType::any()), ScalarTypeEncoder::default(), $item diff --git a/src/Xml/Reader/ElementValueReader.php b/src/Xml/Reader/ElementValueReader.php index 0ceb8e8..40298bc 100644 --- a/src/Xml/Reader/ElementValueReader.php +++ b/src/Xml/Reader/ElementValueReader.php @@ -6,6 +6,7 @@ use DOMElement; use Soap\Encoding\Encoder\Context; use Soap\Encoding\Encoder\XmlEncoder; +use VeeWee\Reflecta\Iso\Iso; use function Psl\Type\string; use function VeeWee\Xml\Dom\Locator\Node\value as readValue; @@ -20,8 +21,28 @@ public function __invoke( XmlEncoder $encoder, DOMElement $element ): mixed { + return self::forEncoder($context, $encoder, $element); + } + + /** + * @param XmlEncoder $encoder + * @psalm-return mixed + */ + public static function forEncoder(Context $context, XmlEncoder $encoder, DOMElement $element): mixed + { return $encoder->iso($context)->from( readValue($element, string()) ); } + + /** + * @param Iso $iso + * @psalm-return mixed + */ + public static function forIso(Iso $iso, DOMElement $element): mixed + { + return $iso->from( + readValue($element, string()) + ); + } } diff --git a/src/Xml/Writer/ElementValueBuilder.php b/src/Xml/Writer/ElementValueBuilder.php index 65a197b..1e0ee15 100644 --- a/src/Xml/Writer/ElementValueBuilder.php +++ b/src/Xml/Writer/ElementValueBuilder.php @@ -3,10 +3,12 @@ namespace Soap\Encoding\Xml\Writer; +use Closure; use Generator; use Soap\Encoding\Encoder\Context; use Soap\Encoding\Encoder\Feature; use Soap\Encoding\Encoder\XmlEncoder; +use VeeWee\Reflecta\Iso\Iso; use XMLWriter; use function VeeWee\Xml\Writer\Builder\cdata; use function VeeWee\Xml\Writer\Builder\children; @@ -15,67 +17,61 @@ final class ElementValueBuilder { /** - * @param XmlEncoder $encoder - * @psalm-param mixed $value + * @param list> $builders */ - public function __construct( - private readonly Context $context, - private readonly XmlEncoder $encoder, - private readonly mixed $value + private function __construct( + private readonly array $builders, ) { } /** - * @return Generator + * @param XmlEncoder $encoder + * @psalm-param mixed $value */ - public function __invoke(XMLWriter $writer): Generator + public static function fromEncoder(Context $context, XmlEncoder $encoder, mixed $value): self { - yield from children([ - $this->buildXsiType(...), - $this->buildValue(...), - ])($writer); + return new self([ + XsiAttributeBuilder::forEncodedValue($context, $encoder, $value), + self::valueWriter($encoder, static fn (): string => $encoder->iso($context)->to($value)), + ]); } /** - * @return Generator + * @param XmlEncoder $encoder + * @param Iso $iso + * @psalm-param mixed $value */ - private function buildXsiType(XMLWriter $writer): Generator + public static function fromIso(Context $context, XmlEncoder $encoder, Iso $iso, mixed $value): self { - yield from XsiAttributeBuilder::forEncodedValue( - $this->context, - $this->encoder, - $this->value, - )($writer); + return new self([ + XsiAttributeBuilder::forEncodedValue($context, $encoder, $value), + self::valueWriter($encoder, static fn (): string => $iso->to($value)), + ]); } /** - * @deprecated Use XsiAttributeBuilder::resolveXsiTypeForValue() instead. Will be removed in 1.0.0. + * @return Generator */ - public static function resolveXsiTypeForValue(Context $context, mixed $value): string + public function __invoke(XMLWriter $writer): Generator { - return XsiAttributeBuilder::resolveXsiTypeForValue($context, $value); + yield from children($this->builders)($writer); } /** - * @deprecated Use XsiAttributeBuilder::shouldIncludeXsiTargetNamespace() instead. Will be removed in 1.0.0. + * @param XmlEncoder $encoder + * @param Closure(): string $valueProvider + * + * @return Closure(XMLWriter): Generator */ - public static function shouldIncludeXsiTargetNamespace(Context $context): bool + private static function valueWriter(XmlEncoder $encoder, Closure $valueProvider): Closure { - return XsiAttributeBuilder::shouldIncludeXsiTargetNamespace($context); - } + $isCData = $encoder instanceof Feature\CData; - /** - * @return Generator - */ - private function buildValue(XMLWriter $writer): Generator - { - $encoded = $this->encoder->iso($this->context)->to($this->value); + return static function (XMLWriter $writer) use ($isCData, $valueProvider): Generator { + $encoded = $valueProvider(); + $builder = $isCData ? cdata(value($encoded)) : value($encoded); - $builder = match (true) { - $this->encoder instanceof Feature\CData => cdata(value($encoded)), - default => value($encoded) + yield from $builder($writer); }; - - yield from $builder($writer); } }