Skip to content

Commit

Permalink
Introduce soap-fault support
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Jun 7, 2024
1 parent d0ac7fd commit 96f2580
Show file tree
Hide file tree
Showing 14 changed files with 678 additions and 3 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"php-soap/wsdl": "^1.6",
"php-soap/xml": "^1.7",
"php-soap/wsdl-reader": "0.14.0",
"goetas-webservices/xsd-reader": "^0.4.5"
"goetas-webservices/xsd-reader": "^0.4.6"
},
"require-dev": {
"vimeo/psalm": "^5.16",
Expand Down
28 changes: 28 additions & 0 deletions src/Exception/SoapFaultException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Exception;

use Psl\Str;
use RuntimeException;
use Soap\Encoding\Fault\SoapFault;

final class SoapFaultException extends RuntimeException implements ExceptionInterface
{
public function __construct(
private readonly SoapFault $fault
) {
parent::__construct(
Str\format(
'A SOAP Fault got triggered: %s (Code: %s)',
$this->fault->reason(),
$this->fault->code(),
),
);
}

public function fault(): SoapFault
{
return $this->fault;
}
}
98 changes: 98 additions & 0 deletions src/Fault/Encoder/Soap11FaultEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Fault\Encoder;

use Soap\Encoding\Fault\Soap11Fault;
use Soap\Xml\Xmlns;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Writer\Writer;
use function Psl\invariant;
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces;
use function VeeWee\Xml\Writer\Builder\children;
use function VeeWee\Xml\Writer\Builder\element;
use function VeeWee\Xml\Writer\Builder\namespaced_element;
use function VeeWee\Xml\Writer\Builder\raw;
use function VeeWee\Xml\Writer\Builder\value;
use function VeeWee\Xml\Writer\Mapper\memory_output;

/**
* @implements SoapFaultEncoder<Soap11Fault>
*/
final class Soap11FaultEncoder implements SoapFaultEncoder
{
/**
* @return Iso<Soap11Fault, non-empty-string>
*/
public function iso(): Iso
{
return new Iso(
$this->to(...),
$this->from(...)
);
}

/**
* @return non-empty-string
*/
private function to(Soap11Fault $fault): string
{
$envNamespace = Xmlns::soap11Envelope()->value();

/** @var non-empty-string */
return Writer::inMemory()
->write(children([
namespaced_element(
$envNamespace,
'env',
'Fault',
children([
element(
'faultcode',
value($fault->faultCode),
),
element(
'faultstring',
value($fault->faultString),
),
...(
$fault->faultActor !== null
? [
element(
'faultactor',
value($fault->faultActor)
)
]
: []
),
...($fault->detail !== null ? [raw($fault->detail)] : []),
])
)
]))
->map(memory_output());
}

/**
* @param non-empty-string $fault
*/
private function from(string $fault): Soap11Fault
{
$document = Document::fromXmlString($fault);
$documentElement = $document->locateDocumentElement();

$envelopeUri = $documentElement->namespaceURI;
invariant($envelopeUri !== null, 'No SoapFault envelope namespace uri was specified.');
$xpath = $document->xpath(namespaces(['env' => $envelopeUri]));

$actor = $xpath->query('./faultactor');
$detail = $xpath->query('./detail');

return new Soap11Fault(
faultCode: $xpath->querySingle('./faultcode')->textContent,
faultString: $xpath->querySingle('./faultstring')->textContent,
faultActor: $actor->count() ? $actor->expectFirst()->textContent : null,
detail: $detail->count() ? Document::fromXmlNode($detail->expectFirst())->stringifyDocumentElement() : null,
);
}
}
145 changes: 145 additions & 0 deletions src/Fault/Encoder/Soap12FaultEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Fault\Encoder;

use Soap\Encoding\Fault\Soap12Fault;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Writer\Writer;
use function Psl\invariant;
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces;
use function VeeWee\Xml\Writer\Builder\children;
use function VeeWee\Xml\Writer\Builder\namespaced_element;
use function VeeWee\Xml\Writer\Builder\prefixed_element;
use function VeeWee\Xml\Writer\Builder\raw;
use function VeeWee\Xml\Writer\Builder\value;
use function VeeWee\Xml\Writer\Mapper\memory_output;

/**
* @implements SoapFaultEncoder<Soap12Fault>
*/
final class Soap12FaultEncoder implements SoapFaultEncoder
{
private const ENV_NAMESPACE = 'http://www.w3.org/2003/05/soap-envelope';

/**
* @return Iso<Soap12Fault, non-empty-string>
*/
public function iso(): Iso
{
return new Iso(
$this->to(...),
$this->from(...)
);
}

/**
* @return non-empty-string
*/
private function to(Soap12Fault $fault): string
{
/** @var non-empty-string */
return Writer::inMemory()
->write(children([
namespaced_element(
self::ENV_NAMESPACE,
'env',
'Fault',
children([
prefixed_element(
'env',
'Code',
children([
prefixed_element(
'env',
'Value',
value($fault->code)
),
...(
$fault->subCode !== null
? [
prefixed_element(
'env',
'Subcode',
children([
prefixed_element(
'env',
'Value',
value($fault->subCode)
)
])
)
]
: []
),

])
),
prefixed_element(
'env',
'Reason',
children([
prefixed_element(
'env',
'Text',
value($fault->reason)
)
])
),
...(
$fault->node !== null
? [
prefixed_element(
'env',
'Node',
value($fault->node)
)
]
: []
),
...(
$fault->role !== null
? [
prefixed_element(
'env',
'Role',
value($fault->role)
)
]
: []
),
...($fault->detail !== null ? [raw($fault->detail)] : []),
])
)
]))
->map(memory_output());
}

/**
* @param non-empty-string $fault
*/
private function from(string $fault): Soap12Fault
{
$document = Document::fromXmlString($fault);
$documentElement = $document->locateDocumentElement();

$envelopeUri = $documentElement->namespaceURI;
invariant($envelopeUri !== null, 'No SoapFault envelope namespace uri was specified.');
$xpath = $document->xpath(namespaces(['env' => $envelopeUri]));

$subCode = $xpath->query('./env:Code/env:Subcode/env:Value');
$node = $xpath->query('./env:Node');
$role = $xpath->query('./env:Role');
$detail = $xpath->query('./env:Detail');

return new Soap12Fault(
code: $xpath->querySingle('./env:Code/env:Value')->textContent,
reason: $xpath->querySingle('./env:Reason/env:Text')->textContent,
subCode: $subCode->count() ? $subCode->expectFirst()->textContent : null,
node: $node->count() ? $node->expectFirst()->textContent : null,
role: $role->count() ? $role->expectFirst()->textContent : null,
detail: $detail->count() ? Document::fromXmlNode($detail->expectFirst())->stringifyDocumentElement() : null,
);
}
}
18 changes: 18 additions & 0 deletions src/Fault/Encoder/SoapFaultEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Fault\Encoder;

use Soap\Encoding\Fault\SoapFault;
use VeeWee\Reflecta\Iso\Iso;

/**
* @template TFault of SoapFault
*/
interface SoapFaultEncoder
{
/**
* @return Iso<TFault, non-empty-string>
*/
public function iso(): Iso;
}
41 changes: 41 additions & 0 deletions src/Fault/Guard/SoapFaultGuard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Fault\Guard;

use Soap\Encoding\Exception\SoapFaultException;
use Soap\Encoding\Fault\Encoder\Soap11FaultEncoder;
use Soap\Encoding\Fault\Encoder\Soap12FaultEncoder;
use Soap\Xml\Xmlns;
use VeeWee\Xml\Dom\Document;
use function Psl\invariant;
use function VeeWee\Xml\Dom\Xpath\Configurator\namespaces;

final class SoapFaultGuard
{
/**
* @throws SoapFaultException
*/
public function __invoke(Document $envelope): void
{
$envelopeUri = $envelope->locateDocumentElement()->namespaceURI;
invariant($envelopeUri !== null, 'No SoapFault envelope namespace uri was specified.');
$xpath = $envelope->xpath(namespaces([
'env' => $envelopeUri,
]));

$fault = $xpath->query('//env:Fault');
if (!$fault->count()) {
return;
}

$faultXml = Document::fromXmlNode($fault->expectFirst())->stringifyDocumentElement();

$fault = match($envelopeUri) {
Xmlns::soap11Envelope()->value() => (new Soap11FaultEncoder())->iso()->from($faultXml),
default => (new Soap12FaultEncoder())->iso()->from($faultXml),
};

throw new SoapFaultException($fault);
}
}
38 changes: 38 additions & 0 deletions src/Fault/Soap11Fault.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Fault;

/**
* @see https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383507
*
* A mandatory faultcode element information item
* A mandatory faultstring element information item
* An optional faultactor element information item
* An optional detail element information item
*/
final class Soap11Fault implements SoapFault
{
public function __construct(
public readonly string $faultCode,
public readonly string $faultString,
public readonly ?string $faultActor = null,
public readonly ?string $detail = null
) {
}

public function code(): string
{
return $this->faultCode;
}

public function reason(): string
{
return $this->faultString;
}

public function detail(): ?string
{
return $this->detail;
}
}
Loading

0 comments on commit 96f2580

Please sign in to comment.