@@ -318,6 +318,28 @@ public function visitProperty(PropertyMetadata $metadata, $v): void
318318 return ;
319319 }
320320
321+ if (false !== strpos ($ metadata ->serializedName , '/@ ' ) && $ this ->trySerializePropertyAsAttributeOnSiblingElement ($ metadata , $ v )) {
322+ return ;
323+ }
324+
325+ if (0 === strpos ($ metadata ->serializedName , '@ ' )) {
326+ [$ attributeValue , $ processedNode ] = $ this ->processValueForXmlAttribute ($ v , $ metadata ->type , $ metadata );
327+
328+ if (null === $ v && null === $ processedNode ) {
329+ return ;
330+ }
331+
332+ $ attributeName = substr ($ metadata ->serializedName , 1 );
333+
334+ if ($ this ->currentNode instanceof \DOMElement) {
335+ $ this ->setAttributeOnNode ($ this ->currentNode , $ attributeName , $ attributeValue , $ metadata ->xmlNamespace );
336+ } else {
337+ throw new RuntimeException ('Cannot set attribute on a non-element node. ' );
338+ }
339+
340+ return ;
341+ }
342+
321343 if ($ addEnclosingElement = !$ this ->isInLineCollection ($ metadata ) && !$ metadata ->inline ) {
322344 $ namespace = $ metadata ->xmlNamespace ?? $ this ->getClassDefaultNamespace ($ this ->objectMetadataStack ->top ());
323345
@@ -354,6 +376,75 @@ public function visitProperty(PropertyMetadata $metadata, $v): void
354376 $ this ->hasValue = false ;
355377 }
356378
379+ private function trySerializePropertyAsAttributeOnSiblingElement (PropertyMetadata $ metadata , $ v ): bool
380+ {
381+ [$ elementName , $ attributeName ] = explode ('/@ ' , $ metadata ->serializedName , 2 );
382+ $ namespace = $ metadata ->xmlNamespace ?? $ this ->getClassDefaultNamespace ($ this ->objectMetadataStack ->top ());
383+ $ targetElement = null ;
384+
385+ if ($ this ->currentNode instanceof \DOMElement) {
386+ foreach ($ this ->currentNode ->childNodes as $ childNode ) {
387+ if ($ childNode instanceof \DOMElement && $ childNode ->localName === $ elementName ) {
388+ $ isNamespaceMatch = false ;
389+ // Case 1: Expected a specific namespace, and child node has it.
390+ // Case 2 (else): Expected no namespace
391+ if (null !== $ namespace && $ childNode ->namespaceURI === $ namespace ) {
392+ $ isNamespaceMatch = true ;
393+ } elseif ((null === $ namespace || '' === $ namespace ) && (null === $ childNode ->namespaceURI || '' === $ childNode ->namespaceURI )) {
394+ $ isNamespaceMatch = true ;
395+ }
396+
397+ if ($ isNamespaceMatch ) {
398+ $ targetElement = $ childNode ;
399+ break ;
400+ }
401+ }
402+ }
403+ }
404+
405+ if (!$ targetElement ) {
406+ return false ;
407+ }
408+
409+ if (null === $ v ) {
410+ return true ;
411+ }
412+
413+ [$ attributeStringValue , $ attributeValueNode ] = $ this ->processValueForXmlAttribute ($ v , $ metadata ->type , $ metadata );
414+
415+ if (null !== $ attributeValueNode || is_scalar ($ v )) {
416+ $ this ->setAttributeOnNode ($ targetElement , $ attributeName , $ attributeStringValue , $ metadata ->xmlNamespace );
417+ }
418+
419+ return true ;
420+ }
421+
422+ /**
423+ * @return array{0:string, 1:\DOMNode|null} string value for attribute, and the processed DOMNode/null.
424+ *
425+ * @throws RuntimeException If the value is unsuitable for an XML attribute.
426+ */
427+ private function processValueForXmlAttribute ($ inputValue , ?array $ valueType , PropertyMetadata $ metadataForNavigatorContext ): array
428+ {
429+ $ this ->setCurrentMetadata ($ metadataForNavigatorContext );
430+ $ processedNode = $ this ->navigator ->accept ($ inputValue , $ valueType );
431+ $ this ->revertCurrentMetadata ();
432+
433+ if ($ processedNode instanceof \DOMCharacterData) {
434+ $ stringValue = $ processedNode ->nodeValue ;
435+ } elseif (null === $ processedNode ) {
436+ $ stringValue = is_scalar ($ inputValue ) ? (string ) $ inputValue : '' ;
437+ } else {
438+ throw new RuntimeException (sprintf (
439+ 'Unsupported value for XML attribute for property "%s". Expected character data or scalar, but got %s. ' ,
440+ $ metadataForNavigatorContext ->name ,
441+ \is_object ($ processedNode ) ? \get_class ($ processedNode ) : \gettype ($ processedNode ),
442+ ));
443+ }
444+
445+ return [$ stringValue , $ processedNode ];
446+ }
447+
357448 private function isInLineCollection (PropertyMetadata $ metadata ): bool
358449 {
359450 return $ metadata ->xmlCollection && $ metadata ->xmlCollectionInline ;
0 commit comments