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
6 changes: 3 additions & 3 deletions benchmarks/scripts/profile_encode.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,12 +204,12 @@
}
printf("typeIso->from(\$element) (no parse): %6.1fus/call\n", (hrtime(true) - $t0) / 1e3 / $N);

// Step 4: Just DocumentToLookupArrayReader
// Step 4: ElementLookup, what ObjectEncoder decodes from
$t0 = hrtime(true);
for ($i = 0; $i < $N; $i++) {
(new \Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader())($partElement);
\Soap\Encoding\Xml\Node\ElementLookup::fromElement($partElement);
}
printf("DocumentToLookupArrayReader: %6.1fus/call\n", (hrtime(true) - $t0) / 1e3 / $N);
printf("ElementLookup::fromElement: %6.1fus/call\n", (hrtime(true) - $t0) / 1e3 / $N);

// Step 5: object_data from (properties_set)
$objectDataIso = \VeeWee\Reflecta\Iso\object_data(\stdClass::class);
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"php-soap/engine": "^2.21",
"php-soap/wsdl": "^1.19",
"php-soap/xml": "^1.10",
"php-soap/wsdl-reader": "~0.32"
"php-soap/wsdl-reader": "~0.34"
},
"require-dev": {
"vimeo/psalm": "~6.13",
Expand Down
31 changes: 12 additions & 19 deletions src/Encoder/AnyElementEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Soap\Encoding\Xml\Node\Element;
use Soap\Encoding\Xml\Node\ElementList;
use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader;
use Soap\Encoding\Xml\Node\ElementLookup;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\Type;
use VeeWee\Reflecta\Iso\Iso;
Expand All @@ -21,9 +21,7 @@
/**
* @implements XmlEncoder<array|string|null, array<array-key, string>|string|null, string, ElementList|string>
*
* @psalm-import-type LookupArray from DocumentToLookupArrayReader
*
* @template-implements Feature\ProvidesObjectDecoderLens<LookupArray, LookupArray, ElementList, ElementList>
* @template-implements Feature\ProvidesObjectDecoderLens<ElementLookup, ElementLookup, ElementList, ElementList>
*/
final class AnyElementEncoder implements Feature\ListAware, Feature\OptionalAware, Feature\ProvidesObjectDecoderLens, XmlEncoder
{
Expand All @@ -32,32 +30,27 @@ final class AnyElementEncoder implements Feature\ListAware, Feature\OptionalAwar
* It will contain all the XML tags available in the object that is surrounding the 'any' property.
* Properties that are already known by the object, will be omitted.
*
* @return Lens<LookupArray, LookupArray, ElementList, ElementList>
* @return Lens<ElementLookup, ElementLookup, ElementList, ElementList>
*/
public static function createObjectDecoderLens(Type $parentType, Property $currentProperty): Lens
{
$omittedKeys = reduce(
$omittedKeys = array_flip(reduce(
$parentType->getProperties(),
static fn (array $omit, Property $property): array => [
...$omit,
...($property->getName() !== $currentProperty->getName() ? [$property->getName()] : []),
],
[]
);
));

/**
* @param LookupArray $data
* @return LookupArray
*/
$omit = static fn (array $data): array => diff_by_key($data, array_flip($omittedKeys));

/** @var Lens<LookupArray, LookupArray, ElementList, ElementList> */
/** @var Lens<ElementLookup, ElementLookup, ElementList, ElementList> */
return Lens::readonly(
/**
* @psalm-suppress MixedArgumentTypeCoercion - Psalm gets confused about the result of omit.
* @param LookupArray $data
*/
static fn (array $data): ElementList => ElementList::fromLookupArray($omit($data))
static function (ElementLookup $lookup) use ($omittedKeys): ElementList {
/** @var array<string, Element|ElementList> $children */
$children = diff_by_key($lookup->children(), $omittedKeys);

return ElementList::fromLookupArray($children);
}
);
}

Expand Down
6 changes: 4 additions & 2 deletions src/Encoder/Feature/ProvidesObjectDecoderLens.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

namespace Soap\Encoding\Encoder\Feature;

use Soap\Encoding\Xml\Node\ElementLookup;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\Type;
use VeeWee\Reflecta\Lens\LensInterface;

/**
* When an encoder implements this feature interface, it knows how to create a lens that will be applied on the parent data that is being decoded.
* The lens reads from the ElementLookup of the parent element, for example $lookup->element('name') or $lookup->attribute('name').
*
* @template-covariant S of array<array-key, mixed>
* @template-covariant T of array<array-key, mixed>
* @template-covariant S of ElementLookup
* @template-covariant T of ElementLookup
* @template-covariant A
* @template-covariant B
*/
Expand Down
29 changes: 25 additions & 4 deletions src/Encoder/ObjectAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
use Soap\Encoding\Xml\Node\Element;
use Soap\Encoding\Xml\Node\ElementList;
use Soap\Encoding\Xml\Node\ElementLookup;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\Type;
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_index;
use function VeeWee\Reflecta\Lens\optional_property;

final class ObjectAccess
Expand All @@ -27,7 +29,7 @@ final class ObjectAccess
*
* @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<array<array-key, mixed>|null, array<array-key, mixed>|null, mixed, mixed>> $decoderLenses Reads the raw XML value from the lookup array of the element on decode.
* @param array<string, LensInterface<ElementLookup|null, ElementLookup|null, mixed, mixed>> $decoderLenses 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 Down Expand Up @@ -105,7 +107,7 @@ private static function build(Context $context): self
$customEncoderLens = self::createCustomEncoderLens($shouldLensBeOptional, $encoder, $type, $property);
$encoderLenses[$normalizedName] = $customEncoderLens ?? optional_property($normalizedName);
$decoderLenses[$normalizedName] = self::createCustomDecoderLens($shouldLensBeOptional, $encoder, $type, $property)
?? optional_index($name);
?? self::createDecoderLens($name, $propertyType);
$isos[$normalizedName] = $encoder->iso($propertyContext);
$isAttribute[$normalizedName] = $propertyTypeMeta->isAttribute()->unwrapOr(false);
$decodeDefaultValues[$normalizedName] = $propertyTypeMeta->isList()->unwrapOr(false) ? [] : null;
Expand Down Expand Up @@ -161,7 +163,26 @@ private static function createCustomEncoderLens(
}

/**
* @return LensInterface<array<array-key, mixed>|null, array<array-key, mixed>|null, mixed, mixed>|null
* @return LensInterface<ElementLookup, ElementLookup, mixed, mixed>
*/
private static function createDecoderLens(string $name, XsdType $type): LensInterface
{
$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));
}

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

