Skip to content

Commit 0271ab5

Browse files
committed
Introduce element aware encoders
1 parent a8f06bf commit 0271ab5

File tree

9 files changed

+121
-8
lines changed

9 files changed

+121
-8
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types=1);
2+
require_once \dirname(__DIR__, 3) . '/vendor/autoload.php';
3+
4+
use Soap\Encoding\Encoder\Context;
5+
use Soap\Encoding\Encoder\Feature\ElementAware;
6+
use Soap\Encoding\Encoder\SimpleType\ScalarTypeEncoder;
7+
use Soap\Encoding\Encoder\XmlEncoder;
8+
use Soap\Encoding\EncoderRegistry;
9+
use VeeWee\Reflecta\Iso\Iso;
10+
use function VeeWee\Xml\Encoding\document_encode;
11+
use function VeeWee\Xml\Encoding\xml_decode;
12+
13+
/**
14+
* Most of the time, you don't need access to the wrapping XML element from within a simple type encoder.
15+
* Sometimes, when using anyXml or anyType, you might want to have full control over the wrapping element.
16+
* This allows you to use 3rd party tools to build the full XML structure from within a simple type encoder.
17+
*
18+
* Do note that:
19+
* - You'll need to check if the current provided type is an attribute or not.
20+
* - If you want to add xsi:type information, you need to add / parse it manually.
21+
* - The result will be used as a raw XML input, meaning it should be valid XML (without the header declearations).
22+
*/
23+
EncoderRegistry::default()
24+
->addSimpleTypeConverter(
25+
'http://www.w3.org/2001/XMLSchema',
26+
'anyXml',
27+
new class implements XmlEncoder, ElementAware {
28+
public function iso(Context $context): Iso
29+
{
30+
if ($context->type->getMeta()->isAttribute()->unwrapOr(false)) {
31+
return (new ScalarTypeEncoder())->iso($context);
32+
}
33+
34+
$targetElementName = $context->type->getXmlTargetNodeName();
35+
return new Iso(
36+
to: static fn (array $data): string => document_encode([$targetElementName => $data])
37+
->manipulate(static fn (\DOMDocument $document) => $document->documentElement->setAttributeNS(
38+
VeeWee\Xml\Xmlns\Xmlns::xsi()->value(),
39+
'xsi:type',
40+
'custom:type'
41+
))
42+
->stringifyDocumentElement(),
43+
from: static fn (string $xml): array => xml_decode($xml)[$targetElementName],
44+
);
45+
}
46+
}
47+
);

