Skip to content

Commit

Permalink
Merge pull request #1 from veewee/wsdl-flattener
Browse files Browse the repository at this point in the history
Wsdl flattener
  • Loading branch information
veewee authored Jan 28, 2022
2 parents 8cf9c1a + 840575b commit d12cf90
Show file tree
Hide file tree
Showing 63 changed files with 1,633 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .phive/phars.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="psalm" version="^4.13.1" installed="4.13.1" location="./tools/psalm.phar" copy="true"/>
<phar name="psalm" version="^4.19.0" installed="4.19.0" location="./tools/psalm.phar" copy="true"/>
<phar name="php-cs-fixer" version="^3.3.2" installed="3.3.2" location="./tools/php-cs-fixer.phar" copy="true"/>
</phive>
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ $loader = new StreamWrapperLoader(
$contents = $loader($wsdl);
```

### FlatteningLoader

This loader can be used if your WSDL file contains WSDL or XSD imports.
It will any other loader internally to load all the parts.
The result of this loader is a completely flattened WSDL file which you can e.g. cache on your local filesystem.

```php
use Soap\Wsdl\Loader\FlatteningLoader;
use Soap\Wsdl\Loader\StreamWrapperLoader;

$loader = new FlatteningLoader(new StreamWrapperLoader());

$contents = $loader($wsdl);
```


## WSDL Validators

Expand Down Expand Up @@ -92,4 +107,3 @@ $wsdl = Document::fromXmlString((new StreamWrapperLoader())($file));
echo "Validating WSDL:".PHP_EOL;
$issues = $wsdl->validate(new Validator\WsdlSyntaxValidator());
echo ($issues->count() ? $issues->toString() : '🟢 ALL GOOD').PHP_EOL;
```
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@
],
"require": {
"php": "^8.0",
"ext-dom": "*",
"azjezz/psl": "^1.9",
"league/uri": "^6.5",
"league/uri-components": "^2.4",
"php-soap/xml": "^1.2",
"veewee/xml": "^1.1"
"veewee/xml": "~1.2"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
Expand Down
6 changes: 5 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<phpunit bootstrap="./tests/bootstrap.php" colors="true">
<phpunit
bootstrap="./tests/bootstrap.php"
colors="true"
convertDeprecationsToExceptions="false"
>
<testsuites>
<testsuite name="Unit">
<directory>./tests/Unit</directory>
Expand Down
5 changes: 4 additions & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
<psalm
errorLevel="1"
resolveFromConfigFile="true"
forbidEcho="true"
strictBinaryOperands="true"
phpVersion="8.0"
allowStringToStandInForClass="true"
Expand All @@ -21,4 +20,8 @@
<directory name="tests" />
</ignoreFiles>
</projectFiles>
<ignoreExceptions>
<class name="InvalidArgumentException" />
<class name="Psl\Exception\InvariantViolationException" />
</ignoreExceptions>
</psalm>
112 changes: 112 additions & 0 deletions src/Loader/Context/FlatteningContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);

namespace Soap\Wsdl\Loader\Context;

use DOMElement;
use Soap\Wsdl\Exception\UnloadableWsdlException;
use Soap\Wsdl\Loader\WsdlLoader;
use Soap\Wsdl\Xml\Configurator\FlattenTypes;
use Soap\Wsdl\Xml\Flattener;
use Soap\Xml\Xpath\WsdlPreset;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Exception\RuntimeException;
use function VeeWee\Xml\Dom\Mapper\xml_string;

final class FlatteningContext
{
/**
* XSD import catalog of location => raw (not flattened) xml
*
* @var array<string, string>
*/
private $catalog = [];

public static function forWsdl(
string $location,
Document $wsdl,
WsdlLoader $loader,
): self {
$new = new self($wsdl, $loader);
$new->catalog[$location] = $wsdl->map(xml_string());

return $new;
}

private function __construct(
private Document $wsdl,
private WsdlLoader $loader
) {
}

/**
* This function can be used to detect if the context knows about a part of the WSDL.
* It knows about a part from the moment that the raw XML version has been loaded once,
* even if the flattening process is still in an underlying import / include.
*/
public function knowsAboutPart(string $location): bool
{
return array_key_exists($location, $this->catalog);
}

/**
* Imports and include only need to occur once.
* This function determines if an import should be done.
*
* It either returns null if the import already was done or the flattened XML if it still requires an import.
*/
public function import(string $location): ?string
{
return $this->knowsAboutPart($location)
? null
: $this->loadFlattenedXml($location);
}

/**
* Returns the base WSDL document that can be worked on by flattener configurators.
*/
public function wsdl(): Document
{
return $this->wsdl;
}

/**
* This method searches for a single <wsdl:types /> tag
* If no tag exists, it will create an empty one.
* If multiple tags exist, it will merge those tags into one.
*
* @throws RuntimeException
*/
public function types(): DOMElement
{
$doc = Document::fromUnsafeDocument($this->wsdl->toUnsafeDocument(), new FlattenTypes());
$xpath = $doc->xpath(new WsdlPreset($doc));

/** @var DOMElement $types */
$types = $xpath->querySingle('//wsdl:types');

return $types;
}

/**
* This function will take care of the import catalog!
* It will first load the raw xml from the remote source and store that internally.
*
* Next it will apply the XML flattening on the loaded xml and return the flattened string.
* We keep track of all nested flattening locations that are in progress.
* This way we can prevent circular includes as well.
*
* @throws UnloadableWsdlException
* @throws RuntimeException
*/
private function loadFlattenedXml(string $location): string
{
if (!array_key_exists($location, $this->catalog)) {
$this->catalog[$location] = ($this->loader)($location);
}

$document = Document::fromXmlString($this->catalog[$location]);

return (new Flattener())($location, $document, $this);
}
}
30 changes: 30 additions & 0 deletions src/Loader/FlatteningLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);