return Lens::readonly(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,
Expand Down
6 changes: 3 additions & 3 deletions src/Encoder/ObjectEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use Exception;
use Soap\Encoding\Xml\Node\Element;
use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader;
use Soap\Encoding\Xml\Node\ElementLookup;
use Soap\Encoding\Xml\Writer\AttributeBuilder;
use Soap\Encoding\Xml\Writer\NilAttributeBuilder;
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
Expand Down Expand Up @@ -121,7 +121,7 @@ private function to(Context $context, ObjectAccess $objectAccess, object|array $
*/
private function from(Context $context, ObjectAccess $objectAccess, Element $data): object
{
$nodes = (new DocumentToLookupArrayReader())($data);
$lookup = ElementLookup::fromElement($data);
/** @var Iso<TObj, TObj, array<string, mixed>, array<string, mixed>> $objectData */
$objectData = object_data($this->className);

Expand All @@ -130,7 +130,7 @@ private function from(Context $context, ObjectAccess $objectAccess, Element $dat
/** @var string|null $value */
$value = self::runLens(
$objectAccess->decoderLenses[$normalizePropertyName],
$nodes,
$lookup,
);

/**
Expand Down
10 changes: 2 additions & 8 deletions src/Xml/Node/ElementList.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@
use Closure;
use Countable;
use Dom\Element as DomElement;
use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader;
use Stringable;
use VeeWee\Xml\Dom\Document;
use function count;
use function Psl\Iter\reduce;
use function Psl\Vec\map;
use function VeeWee\Xml\Dom\Locator\Element\children as readChildren;

/**
* @psalm-import-type LookupArray from DocumentToLookupArrayReader
*/
final class ElementList implements Countable, Stringable
{
/** @var list<Element> */
Expand All @@ -31,11 +27,9 @@ public function __construct(Element ...$elements)
}

/**
* Can be used to parse a nested array structure to a full flattened ElementList.
* Flattens elements and lists of elements into one list. Strings are simple values and are skipped.
*
* @see \Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader::__invoke
*
* @param LookupArray $data
* @param array<array-key, string|Element|ElementList> $data
*/
public static function fromLookupArray(array $data): self
{
Expand Down
83 changes: 83 additions & 0 deletions src/Xml/Node/ElementLookup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Xml\Node;

use Dom\Element as DomElement;
use VeeWee\Xml\Xmlns\Xmlns;

/**
* Read model of one XML element that is being decoded:
* child elements by local name, attributes by namespace and local name, and the text content.
* Attributes and child elements never stand in for each other.
*/
final readonly class ElementLookup
{
/**
* @param array<string, Element|ElementList> $children Keyed by local name, in document order.
*/
private function __construct(
private DomElement $root,
private array $children,
) {
}

public static function fromElement(Element $xml): self
{
$root = $xml->element();
$children = [];

for ($node = $root->firstElementChild; $node !== null; $node = $node->nextElementSibling) {
$key = $node->localName;
$previous = $children[$key] ?? null;
$current = Element::fromDOMElement($node);

$children[$key] = match(true) {
$previous instanceof ElementList => $previous->append($current),
$previous instanceof Element => new ElementList($previous, $current),
default => $current,
};
}

return new self($root, $children);
}

/**
* The child element with this local name, or the list of them when it repeats. Null when there is none.
*/
public function element(string $name): Element|ElementList|null
{
return $this->children[$name] ?? null;
}

/**
* Every child element, keyed by local name in document order. Repeated children are grouped in one list.
*
* @return array<string, Element|ElementList>
*/
public function children(): array
{
return $this->children;
}

/**
* The value of the attribute in this namespace, where '' is the attribute without namespace.
* Null when absent, also when an attribute with this local name exists in another namespace.
*/
public function attribute(string $localName, string $namespace = ''): ?string
{
return $this->root->getAttributeNS($namespace, $localName);
}

/**
* The text content of an element without child elements. Null when it has child elements or is xsi:nil.
*/
public function text(): ?string
{
if ($this->children || $this->root->getAttributeNS(Xmlns::xsi()->value(), 'nil') === 'true') {
return null;
}

return $this->root->textContent ?? '';
}
}
69 changes: 0 additions & 69 deletions src/Xml/Reader/DocumentToLookupArrayReader.php

This file was deleted.

Loading
Loading