src/Encoder/ElementEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
/**
1313
* @implements XmlEncoder<mixed, string>
1414
*/
15-
final class ElementEncoder implements XmlEncoder
15+
final class ElementEncoder implements XmlEncoder, Feature\ElementAware
1616
{
1717
/**
1818
* @param XmlEncoder<mixed, string> $typeEncoder

src/Encoder/Feature/ElementAware.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Soap\Encoding\Encoder\Feature;
4+
5+
/**
6+
* Tells the encoder knows how to encode elements.
7+
* It can be used on simpleType encoders so that you get in control about the wrapping XML element.
8+
*/
9+
interface ElementAware
10+
{
11+
}

src/Encoder/ObjectEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
*
2828
* @implements XmlEncoder<TObj|array, non-empty-string>
2929
*/
30-
final class ObjectEncoder implements XmlEncoder
30+
final class ObjectEncoder implements XmlEncoder, Feature\ElementAware
3131
{
3232
/**
3333
* @param class-string<TObj> $className

src/Encoder/OptionalElementEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* @template T of mixed
1414
* @implements XmlEncoder<T, string>
1515
*/
16-
final class OptionalElementEncoder implements Feature\OptionalAware, XmlEncoder
16+
final class OptionalElementEncoder implements Feature\OptionalAware, Feature\ElementAware, XmlEncoder
1717
{
1818
/**
1919
* @param XmlEncoder<T, string> $elementEncoder

src/Encoder/RepeatingElementEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* @template T
1515
* @implements XmlEncoder<iterable<array-key, T>|null, string>
1616
*/
17-
final class RepeatingElementEncoder implements Feature\ListAware, XmlEncoder
17+
final class RepeatingElementEncoder implements Feature\ListAware, Feature\ElementAware, XmlEncoder
1818
{
1919
/**
2020
* @param XmlEncoder<T, string> $typeEncoder

src/Encoder/SimpleType/EncoderDetector.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ public function __invoke(Context $context): XmlEncoder
3939
}
4040

4141
if ($meta->isElement()->unwrapOr(false)) {
42-
$encoder = new ElementEncoder($encoder);
42+
if (!$encoder instanceof Feature\ElementAware) {
43+
$encoder = new ElementEncoder($encoder);
44+
}
4345

44-
if ($meta->isNullable()->unwrapOr(false)) {
46+
if ($meta->isNullable()->unwrapOr(false) && !$encoder instanceof Feature\OptionalAware) {
4547
$encoder = new OptionalElementEncoder($encoder);
4648
}
4749
}

tests/Unit/ContextCreatorTrait.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ trait ContextCreatorTrait
2222
{
2323
public static function createContext(
2424
XsdType $currentType,
25-
TypeCollection $allTypes = new TypeCollection()
25+
TypeCollection $allTypes = new TypeCollection(),
26+
?EncoderRegistry $encoderRegistry = null
2627
): Context {
2728
return new Context(
2829
$currentType,
2930
new InMemoryMetadata(
3031
$allTypes,
3132
new MethodCollection(),
3233
),
33-
EncoderRegistry::default(),
34+
$encoderRegistry ?? EncoderRegistry::default(),
3435
self::buildNamespaces(),
3536
);
3637
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Soap\Encoding\Test\Unit\Encoder\Feature;
4+
5+
use PHPUnit\Framework\Attributes\CoversClass;
6+
use Soap\Encoding\Encoder\Context;
7+
use Soap\Encoding\Encoder\Feature\ElementAware;
8+
use Soap\Encoding\Encoder\SimpleType\EncoderDetector;
9+
use Soap\Encoding\Encoder\XmlEncoder;
10+
use Soap\Encoding\EncoderRegistry;
11+
use Soap\Encoding\Test\Unit\Encoder\AbstractEncoderTests;
12+
use Soap\Engine\Metadata\Model\XsdType;
13+
use Soap\Xml\Xmlns;
14+
use VeeWee\Reflecta\Iso\Iso;
15+
use function VeeWee\Xml\Encoding\document_encode;
16+
use function VeeWee\Xml\Encoding\xml_decode;
17+
18+
#[CoversClass(EncoderDetector::class)]
19+
final class ElementAwareEncoderTest extends AbstractEncoderTests
20+
{
21+
public static function provideIsomorphicCases(): iterable
22+
{
23+
$registry = EncoderRegistry::default()
24+
->addSimpleTypeConverter(
25+
Xmlns::xsd()->value(),
26+
'anyType',
27+
new class implements XmlEncoder, ElementAware {
28+
public function iso(Context $context): Iso
29+
{
30+
$typeName = $context->type->getXmlTargetNodeName();
31+
return new Iso(
32+
to: static fn (array $data): string => document_encode([$typeName => $data])->stringifyDocumentElement(),
33+
from: static fn (string $xml): array => xml_decode($xml)[$typeName],
34+
);
35+
}
36+
}
37+
);
38+
39+
$context = self::createContext(
40+
XsdType::any()->withXmlTargetNodeName('data'),
41+
encoderRegistry: $registry
42+
);
43+
$encoder = $registry->detectEncoderForContext($context);
44+
45+
yield 'element-aware-simple-type' => [
46+
'encoder' => $encoder,
47+
'context' => $context,
48+
'xml' => '<data><key>value</key></data>',
49+
'data' => ['key' => 'value'],
50+
];
51+
}
52+
}

0 commit comments

Comments
 (0)