Skip to content
Open
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
8 changes: 8 additions & 0 deletions benchmarks/ComplexTypeBenchTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ trait ComplexTypeBenchTrait
/** @var list<object> */
protected array $items;
protected SoapResponse $literalResponse;
protected SoapResponse $literalSparseResponse;
protected SoapResponse $encodedResponse;

protected BenchSoapClient $extSoapLiteral;
Expand Down Expand Up @@ -77,6 +78,13 @@ public function setUp(): void
$this->encodedResponse = new SoapResponse(
$this->encodedDriver->encode('test', [$this->singleItem])->getRequest()
);
// Real-world responses often leave most elements out: only 2 of the 12 elements are present here.
$this->literalSparseResponse = new SoapResponse(
'<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'
. '<SOAP-ENV:Body><ns1:testResponse xmlns:ns1="http://test-uri/">'
. '<testParam><id>42</id><address><city>Springfield</city></address></testParam>'
. '</ns1:testResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>'
);

// ext-soap
$this->extSoapLiteral = $this->buildExtSoapClient('literal');
Expand Down
16 changes: 16 additions & 0 deletions benchmarks/DecodeBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ public function benchLiteral_x500(): void
}
}

#[Revs(100)]
#[Groups(['literal', 'php-soap', 'x1', 'sparse'])]
public function benchLiteralSparse_x1(): void
{
$this->literalDriver->decode('test', $this->literalSparseResponse);
}

#[Groups(['literal', 'php-soap', 'x500', 'sparse'])]
public function benchLiteralSparse_x500(): void
{
$response = $this->literalSparseResponse;
foreach ($this->items as $item) {
$this->literalDriver->decode('test', $response);
}
}

// -- php-soap/encoding: encoded --

#[Revs(100)]
Expand Down
18 changes: 16 additions & 2 deletions src/Encoder/ObjectAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
use Soap\Engine\Metadata\Model\Type;
use Soap\Engine\Metadata\Model\TypeMeta;
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\index;
use function VeeWee\Reflecta\Lens\optional;
use function VeeWee\Reflecta\Lens\property;

Expand Down Expand Up @@ -137,12 +137,26 @@ private static function createDecoderLensForType(
$lens = match(true) {
$encoder instanceof Feature\DecoratingEncoder => self::createDecoderLensForType($shouldLensBeOptional, $name, $encoder->decoratedEncoder(), $type, $property),
$encoder instanceof Feature\ProvidesObjectDecoderLens => $encoder::createObjectDecoderLens($type, $property),
default => index($name),
default => self::createLookupLens($name),
};

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

/**
* Resolves absent nodes to null instead of throwing like index() does:
* absent optional elements and attributes are common in large responses,
* and building + catching an exception for each of them dominates decoding time.
*
* @return LensInterface<array<array-key, mixed>|null, array<array-key, mixed>|null, mixed, mixed>
*/
private static function createLookupLens(string $name): LensInterface

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might move this to a Lens namespace where we provide some highly optimized lenses.

{
return Lens::readonly(
static fn (?array $data): mixed => $data[$name] ?? null
);
}

private static function shouldLensBeOptional(TypeMeta $meta): bool
{
if ($meta->isNullable()->unwrapOr(false)) {
Expand Down
54 changes: 31 additions & 23 deletions src/Encoder/ObjectEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
use Soap\Encoding\Xml\Writer\XsiAttributeBuilder;
use Soap\Engine\Metadata\Model\Property;
use stdClass;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Reflecta\Lens\LensInterface;
use function is_array;
Expand Down Expand Up @@ -125,32 +126,39 @@ static function (string $normalizePropertyName, Property $property) use ($object
private function from(Context $context, ObjectAccess $objectAccess, Element $data): object
{
$nodes = (new DocumentToLookupArrayReader())($data);
$values = 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 = self::runLens(
$objectAccess->decoderLenses[$normalizePropertyName],
$nodes,
);
$iso = $objectAccess->isos[$normalizePropertyName];
$defaultValue = $meta->isList()->unwrapOr(false) ? [] : null;

/** @psalm-suppress PossiblyNullArgument */
return match(true) {
$meta->isAttribute()->unwrapOr(false) => $iso->from($value),
default => $value !== null ? $iso->from($value) : $defaultValue,
};
},
);

// stdClass has no declared properties: reflection-based hydration would build and catch
// an exception for every single property before falling back to a dynamic one.
if ($this->className === stdClass::class) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will of course only add benifits for stdClass.
It feels a bit like a shortcut

Wouldn't it make sense to make set_properties smarter?

https://github.com/veewee/reflecta/blob/main/src/Reflect/properties_set.php

We could always load all properties before adding the predicate.
When the property does not exist and it's a dynamic class, we can just assign.
That will add a speedup for all dynamic classes and would also make the lookup of the property more simple (cause we already have them all and just need to pick based on key).

Not sure what the timing impact would be for the both scenarios.

/** @var TObj */
return (object) $values;
}

/** @var Iso<TObj, TObj, array<string, mixed>, array<string, mixed>> $objectData */
$objectData = object_data($this->className);

return $objectData->from(
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 = self::runLens(
$objectAccess->decoderLenses[$normalizePropertyName],
$nodes,
);
$iso = $objectAccess->isos[$normalizePropertyName];
$defaultValue = $meta->isList()->unwrapOr(false) ? [] : null;

/** @psalm-suppress PossiblyNullArgument */
return match(true) {
$meta->isAttribute()->unwrapOr(false) => $iso->from($value),
default => $value !== null ? $iso->from($value) : $defaultValue,
};
},
)
);
return $objectData->from($values);
}

private static function runLens(LensInterface $lens, mixed $data, mixed $default = null): mixed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to have the speedup everywhere, we should somehow be able to get rid of this runLens method here and make all used lenses non-throwing.
We could provide a set of faster lenses in this package for this purpose.

Expand Down
44 changes: 44 additions & 0 deletions tests/Unit/Encoder/ObjectEncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,50 @@ public function test_it_can_decode_from_xml_item(): void
static::assertEquals($expected, $actual);
}

public function test_it_decodes_absent_elements_as_null(): void
{
$encoder = new ObjectEncoder(stdClass::class);
$xsdType = XsdType::create('user')
->withXmlNamespace("https://test")
->withXmlNamespaceName('test')
->withXmlTargetNodeName('user')
->withMeta(static fn (TypeMeta $meta): TypeMeta => $meta->withIsQualified(true));
$context = self::createContext($xsdType, self::buildTypes());

$actual = $encoder->iso($context)->from(Element::fromString('<user><hat /></user>'));

static::assertEquals(
(object)[
'active' => null,
'hat' => (object)[
'color' => null,
],
],
$actual
);
}

public function test_it_decodes_absent_attributes_as_null(): void
{
$encoder = new ObjectEncoder(stdClass::class);
$xsdType = XsdType::create('user')
->withXmlNamespace("https://test")
->withXmlNamespaceName('test')
->withXmlTargetNodeName('user')
->withMeta(static fn (TypeMeta $meta): TypeMeta => $meta->withIsQualified(true));
$context = self::createContext($xsdType, self::buildTypes(activeAsAttribute: true));

$actual = $encoder->iso($context)->from(Element::fromString('<user />'));

static::assertEquals(
(object)[
'active' => null,
'hat' => null,
],
$actual
);
}

public function test_it_can_encode_from_array(): void
{
$encoder = new ObjectEncoder(stdClass::class);
Expand Down
Loading