Skip to content

Commit d12cf90

Browse files
authored
Merge pull request #1 from veewee/wsdl-flattener
Wsdl flattener
2 parents 8cf9c1a + 840575b commit d12cf90

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1633
-5
lines changed

.phive/phars.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phive xmlns="https://phar.io/phive">
3-
<phar name="psalm" version="^4.13.1" installed="4.13.1" location="./tools/psalm.phar" copy="true"/>
3+
<phar name="psalm" version="^4.19.0" installed="4.19.0" location="./tools/psalm.phar" copy="true"/>
44
<phar name="php-cs-fixer" version="^3.3.2" installed="3.3.2" location="./tools/php-cs-fixer.phar" copy="true"/>
55
</phive>

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ $loader = new StreamWrapperLoader(
5656
$contents = $loader($wsdl);
5757
```
5858

59+
### FlatteningLoader
60+
61+
This loader can be used if your WSDL file contains WSDL or XSD imports.
62+
It will any other loader internally to load all the parts.
63+
The result of this loader is a completely flattened WSDL file which you can e.g. cache on your local filesystem.
64+
65+
```php
66+
use Soap\Wsdl\Loader\FlatteningLoader;
67+
use Soap\Wsdl\Loader\StreamWrapperLoader;
68+
69+
$loader = new FlatteningLoader(new StreamWrapperLoader());
70+
71+
$contents = $loader($wsdl);
72+
```
73+
5974

6075
## WSDL Validators
6176

@@ -92,4 +107,3 @@ $wsdl = Document::fromXmlString((new StreamWrapperLoader())($file));
92107
echo "Validating WSDL:".PHP_EOL;
93108
$issues = $wsdl->validate(new Validator\WsdlSyntaxValidator());
94109
echo ($issues->count() ? $issues->toString() : '🟢 ALL GOOD').PHP_EOL;
95-
```

composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121
],
2222
"require": {
2323
"php": "^8.0",
24+
"ext-dom": "*",
25+
"azjezz/psl": "^1.9",
26+
"league/uri": "^6.5",
27+
"league/uri-components": "^2.4",
2428
"php-soap/xml": "^1.2",
25-
"veewee/xml": "^1.1"
29+
"veewee/xml": "~1.2"
2630
},
2731
"require-dev": {
2832
"phpunit/phpunit": "^9.5"

phpunit.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
<phpunit bootstrap="./tests/bootstrap.php" colors="true">
1+
<phpunit
2+
bootstrap="./tests/bootstrap.php"
3+
colors="true"
4+
convertDeprecationsToExceptions="false"
5+
>
26
<testsuites>
37
<testsuite name="Unit">
48
<directory>./tests/Unit</directory>

psalm.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
<psalm
33
errorLevel="1"
44
resolveFromConfigFile="true"
5-
forbidEcho="true"
65
strictBinaryOperands="true"
76
phpVersion="8.0"
87
allowStringToStandInForClass="true"
@@ -21,4 +20,8 @@
2120
<directory name="tests" />
2221
</ignoreFiles>
2322
</projectFiles>
23+
<ignoreExceptions>
24+
<class name="InvalidArgumentException" />
25+
<class name="Psl\Exception\InvariantViolationException" />
26+
</ignoreExceptions>
2427
</psalm>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Wsdl\Loader\Context;
5+
6+
use DOMElement;
7+
use Soap\Wsdl\Exception\UnloadableWsdlException;
8+
use Soap\Wsdl\Loader\WsdlLoader;
9+
use Soap\Wsdl\Xml\Configurator\FlattenTypes;
10+
use Soap\Wsdl\Xml\Flattener;
11+
use Soap\Xml\Xpath\WsdlPreset;
12+
use VeeWee\Xml\Dom\Document;
13+
use VeeWee\Xml\Exception\RuntimeException;
14+
use function VeeWee\Xml\Dom\Mapper\xml_string;
15+
16+
final class FlatteningContext
17+
{
18+
/**
19+
* XSD import catalog of location => raw (not flattened) xml
20+
*
21+
* @var array<string, string>
22+
*/
23+
private $catalog = [];
24+
25+
public static function forWsdl(
26+
string $location,
27+
Document $wsdl,
28+
WsdlLoader $loader,
29+
): self {
30+
$new = new self($wsdl, $loader);
31+
$new->catalog[$location] = $wsdl->map(xml_string());
32+
33+
return $new;
34+
}
35+
36+
private function __construct(
37+
private Document $wsdl,
38+
private WsdlLoader $loader
39+
) {
40+
}
41+
42+
/**
43+
* This function can be used to detect if the context knows about a part of the WSDL.
44+
* It knows about a part from the moment that the raw XML version has been loaded once,
45+
* even if the flattening process is still in an underlying import / include.
46+
*/
47+
public function knowsAboutPart(string $location): bool
48+
{
49+
return array_key_exists($location, $this->catalog);
50+
}
51+
52+
/**
53+
* Imports and include only need to occur once.
54+
* This function determines if an import should be done.
55+
*
56+
* It either returns null if the import already was done or the flattened XML if it still requires an import.
57+
*/
58+
public function import(string $location): ?string
59+
{
60+
return $this->knowsAboutPart($location)
61+
? null
62+
: $this->loadFlattenedXml($location);
63+
}
64+
65+
/**
66+
* Returns the base WSDL document that can be worked on by flattener configurators.
67+
*/
68+
public function wsdl(): Document
69+
{
70+
return $this->wsdl;
71+
}
72+
73+
/**
74+
* This method searches for a single <wsdl:types /> tag
75+
* If no tag exists, it will create an empty one.
76+
* If multiple tags exist, it will merge those tags into one.
77+
*
78+
* @throws RuntimeException
79+
*/
80+
public function types(): DOMElement
81+
{
82+
$doc = Document::fromUnsafeDocument($this->wsdl->toUnsafeDocument(), new FlattenTypes());
83+
$xpath = $doc->xpath(new WsdlPreset($doc));
84+
85+
/** @var DOMElement $types */
86+
$types = $xpath->querySingle('//wsdl:types');
87+
88+
return $types;
89+
}
90+
91+
/**
92+
* This function will take care of the import catalog!
93+
* It will first load the raw xml from the remote source and store that internally.
94+
*
95+
* Next it will apply the XML flattening on the loaded xml and return the flattened string.
96+
* We keep track of all nested flattening locations that are in progress.
97+
* This way we can prevent circular includes as well.
98+
*
99+
* @throws UnloadableWsdlException
100+
* @throws RuntimeException
101+
*/
102+
private function loadFlattenedXml(string $location): string
103+
{
104+
if (!array_key_exists($location, $this->catalog)) {
105+
$this->catalog[$location] = ($this->loader)($location);
106+
}
107+
108+
$document = Document::fromXmlString($this->catalog[$location]);
109+
110+
return (new Flattener())($location, $document, $this);
111+
}
112+
}

src/Loader/FlatteningLoader.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Wsdl\Loader;
5+
6+
use Soap\Wsdl\Exception\UnloadableWsdlException;
7+
use Soap\Wsdl\Loader\Context\FlatteningContext;
8+
use Soap\Wsdl\Xml\Flattener;
9+
use VeeWee\Xml\Dom\Document;
10+
use VeeWee\Xml\Exception\RuntimeException;
11+
12+
final class FlatteningLoader implements WsdlLoader
13+
{
14+
public function __construct(
15+
private WsdlLoader $loader,
16+
) {
17+
}
18+
19+
/**
20+
* @throws RuntimeException
21+
* @throws UnloadableWsdlException
22+
*/
23+
public function __invoke(string $location): string
24+
{
25+
$currentDoc = Document::fromXmlString(($this->loader)($location));
26+
$context = FlatteningContext::forWsdl($location, $currentDoc, $this->loader);
27+
28+
return (new Flattener())($location, $currentDoc, $context);
29+
}
30+
}

src/Uri/IncludePathBuilder.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Soap\Wsdl\Uri;
6+
7+
use League\Uri\Uri;
8+
use League\Uri\UriModifier;
9+
use League\Uri\UriResolver;
10+
11+
final class IncludePathBuilder
12+
{
13+
public static function build(string $relativePath, string $fromFile): string
14+
{
15+
return UriModifier::removeEmptySegments(
16+
UriModifier::removeDotSegments(
17+
UriResolver::resolve(
18+
Uri::createFromString($relativePath),
19+
Uri::createFromString($fromFile)
20+
)
21+
)
22+
)->__toString();
23+
}
24+
}

src/Xml/Configurator/FlattenTypes.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Wsdl\Xml\Configurator;
5+
6+
use DOMDocument;
7+
use DOMElement;
8+
use Soap\Xml\Xmlns;
9+
use Soap\Xml\Xpath\WsdlPreset;
10+
use VeeWee\Xml\Dom\Configurator\Configurator;
11+
use VeeWee\Xml\Dom\Document;
12+
use VeeWee\Xml\Exception\RuntimeException;
13+
use function VeeWee\Xml\Dom\Builder\namespaced_element;
14+
use function VeeWee\Xml\Dom\Locator\Node\children;
15+
use function VeeWee\Xml\Dom\Manipulator\append;
16+
use function VeeWee\Xml\Dom\Manipulator\Node\remove;
17+
18+
/**
19+
* This class transforms multiple wsdl:types elements into 1 single element.
20+
* This makes importing xsd's easier (and prevents some bugs in some soap related tools)
21+
*/
22+
final class FlattenTypes implements Configurator
23+
{
24+
/**
25+
* @throws RuntimeException
26+
*/
27+
public function __invoke(DOMDocument $document): DOMDocument
28+
{
29+
$xml = Document::fromUnsafeDocument($document);
30+
$xpath = $xml->xpath(new WsdlPreset($xml));
31+
/** @var list<DOMElement> $types */
32+
$types = [...$xpath->query('wsdl:types')];
33+
34+
// Creates wsdl:types if no matching element exists yet
35+
if (!count($types)) {
36+
$document->documentElement->append(
37+
namespaced_element(Xmlns::wsdl()->value(), 'types')($document)
38+
);
39+
40+
return $document;
41+
}
42+
43+
// Skip if only one exists
44+
$first = array_shift($types);
45+
if (!count($types)) {
46+
return $document;
47+
}
48+
49+
// Flattens multiple wsdl:types elements.
50+
foreach ($types as $additionalTypes) {
51+
$children = children($additionalTypes);
52+
if (count($children)) {
53+
append(...$children)($first);
54+
}
55+
56+
remove($additionalTypes);
57+
}
58+
59+
return $document;
60+
}
61+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Soap\Wsdl\Xml\Configurator;
6+
7+
use DOMDocument;
8+
use DOMElement;
9+
use Soap\Wsdl\Exception\UnloadableWsdlException;
10+
use Soap\Wsdl\Loader\Context\FlatteningContext;
11+
use Soap\Wsdl\Uri\IncludePathBuilder;
12+
use Soap\Xml\Xpath\WsdlPreset;
13+
use VeeWee\Xml\Dom\Configurator\Configurator;
14+
use VeeWee\Xml\Dom\Document;
15+
use VeeWee\Xml\Exception\RuntimeException;
16+
use function VeeWee\Xml\Dom\Locator\document_element;
17+
use function VeeWee\Xml\Dom\Locator\Node\children;
18+
use function VeeWee\Xml\Dom\Manipulator\Node\remove;
19+
use function VeeWee\Xml\Dom\Manipulator\Node\replace_by_external_nodes;
20+
21+
final class FlattenWsdlImports implements Configurator
22+
{
23+
public function __construct(
24+
private string $currentLocation,
25+
private FlatteningContext $context
26+
) {
27+
}
28+
29+
/**
30+
* This method flattens wsdl:import locations.
31+
* It loads the WSDL and adds the definitions replaces the import tag with the definition children from the external file.
32+
*
33+
* For now, we don't care about the namespace property on the wsdl:import tag.
34+
* Future reference:
35+
* @link http://itdoc.hitachi.co.jp/manuals/3020/30203Y2310e/EY230669.HTM#ID01496
36+
*
37+
* @throws RuntimeException
38+
* @throws UnloadableWsdlException
39+
*/
40+
public function __invoke(DOMDocument $document): DOMDocument
41+
{
42+
$xml = Document::fromUnsafeDocument($document);
43+
$xpath = $xml->xpath(new WsdlPreset($xml));
44+
45+
$imports = $xpath->query('wsdl:import');
46+
$imports->forEach(fn (DOMElement $import) => $this->importWsdlImportElement($import));
47+
48+
return $document;
49+
}
50+
51+
/**
52+
* @throws RuntimeException
53+
* @throws UnloadableWsdlException
54+
*/
55+
private function importWsdlImportElement(DOMElement $import): void
56+
{
57+
$location = IncludePathBuilder::build(
58+
$import->getAttribute('location'),
59+
$this->currentLocation
60+
);
61+
62+
$result = $this->context->import($location);
63+
if (!$result) {
64+
remove($import);
65+
return;
66+
}
67+
68+
$imported = Document::fromXmlString($result);
69+
$definitions = $imported->map(document_element());
70+
71+
replace_by_external_nodes(
72+
$import,
73+
children($definitions)
74+
);
75+
}
76+
}

0 commit comments

Comments
 (0)