Files
2025-08-02 16:30:27 +02:00

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;
}
}