Skip to content

Commit ac81f2e

Browse files
authored
Merge pull request #3 from php-soap/soap-fault
Introduce soap-fault support
2 parents d0ac7fd + d243656 commit ac81f2e

14 files changed

+680
-3
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"php-soap/wsdl": "^1.6",
2929
"php-soap/xml": "^1.7",
3030
"php-soap/wsdl-reader": "0.14.0",
31-
"goetas-webservices/xsd-reader": "^0.4.5"
31+
"goetas-webservices/xsd-reader": "^0.4.6"
3232
},
3333
"require-dev": {
3434
"vimeo/psalm": "^5.16",

src/Exception/SoapFaultException.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Exception;
5+
6+
use Psl\Str;
7+
use RuntimeException;
8+
use Soap\Encoding\Fault\SoapFault;
9+
10+
final class SoapFaultException extends RuntimeException implements ExceptionInterface
11+
{
12+
public function __construct(
13+
private readonly SoapFault $fault
14+
) {
15+
parent::__construct(
16+
Str\format(
17+
'SOAP Fault: %s (Code: %s)',
18+
$this->fault->reason(),
19+
$this->fault->code(),
20+
),
21+
);
22+
}
23+
24+
public function fault(): SoapFault
25+
{
26+
return $this->fault;
27+
}
28+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Fault\Encoder;
5+
6+
use Soap\Encoding\Fault\Soap11Fault;
7+
use Soap\Encoding\Restriction\WhitespaceRestriction;
8+
use Soap\Xml\Xmlns;
9+
use VeeWee\Reflecta\Iso\Iso;
10+
use VeeWee\Xml\Dom\Document;
11+
use VeeWee\Xml\Writer\Writer;
12+
use function Psl\invariant;
13+
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces;
14+
use function VeeWee\Xml\Writer\Builder\children;
15+
use function VeeWee\Xml\Writer\Builder\element;
16+
use function VeeWee\Xml\Writer\Builder\namespaced_element;
17+
use function VeeWee\Xml\Writer\Builder\raw;
18+
use function VeeWee\Xml\Writer\Builder\value;
19+
use function VeeWee\Xml\Writer\Mapper\memory_output;
20+
21+
/**
22+
* @implements SoapFaultEncoder<Soap11Fault>
23+
*/
24+
final class Soap11FaultEncoder implements SoapFaultEncoder
25+
{
26+
/**
27+
* @return Iso<Soap11Fault, non-empty-string>
28+
*/
29+
public function iso(): Iso
30+
{
31+
return new Iso(
32+
$this->to(...),
33+
$this->from(...)
34+
);
35+
}
36+
37+
/**
38+
* @return non-empty-string
39+
*/
40+
private function to(Soap11Fault $fault): string
41+
{
42+
$envNamespace = Xmlns::soap11Envelope()->value();
43+
44+
/** @var non-empty-string */
45+
return Writer::inMemory()
46+
->write(children([
47+
namespaced_element(
48+
$envNamespace,
49+
'env',
50+
'Fault',
51+
children([
52+
element(
53+
'faultcode',
54+
value($fault->faultCode),
55+
),
56+
element(
57+
'faultstring',
58+
value($fault->faultString),
59+
),
60+
...(
61+
$fault->faultActor !== null
62+
? [
63+
element(
64+
'faultactor',
65+
value($fault->faultActor)
66+
)
67+
]
68+
: []
69+
),
70+
...($fault->detail !== null ? [raw($fault->detail)] : []),
71+
])
72+
)
73+
]))
74+
->map(memory_output());
75+
}
76+
77+
/**
78+
* @param non-empty-string $fault
79+
*/
80+
private function from(string $fault): Soap11Fault
81+
{
82+
$document = Document::fromXmlString($fault);
83+
$documentElement = $document->locateDocumentElement();
84+
85+
$envelopeUri = $documentElement->namespaceURI;
86+
invariant($envelopeUri !== null, 'No SoapFault envelope namespace uri was specified.');
87+
$xpath = $document->xpath(namespaces(['env' => $envelopeUri]));
88+
89+
$actor = $xpath->query('./faultactor');
90+
$detail = $xpath->query('./detail');
91+
92+
return new Soap11Fault(
93+
faultCode: WhitespaceRestriction::collapse($xpath->querySingle('./faultcode')->textContent),
94+
faultString: WhitespaceRestriction::collapse($xpath->querySingle('./faultstring')->textContent),
95+
faultActor: $actor->count() ? trim($actor->expectFirst()->textContent) : null,
96+
detail: $detail->count() ? Document::fromXmlNode($detail->expectFirst())->stringifyDocumentElement() : null,
97+
);
98+
}
99+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Fault\Encoder;
5+
6+
use Soap\Encoding\Fault\Soap12Fault;
7+
use Soap\Encoding\Restriction\WhitespaceRestriction;
8+
use VeeWee\Reflecta\Iso\Iso;
9+
use VeeWee\Xml\Dom\Document;
10+
use VeeWee\Xml\Writer\Writer;
11+
use function Psl\invariant;
12+
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces;
13+
use function VeeWee\Xml\Writer\Builder\children;
14+
use function VeeWee\Xml\Writer\Builder\namespaced_element;
15+
use function VeeWee\Xml\Writer\Builder\prefixed_element;
16+
use function VeeWee\Xml\Writer\Builder\raw;
17+
use function VeeWee\Xml\Writer\Builder\value;
18+
use function VeeWee\Xml\Writer\Mapper\memory_output;
19+
20+
/**
21+
* @implements SoapFaultEncoder<Soap12Fault>
22+
*/
23+
final class Soap12FaultEncoder implements SoapFaultEncoder
24+
{
25+
private const ENV_NAMESPACE = 'http://www.w3.org/2003/05/soap-envelope';
26+
27+
/**
28+
* @return Iso<Soap12Fault, non-empty-string>
29+
*/
30+
public function iso(): Iso
31+
{
32+
return new Iso(
33+
$this->to(...),
34+
$this->from(...)
35+
);
36+
}
37+
38+
/**
39+
* @return non-empty-string
40+
*/
41+
private function to(Soap12Fault $fault): string
42+
{
43+
/** @var non-empty-string */
44+
return Writer::inMemory()
45+
->write(children([
46+
namespaced_element(
47+
self::ENV_NAMESPACE,
48+
'env',
49+
'Fault',
50+
children([
51+
prefixed_element(
52+
'env',
53+
'Code',
54+
children([
55+
prefixed_element(
56+
'env',
57+
'Value',
58+
value($fault->code)
59+
),
60+
...(
61+
$fault->subCode !== null
62+
? [
63+
prefixed_element(
64+
'env',
65+
'Subcode',
66+
children([
67+
prefixed_element(
68+
'env',
69+
'Value',
70+
value($fault->subCode)
71+
)
72+
])
73+
)
74+
]
75+
: []
76+
),
77+
78+
])
79+
),
80+
prefixed_element(
81+
'env',
82+
'Reason',
83+
children([
84+
prefixed_element(
85+
'env',
86+
'Text',
87+
value($fault->reason)
88+
)
89+
])
90+
),
91+
...(
92+
$fault->node !== null
93+
? [
94+
prefixed_element(
95+
'env',
96+
'Node',
97+
value($fault->node)
98+
)
99+
]
100+
: []
101+
),
102+
...(
103+
$fault->role !== null
104+
? [
105+
prefixed_element(
106+
'env',
107+
'Role',
108+
value($fault->role)
109+
)
110+
]
111+
: []
112+
),
113+
...($fault->detail !== null ? [raw($fault->detail)] : []),
114+
])
115+
)
116+
]))
117+
->map(memory_output());
118+
}
119+
120+
/**
121+
* @param non-empty-string $fault
122+
*/
123+
private function from(string $fault): Soap12Fault
124+
{
125+
$document = Document::fromXmlString($fault);
126+
$documentElement = $document->locateDocumentElement();
127+
128+
$envelopeUri = $documentElement->namespaceURI;
129+
invariant($envelopeUri !== null, 'No SoapFault envelope namespace uri was specified.');
130+
$xpath = $document->xpath(namespaces(['env' => $envelopeUri]));
131+
132+
$subCode = $xpath->query('./env:Code/env:Subcode/env:Value');
133+
$node = $xpath->query('./env:Node');
134+
$role = $xpath->query('./env:Role');
135+
$detail = $xpath->query('./env:Detail');
136+
137+
return new Soap12Fault(
138+
code: WhitespaceRestriction::collapse($xpath->querySingle('./env:Code/env:Value')->textContent),
139+
reason: WhitespaceRestriction::collapse($xpath->querySingle('./env:Reason/env:Text')->textContent),
140+
subCode: $subCode->count() ? WhitespaceRestriction::collapse($subCode->expectFirst()->textContent) : null,
141+
node: $node->count() ? trim($node->expectFirst()->textContent) : null,
142+
role: $role->count() ? trim($role->expectFirst()->textContent) : null,
143+
detail: $detail->count() ? Document::fromXmlNode($detail->expectFirst())->stringifyDocumentElement() : null,
144+
);
145+
}
146+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Fault\Encoder;
5+
6+
use Soap\Encoding\Fault\SoapFault;
7+
use VeeWee\Reflecta\Iso\Iso;
8+
9+
/**
10+
* @template TFault of SoapFault
11+
*/
12+
interface SoapFaultEncoder
13+
{
14+
/**
15+
* @return Iso<TFault, non-empty-string>
16+
*/
17+
public function iso(): Iso;
18+
}

src/Fault/Guard/SoapFaultGuard.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Fault\Guard;
5+
6+
use Soap\Encoding\Exception\SoapFaultException;
7+
use Soap\Encoding\Fault\Encoder\Soap11FaultEncoder;
8+
use Soap\Encoding\Fault\Encoder\Soap12FaultEncoder;
9+
use Soap\Xml\Xmlns;
10+
use VeeWee\Xml\Dom\Document;
11+
use function Psl\invariant;
12+
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces;
13+
14+
final class SoapFaultGuard
15+
{
16+
/**
17+
* @throws SoapFaultException
18+
*/
19+
public function __invoke(Document $envelope): void
20+
{
21+
$envelopeUri = $envelope->locateDocumentElement()->namespaceURI;
22+
invariant($envelopeUri !== null, 'No SoapFault envelope namespace uri was specified.');
23+
$xpath = $envelope->xpath(namespaces([
24+
'env' => $envelopeUri,
25+
]));
26+
27+
$fault = $xpath->query('//env:Fault');
28+
if (!$fault->count()) {
29+
return;
30+
}
31+
32+
$faultXml = Document::fromXmlNode($fault->expectFirst())->stringifyDocumentElement();
33+
34+
$fault = match($envelopeUri) {
35+
Xmlns::soap11Envelope()->value() => (new Soap11FaultEncoder())->iso()->from($faultXml),
36+
default => (new Soap12FaultEncoder())->iso()->from($faultXml),
37+
};
38+
39+
throw new SoapFaultException($fault);
40+
}
41+
}

src/Fault/Soap11Fault.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Encoding\Fault;
5+
6+
/**
7+
* @see https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383507
8+
*
9+
* A mandatory faultcode element information item
10+
* A mandatory faultstring element information item
11+
* An optional faultactor element information item
12+
* An optional detail element information item
13+
*/
14+
final class Soap11Fault implements SoapFault
15+
{
16+
public function __construct(
17+
public readonly string $faultCode,
18+
public readonly string $faultString,
19+
public readonly ?string $faultActor = null,
20+
public readonly ?string $detail = null
21+
) {
22+
}
23+
24+
public function code(): string
25+
{
26+
return $this->faultCode;
27+
}
28+
29+
public function reason(): string
30+
{
31+
return $this->faultString;
32+
}
33+
34+
public function detail(): ?string
35+
{
36+
return $this->detail;
37+
}
38+
}

0 commit comments

Comments
 (0)