namespace Soap\Wsdl\Loader;

use Soap\Wsdl\Exception\UnloadableWsdlException;
use Soap\Wsdl\Loader\Context\FlatteningContext;
use Soap\Wsdl\Xml\Flattener;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Exception\RuntimeException;

final class FlatteningLoader implements WsdlLoader
{
public function __construct(
private WsdlLoader $loader,
) {
}

/**
* @throws RuntimeException
* @throws UnloadableWsdlException
*/
public function __invoke(string $location): string
{
$currentDoc = Document::fromXmlString(($this->loader)($location));
$context = FlatteningContext::forWsdl($location, $currentDoc, $this->loader);

return (new Flattener())($location, $currentDoc, $context);
}
}
24 changes: 24 additions & 0 deletions src/Uri/IncludePathBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Soap\Wsdl\Uri;

use League\Uri\Uri;
use League\Uri\UriModifier;
use League\Uri\UriResolver;

final class IncludePathBuilder
{
public static function build(string $relativePath, string $fromFile): string
{
return UriModifier::removeEmptySegments(
UriModifier::removeDotSegments(
UriResolver::resolve(
Uri::createFromString($relativePath),
Uri::createFromString($fromFile)
)
)
)->__toString();
}
}
61 changes: 61 additions & 0 deletions src/Xml/Configurator/FlattenTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);

namespace Soap\Wsdl\Xml\Configurator;

use DOMDocument;
use DOMElement;
use Soap\Xml\Xmlns;
use Soap\Xml\Xpath\WsdlPreset;
use VeeWee\Xml\Dom\Configurator\Configurator;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Exception\RuntimeException;
use function VeeWee\Xml\Dom\Builder\namespaced_element;
use function VeeWee\Xml\Dom\Locator\Node\children;
use function VeeWee\Xml\Dom\Manipulator\append;
use function VeeWee\Xml\Dom\Manipulator\Node\remove;

/**
* This class transforms multiple wsdl:types elements into 1 single element.
* This makes importing xsd's easier (and prevents some bugs in some soap related tools)
*/
final class FlattenTypes implements Configurator
{
/**
* @throws RuntimeException
*/
public function __invoke(DOMDocument $document): DOMDocument
{
$xml = Document::fromUnsafeDocument($document);
$xpath = $xml->xpath(new WsdlPreset($xml));
/** @var list<DOMElement> $types */
$types = [...$xpath->query('wsdl:types')];

// Creates wsdl:types if no matching element exists yet
if (!count($types)) {
$document->documentElement->append(
namespaced_element(Xmlns::wsdl()->value(), 'types')($document)
);

return $document;
}

// Skip if only one exists
$first = array_shift($types);
if (!count($types)) {
return $document;
}

// Flattens multiple wsdl:types elements.
foreach ($types as $additionalTypes) {
$children = children($additionalTypes);
if (count($children)) {
append(...$children)($first);
}

remove($additionalTypes);
}

return $document;
}
}
76 changes: 76 additions & 0 deletions src/Xml/Configurator/FlattenWsdlImports.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Soap\Wsdl\Xml\Configurator;

use DOMDocument;
use DOMElement;
use Soap\Wsdl\Exception\UnloadableWsdlException;
use Soap\Wsdl\Loader\Context\FlatteningContext;
use Soap\Wsdl\Uri\IncludePathBuilder;
use Soap\Xml\Xpath\WsdlPreset;
use VeeWee\Xml\Dom\Configurator\Configurator;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Exception\RuntimeException;
use function VeeWee\Xml\Dom\Locator\document_element;
use function VeeWee\Xml\Dom\Locator\Node\children;
use function VeeWee\Xml\Dom\Manipulator\Node\remove;
use function VeeWee\Xml\Dom\Manipulator\Node\replace_by_external_nodes;

final class FlattenWsdlImports implements Configurator
{
public function __construct(
private string $currentLocation,
private FlatteningContext $context
) {
}

/**
* This method flattens wsdl:import locations.
* It loads the WSDL and adds the definitions replaces the import tag with the definition children from the external file.
*
* For now, we don't care about the namespace property on the wsdl:import tag.
* Future reference:
* @link http://itdoc.hitachi.co.jp/manuals/3020/30203Y2310e/EY230669.HTM#ID01496
*
* @throws RuntimeException
* @throws UnloadableWsdlException
*/
public function __invoke(DOMDocument $document): DOMDocument
{
$xml = Document::fromUnsafeDocument($document);
$xpath = $xml->xpath(new WsdlPreset($xml));

$imports = $xpath->query('wsdl:import');
$imports->forEach(fn (DOMElement $import) => $this->importWsdlImportElement($import));

return $document;
}

/**
* @throws RuntimeException
* @throws UnloadableWsdlException
*/
private function importWsdlImportElement(DOMElement $import): void
{
$location = IncludePathBuilder::build(
$import->getAttribute('location'),
$this->currentLocation
);

$result = $this->context->import($location);
if (!$result) {
remove($import);
return;
}

$imported = Document::fromXmlString($result);
$definitions = $imported->map(document_element());

replace_by_external_nodes(
$import,
children($definitions)
);
}
}
Loading

0 comments on commit d12cf90

Please sign in to comment.