XML($xmlContent)) { throw new UnreadableXMLException('Failed to read XML from string.'); } else { $this->reader = $reader; } } /** * @throws UnreadableXMLException */ public function openFile(string $url): void { $reader = @\XMLReader::open($url); if (!$reader || @filesize($url) === 0) { throw new UnreadableXMLException('Failed to read XML.'); } else { $this->reader = $reader; } } private function goToStart(): bool { if (empty($this->basePath)) { return false; } foreach ($this->basePath as $pathPart) { while ($this->reader->name !== $pathPart) { $res = $this->reader->read(); if (!$res) { return false; } } } return true; } private function goToNext(): bool { return $this->reader->next(end($this->basePath)); } /** * Supplies a \Generator which yields XML elements converted to associative arrays. * * @throws XMLFeedReaderException */ public function read(): \Generator { $hasNext = $this->goToStart(); if (!$hasNext) { throw new XMLFeedReaderException('Could not find feed root element.'); } while ($hasNext) { $search = $this->reader->getAttribute(self::SEARCH_ATTRIBUTE_NAME); $doc = $this->xmlToAssoc(new \SimpleXMLElement($this->reader->readOuterXml())); $doc['__search'] = $search ?? ''; $hasNext = $this->goToNext(); yield $doc; } } /** * @throws XMLFeedReaderException */ public function findElementBySearchAttribute(string $value, string $searchAttributeName = self::SEARCH_ATTRIBUTE_NAME): array { $hasNext = $this->goToStart(); if (!$hasNext) { throw new XMLFeedReaderException('Could not find feed root element.'); } while ($hasNext) { if ($this->reader->getAttribute($searchAttributeName) === $value) { return $this->xmlToAssoc(new \SimpleXMLElement($this->reader->readOuterXml())); } $hasNext = $this->goToNext(); } return []; } private function xmlToAssoc(\SimpleXMLElement $xml, string $currentBaseFieldName = ''): array { $res = []; foreach ($xml->children() as $child) { $childName = $child->getName(); $fieldName = empty($currentBaseFieldName) ? $childName : "{$currentBaseFieldName}.{$childName}"; // if the current node is a value without children if ($child->count() === 0) { $corrected = $this->correctValueType($child, $fieldName); $index = self::getElementAttribute($child, self::INDEX_ATTRIBUTE_NAME); $isAlreadyDefined = isset($res[$childName]); if ($isAlreadyDefined || $index !== null) { $current = $isAlreadyDefined ? (array) $res[$childName] : []; $index ??= $isAlreadyDefined ? count((array) $res[$childName]) : 0; $res[$childName] = $current + [$index => $corrected]; continue; } $res[$childName] = $corrected; continue; } // array in XML (to not override existing entry, but merge into one array) if (isset($res[$childName])) { $res[$childName] = array_merge( (array) $res[$childName], [$this->xmlToAssoc($child, $fieldName)], ); continue; } // only one occurrence in XML, but has maxOccurs=unbounded in XSD if ($this->typeCorrectionMap[$fieldName] ?? '' === Parser::TYPE_ARRAY) { $res[$childName] = [$this->xmlToAssoc($child, $fieldName)]; continue; } $res[$childName] = $this->xmlToAssoc($child, $fieldName); } return $res; } private function correctValueType(\SimpleXMLElement $element, string $fieldName) { switch ($this->typeCorrectionMap[$fieldName] ?? '') { case Parser::TYPE_INT: return (int) $element; case Parser::TYPE_FLOAT: return (float) $element; case Parser::TYPE_BOOL: // bool cast checks if $element is nullish -> has to be checked manually // https://www.w3.org/TR/xmlschema-2/#boolean $val = (string) $element; return $val === 'true' || $val === '1'; default: return (string) $element; } } private static function getElementAttribute(\SimpleXMLElement $self, string $name, $default = null) { $attributes = $self->attributes(); if ($attributes === null) { return $default; } foreach ($attributes as $attr) { if ($attr->getName() === $name) { return (string) $attr; } } return $default; } public function setBasePath(array $basePath): void { $this->basePath = $basePath; } public function setTypeCorrectionMap(array $typeCorrectionMap): void { $this->typeCorrectionMap = $typeCorrectionMap; } }