open($path, 'UTF-8', LIBXML_NONET | LIBXML_COMPACT)) { throw new ExchangeException('Не удалось открыть XML: ' . esc_html(basename($path))); } return $reader; } /** * Текущий элемент целиком (вместе с вложенными) как SimpleXML. */ public static function expand(XMLReader $reader): SimpleXMLElement { $doc = new \DOMDocument(); $node = $reader->expand($doc); if ($node === false) { throw new ExchangeException('Ошибка чтения XML'); } $doc->appendChild($node); return simplexml_import_dom($doc->documentElement); } /** * Первая серьёзная ошибка разбора, накопленная libxml, или null. */ public static function lastError(): ?string { foreach (libxml_get_errors() as $error) { if ($error->level >= LIBXML_ERR_ERROR) { return sprintf('строка %d: %s', $error->line, trim($error->message)); } } return null; } /** * Безопасный обход вложенных элементов: nodes($el, 'Группы', 'Ид'). * * @return list */ public static function nodes(SimpleXMLElement $element, string ...$path): array { $current = [$element]; foreach ($path as $name) { $next = []; foreach ($current as $node) { foreach ($node->{$name} as $child) { $next[] = $child; } } $current = $next; } return $current; } public static function text(SimpleXMLElement $element, string $child): string { return isset($element->{$child}) ? trim((string) $element->{$child}) : ''; } /** * Число из 1С: «1 200,50» → 1200.5. */ public static function number(string $value): ?float { $value = str_replace([' ', "\u{00A0}", "\u{202F}", ','], ['', '', '', '.'], trim($value)); return is_numeric($value) ? (float) $value : null; } public static function isGuid(string $value): bool { return (bool) preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value); } /** * Пометка удаления у товара или предложения (разные версии схемы пишут её по-разному). */ public static function isDeleted(SimpleXMLElement $item): bool { return (string) $item['Статус'] === 'Удален' || self::text($item, 'Статус') === 'Удален' || self::text($item, 'ПометкаУдаления') === 'true'; } }