226 lines
6.3 KiB
PHP
226 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace KupShop\FeedGeneratorBundle\XSD;
|
|
|
|
use KupShop\FeedGeneratorBundle\Exception\XSDParser\XSDParsingException;
|
|
|
|
class Parser
|
|
{
|
|
public const TYPE_INT = 'int';
|
|
public const TYPE_FLOAT = 'float';
|
|
public const TYPE_ARRAY = 'array';
|
|
public const TYPE_BOOL = 'bool';
|
|
|
|
private const XSD_FLOAT = 'decimal';
|
|
private const XSD_BOOL = 'boolean';
|
|
|
|
private const XSD_INT_TYPES = [
|
|
'byte',
|
|
'int',
|
|
'integer',
|
|
'long',
|
|
'negativeInteger',
|
|
'nonNegativeInteger',
|
|
'nonPositiveInteger',
|
|
'positiveInteger',
|
|
'short',
|
|
'unsignedLong',
|
|
'unsignedInt',
|
|
'unsignedShort',
|
|
'unsignedByte',
|
|
];
|
|
|
|
public static string $xsdNamespacePrefix = 'xs';
|
|
|
|
private \DOMDocument $doc;
|
|
private \DOMXPath $xpath;
|
|
|
|
/** @var \ArrayObject<TypeNode> Contains instances of named types in schema */
|
|
private \ArrayObject $typeContext;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->doc = new \DOMDocument();
|
|
$this->typeContext = new \ArrayObject();
|
|
}
|
|
|
|
/**
|
|
* Initializes XSDParser with schema loaded into a string.
|
|
*
|
|
* @throws XSDParsingException
|
|
*/
|
|
public function loadString(string $xsdString): void
|
|
{
|
|
$loaded = $this->doc->loadXML($xsdString);
|
|
if ($loaded === false) {
|
|
throw new XSDParsingException('Incorrect XML Schema!');
|
|
}
|
|
|
|
$this->xpath = new \DOMXPath($this->doc);
|
|
$this->xpath->registerNamespace(self::$xsdNamespacePrefix, 'http://www.w3.org/2001/XMLSchema');
|
|
$this->loadNamedTypes();
|
|
}
|
|
|
|
/**
|
|
* Initializes XSDParser with file path to schema.
|
|
*
|
|
* @throws XSDParsingException
|
|
*/
|
|
public function loadFile(string $filePath): void
|
|
{
|
|
$loaded = $this->doc->load($filePath);
|
|
if ($loaded === false) {
|
|
throw new XSDParsingException('Failed to load XML Schema (wrong source URL or incorrect schema)!');
|
|
}
|
|
|
|
$this->xpath = new \DOMXPath($this->doc);
|
|
$this->xpath->registerNamespace(self::$xsdNamespacePrefix, 'http://www.w3.org/2001/XMLSchema');
|
|
$this->loadNamedTypes();
|
|
}
|
|
|
|
/**
|
|
* Loads all named types from schema into an array.
|
|
*/
|
|
private function loadNamedTypes(): void
|
|
{
|
|
$ns = self::$xsdNamespacePrefix;
|
|
|
|
$types = ParserUtil::filterElements(
|
|
$this->xpath->query("//{$ns}:complexType[@name]"),
|
|
);
|
|
|
|
$types = array_merge($types,
|
|
ParserUtil::filterElements(
|
|
$this->xpath->query("//{$ns}:simpleType[@name]"),
|
|
),
|
|
);
|
|
|
|
foreach ($types as $type) {
|
|
$this->typeContext[$type->getAttribute('name')] = new TypeNode($this->xpath, $type, $this->typeContext);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the root node of the entire schema.
|
|
*
|
|
* @throws XSDParsingException
|
|
*/
|
|
public function getSchemaRootElement(): ElementNode
|
|
{
|
|
$ns = self::$xsdNamespacePrefix;
|
|
|
|
$feed = ParserUtil::filterElements(
|
|
$this->xpath->query("{$ns}:element")
|
|
);
|
|
if (count($feed) !== 1) {
|
|
throw new XSDParsingException('Unsupported or incorrect XML schema structure: singleton root element not found.');
|
|
}
|
|
|
|
return new ElementNode($this->xpath, $feed[0], $this->typeContext);
|
|
}
|
|
|
|
/**
|
|
* Returns the root node of a specific feed.
|
|
*
|
|
* @throws XSDParsingException
|
|
*/
|
|
public function getFeedRootElement(): ElementNode
|
|
{
|
|
$element = $this->getSchemaRootElement();
|
|
|
|
while (true) {
|
|
$type = $element->getSelfType() ?? $element->getChildType();
|
|
if (!($type instanceof TypeNode)) {
|
|
break;
|
|
}
|
|
|
|
$descendants = $type->getDescendantElements();
|
|
if (count($descendants) !== 1) {
|
|
break;
|
|
}
|
|
$element = $descendants[0];
|
|
}
|
|
|
|
return $element;
|
|
}
|
|
|
|
/**
|
|
* Returns path to the 1. element of a specific feed.
|
|
*
|
|
* @throws XSDParsingException
|
|
*/
|
|
public function getFeedRootElementPath(): array
|
|
{
|
|
$element = $this->getSchemaRootElement();
|
|
$path = [];
|
|
|
|
while (true) {
|
|
$path[] = $element->getAttributes()['name'];
|
|
$type = $element->getSelfType() ?? $element->getChildType();
|
|
if (!($type instanceof TypeNode)) {
|
|
break;
|
|
}
|
|
|
|
$descendants = $type->getDescendantElements();
|
|
if (count($descendants) !== 1) {
|
|
break;
|
|
}
|
|
$element = $descendants[0];
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* @throws XSDParsingException
|
|
*/
|
|
public function getTypeConstraints(): array
|
|
{
|
|
$keepKeys = ['maxOccurs'];
|
|
|
|
$types = ParserUtil::arrayFlatten(
|
|
ParserUtil::keyValueChildren(
|
|
$this->getFeedRootElement()->serialize()['children'],
|
|
$keepKeys,
|
|
),
|
|
);
|
|
|
|
$types = array_filter($types, function ($value, $key) use ($keepKeys) {
|
|
foreach (['type', ...$keepKeys] as $keyToKeep) {
|
|
if ((bool) preg_match("/^[\w.]+\.{$keyToKeep}$/", $key)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}, ARRAY_FILTER_USE_BOTH);
|
|
|
|
$constraints = [];
|
|
|
|
foreach ($types as $key => $val) {
|
|
// handle ints, floats and bools
|
|
if ((bool) preg_match('/^[\w.]+\.type$/', $key)) {
|
|
$key = preg_replace('/^([\w.]+)\.type$/', '$1', $key);
|
|
$type = str_replace(self::$xsdNamespacePrefix.':', '', $val);
|
|
|
|
if (in_array($type, self::XSD_INT_TYPES)) {
|
|
$constraints[$key] = self::TYPE_INT;
|
|
} elseif ($type === self::XSD_FLOAT) {
|
|
$constraints[$key] = self::TYPE_FLOAT;
|
|
} elseif ($type === self::XSD_BOOL) {
|
|
$constraints[$key] = self::TYPE_BOOL;
|
|
}
|
|
}
|
|
// handle arrays
|
|
elseif ((bool) preg_match('/^[\w.]+\.maxOccurs$/', $key)) {
|
|
$key = preg_replace('/^([\w.]+)\.maxOccurs$/', '$1', $key);
|
|
if ($val === 'unbounded') {
|
|
$constraints[$key] = self::TYPE_ARRAY;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $constraints;
|
|
}
|
|
}
|