Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wsdl flattener #1

Merged
merged 12 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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