From 5ea8ffe06dc7ed651da3a121895f51ac2d0c3cea Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 2 Jan 2025 11:29:52 +0100 Subject: [PATCH 1/3] Prefix attribute type-names with parent type name. --- .../Converter/SchemaToTypesConverter.php | 16 ++++- .../AttributeDeclaringParentTypeDetector.php | 67 +++++++++++++++++++ .../Converter/Types/ParentContext.php | 38 +++++++++++ .../Converter/Types/TypesConverterContext.php | 24 +++++++ .../Visitor/AttributeContainerVisitor.php | 25 ++++++- .../Visitor/InlineElementTypeVisitor.php | 4 +- tests/PhpCompatibility/schema1013.phpt | 32 +++++++++ 7 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php create mode 100644 src/Metadata/Converter/Types/ParentContext.php create mode 100644 tests/PhpCompatibility/schema1013.phpt diff --git a/src/Metadata/Converter/SchemaToTypesConverter.php b/src/Metadata/Converter/SchemaToTypesConverter.php index 48db279..a002d7c 100644 --- a/src/Metadata/Converter/SchemaToTypesConverter.php +++ b/src/Metadata/Converter/SchemaToTypesConverter.php @@ -7,6 +7,7 @@ use GoetasWebservices\XML\XSDReader\Schema\Schema; use GoetasWebservices\XML\XSDReader\Schema\Type\Type; use Soap\Engine\Metadata\Collection\TypeCollection; +use Soap\WsdlReader\Metadata\Converter\Types\ParentContext; use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext; use Soap\WsdlReader\Metadata\Converter\Types\Visitor\ElementVisitor; use Soap\WsdlReader\Metadata\Converter\Types\Visitor\TypeVisitor; @@ -22,15 +23,24 @@ public function __invoke(Schema $schema, TypesConverterContext $context): TypeCo ...filter_nulls([ ...flat_map( $schema->getTypes(), - static fn (Type $type): TypeCollection => (new TypeVisitor())($type, $context) + static fn (Type $type): TypeCollection => (new TypeVisitor())( + $type, + $context->onParent(ParentContext::create($type)) + ) ), ...flat_map( $schema->getElements(), - static fn (ElementDef $element): TypeCollection => (new ElementVisitor())($element, $context) + static fn (ElementDef $element): TypeCollection => (new ElementVisitor())( + $element, + $context->onParent(ParentContext::create($element)) + ) ), ...flat_map( $schema->getSchemas(), - fn (Schema $childSchema): TypeCollection => $this->__invoke($childSchema, $context) + fn (Schema $childSchema): TypeCollection => $this->__invoke( + $childSchema, + $context->onParent(null) + ) ) ]) ); diff --git a/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php b/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php new file mode 100644 index 0000000..3aab317 --- /dev/null +++ b/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php @@ -0,0 +1,67 @@ + + */ + public function __invoke(AttributeItem $item, ?SchemaItem $parent): Option + { + $parent = match(true) { + $parent instanceof Item => $parent->getType(), + default => $parent, + }; + + if (!$parent instanceof Type) { + return none(); + } + + if ($parent instanceof AttributeContainer) { + foreach ($parent->getAttributes() as $parentAttribute) { + if ($parentAttribute->getName() === $item->getName()) { + /** @var Option */ + return some($parent); + } + } + } + + $extensionBase = $parent->getExtension()?->getBase(); + if ($extensionBase) { + return $this->__invoke($item, $extensionBase); + } + + return none(); + } + + + /** + * @param Option $parentContext + * @return Option + */ + public static function detectWithParentContext(AttributeItem $item, Option $parentContext): Option + { + /** @var self $calculate */ + static $calculate = new self(); + + return $parentContext + ->andThen(static fn (ParentContext $context) => $calculate($item, $context->currentParent())); + } +} diff --git a/src/Metadata/Converter/Types/ParentContext.php b/src/Metadata/Converter/Types/ParentContext.php new file mode 100644 index 0000000..724be71 --- /dev/null +++ b/src/Metadata/Converter/Types/ParentContext.php @@ -0,0 +1,38 @@ + $items + */ + private function __construct( + public readonly array $items, + ) { + } + + public static function create(SchemaItem $item): self + { + return new self([$item]); + } + + public function withNextParent(SchemaItem $item): self + { + return new self([...$this->items, $item]); + } + + public function rootParent(): SchemaItem + { + return first($this->items); + } + + public function currentParent(): SchemaItem + { + return last($this->items); + } +} diff --git a/src/Metadata/Converter/Types/TypesConverterContext.php b/src/Metadata/Converter/Types/TypesConverterContext.php index 857e45a..56590f8 100644 --- a/src/Metadata/Converter/Types/TypesConverterContext.php +++ b/src/Metadata/Converter/Types/TypesConverterContext.php @@ -4,9 +4,12 @@ namespace Soap\WsdlReader\Metadata\Converter\Types; use GoetasWebservices\XML\XSDReader\Schema\Schema; +use Psl\Option\Option; use Soap\Engine\Metadata\Collection\TypeCollection; use Soap\WsdlReader\Model\Definitions\Namespaces; use Soap\WsdlReader\Parser\Definitions\SchemaParser; +use function Psl\Option\from_nullable; +use function Psl\Option\none; final class TypesConverterContext { @@ -20,9 +23,15 @@ final class TypesConverterContext */ private array $visited = []; + /** + * @var Option + */ + private Option $parentContext; + private function __construct( public readonly Namespaces $knownNamespaces ) { + $this->parentContext = none(); } public static function default(Namespaces $knownNamespaces): self @@ -57,4 +66,19 @@ public function visit(Schema $schema, callable $visitor): TypeCollection return $visitor($schema); } + + public function onParent(?ParentContext $parentContext): self + { + $this->parentContext = from_nullable($parentContext); + + return $this; + } + + /** + * @return Option + */ + public function parent(): Option + { + return $this->parentContext; + } } diff --git a/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php b/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php index c6a2a9d..2ef860d 100644 --- a/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php +++ b/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php @@ -8,13 +8,16 @@ use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeSingle; use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group; use GoetasWebservices\XML\XSDReader\Schema\Type\Type; +use Psl\Option\Option; use Soap\Engine\Metadata\Collection\PropertyCollection; use Soap\Engine\Metadata\Model\Property; use Soap\Engine\Metadata\Model\TypeMeta; use Soap\Engine\Metadata\Model\XsdType as EngineType; use Soap\WsdlReader\Metadata\Converter\Types\Configurator; +use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeDeclaringParentTypeDetector; use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext; use function Psl\Fun\pipe; +use function Psl\Option\from_nullable; use function Psl\Result\wrap; use function Psl\Type\instance_of; use function Psl\Vec\flat_map; @@ -88,8 +91,26 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext return $this->parseAttributes($attribute, $context); } + // Detecting the type-name for an attribute is complex. + // We first try to use the type name, + // Next up is the base type of the restriction if there aren't any restriction checks configured. + // Finally there is a fallback to the attribute name $attributeType = $attribute instanceof AttributeSingle ? $attribute->getType() : null; - $typeName = $attributeType?->getName() ?: $attribute->getName(); + $attributeRestriction = $attributeType?->getRestriction(); + $attributeTypeName = $attributeType?->getName(); + $attributeRestrictionName = ($attributeRestriction && !$attributeRestriction->getChecks()) ? $attributeRestriction->getBase()?->getName() : null; + + $typeName = $attributeTypeName ?: ($attributeRestrictionName ?: $attribute->getName()); + $engineType = EngineType::guess($typeName); + + // If a name cannot be determined from the type, we fallback to the attribute name: + // Prefix the attribute name with the parent element name resulting in a more unique type-name. + if (!$attributeTypeName && !$attributeRestrictionName) { + $engineType = AttributeDeclaringParentTypeDetector::detectWithParentContext($attribute, $context->parent()) + ->andThen(static fn (Type $parent): Option => from_nullable($parent->getName())) + ->map(static fn (string $parentName): EngineType => $engineType->copy($parentName . ucfirst($typeName))) + ->unwrapOr($engineType); + } $configure = pipe( static fn (EngineType $engineType): EngineType => (new Configurator\AttributeConfigurator())($engineType, $attribute, $context), @@ -98,7 +119,7 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext return new PropertyCollection( new Property( $attribute->getName(), - $configure(EngineType::guess($typeName)) + $configure($engineType) ) ); } diff --git a/src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php b/src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php index 68278f3..05d1d8a 100644 --- a/src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php +++ b/src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php @@ -62,6 +62,8 @@ private function detectInlineTypes(ElementItem $element, TypesConverterContext $ return new TypeCollection(); } - return $elementVisitor($element, $context); + return $elementVisitor($element, $context->onParent( + $context->parent()->unwrap()->withNextParent($element) + )); } } diff --git a/tests/PhpCompatibility/schema1013.phpt b/tests/PhpCompatibility/schema1013.phpt new file mode 100644 index 0000000..cfd386b --- /dev/null +++ b/tests/PhpCompatibility/schema1013.phpt @@ -0,0 +1,32 @@ +--TEST-- +SOAP XML Schema 1001: Prepend element name before attribute type names for more unique type-names. +--FILE-- + + + + + + + + + + + + + + +EOF; +test_schema($schema,'type="tns:VehicleCoreType"'); +?> +--EXPECT-- +Methods: + > test(VehicleCoreType $testParam): void + +Types: + > http://test-uri/:VehicleCoreType { + ?string $VehType + @?VehicleCoreTypeDriveType in (AWD|4WD|Unspecified) $DriveType + } From 77a255088ccc1274ab6c7ca0c9ce8f65c97d37f6 Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 2 Jan 2025 15:28:07 +0100 Subject: [PATCH 2/3] Also prefix inline element simple-type names --- .../Detector/ElementTypeNameDetector.php | 31 +++++++++++++++++++ .../Types/Visitor/ElementContainerVisitor.php | 5 ++- tests/PhpCompatibility/schema1013.phpt | 2 +- tests/PhpCompatibility/schema1014.phpt | 31 +++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/Metadata/Converter/Types/Detector/ElementTypeNameDetector.php create mode 100644 tests/PhpCompatibility/schema1014.phpt diff --git a/src/Metadata/Converter/Types/Detector/ElementTypeNameDetector.php b/src/Metadata/Converter/Types/Detector/ElementTypeNameDetector.php new file mode 100644 index 0000000..fb8db0a --- /dev/null +++ b/src/Metadata/Converter/Types/Detector/ElementTypeNameDetector.php @@ -0,0 +1,31 @@ +getType() : null; + $typeName = $calculatedTypeName ?? ($type?->getName() ?: $element->getName()); + + // For inline simple types, we prefix the name of the element with the name of the parent type. + if ($type instanceof SimpleType && !$type->getName()) { + $parent = $parentContext->currentParent(); + + if ($parent instanceof Type || $parent instanceof ElementItem) { + if ($parentName = $parent->getName()) { + $typeName = $parentName . ucfirst($typeName); + } + } + } + + return $typeName; + } +} diff --git a/src/Metadata/Converter/Types/Visitor/ElementContainerVisitor.php b/src/Metadata/Converter/Types/Visitor/ElementContainerVisitor.php index 5820f4b..8b99ef4 100644 --- a/src/Metadata/Converter/Types/Visitor/ElementContainerVisitor.php +++ b/src/Metadata/Converter/Types/Visitor/ElementContainerVisitor.php @@ -6,13 +6,13 @@ use GoetasWebservices\XML\XSDReader\Schema\Element\Choice; use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer; use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem; -use GoetasWebservices\XML\XSDReader\Schema\Element\ElementSingle; use GoetasWebservices\XML\XSDReader\Schema\Element\Group; use GoetasWebservices\XML\XSDReader\Schema\Element\Sequence; use Soap\Engine\Metadata\Collection\PropertyCollection; use Soap\Engine\Metadata\Model\Property; use Soap\Engine\Metadata\Model\XsdType as EngineType; use Soap\WsdlReader\Metadata\Converter\Types\Configurator; +use Soap\WsdlReader\Metadata\Converter\Types\Detector\ElementTypeNameDetector; use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext; use function Psl\Fun\pipe; use function Psl\Vec\flat_map; @@ -35,8 +35,7 @@ private function parseElementItem(ElementItem $element, TypesConverterContext $c return $this->__invoke($element, $context); } - $type = $element instanceof ElementSingle ? $element->getType() : null; - $typeName = $type?->getName() ?: $element->getName(); + $typeName = (new ElementTypeNameDetector())($element, $context->parent()->unwrap()); $configure = pipe( static fn (EngineType $engineType): EngineType => (new Configurator\ElementConfigurator())($engineType, $element, $context), static fn (EngineType $engineType): EngineType => (new Configurator\AnyElementConfigurator())($engineType, $element, $context), diff --git a/tests/PhpCompatibility/schema1013.phpt b/tests/PhpCompatibility/schema1013.phpt index cfd386b..3f179f7 100644 --- a/tests/PhpCompatibility/schema1013.phpt +++ b/tests/PhpCompatibility/schema1013.phpt @@ -1,5 +1,5 @@ --TEST-- -SOAP XML Schema 1001: Prepend element name before attribute type names for more unique type-names. +SOAP XML Schema 1001: Prepend parent type name before attribute type names for more unique type-names. --FILE-- + + + + + + + + + + + + + +EOF; +test_schema($schema,'type="tns:Element"'); +?> +--EXPECT-- +Methods: + > test(Element $testParam): void + +Types: + > http://test-uri/:Element { + ElementEnum in (foo|bar) $Enum + } From b25d6089fd222d62398eea159911086b60fe760f Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Thu, 2 Jan 2025 15:42:08 +0100 Subject: [PATCH 3/3] Use the new naming logic for specifying the xml type name as well so that the encoder doesnt get confused --- .../Configurator/XmlTypeInfoConfigurator.php | 13 ++++++- .../AttributeDeclaringParentTypeDetector.php | 15 -------- .../Detector/AttributeTypeNameDetector.php | 34 +++++++++++++++++++ .../Visitor/AttributeContainerVisitor.php | 29 ++-------------- 4 files changed, 49 insertions(+), 42 deletions(-) create mode 100644 src/Metadata/Converter/Types/Detector/AttributeTypeNameDetector.php diff --git a/src/Metadata/Converter/Types/Configurator/XmlTypeInfoConfigurator.php b/src/Metadata/Converter/Types/Configurator/XmlTypeInfoConfigurator.php index 0ae37b2..5a2e0b5 100644 --- a/src/Metadata/Converter/Types/Configurator/XmlTypeInfoConfigurator.php +++ b/src/Metadata/Converter/Types/Configurator/XmlTypeInfoConfigurator.php @@ -3,10 +3,14 @@ namespace Soap\WsdlReader\Metadata\Converter\Types\Configurator; +use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem; +use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem; use GoetasWebservices\XML\XSDReader\Schema\Item; use GoetasWebservices\XML\XSDReader\Schema\SchemaItem; use GoetasWebservices\XML\XSDReader\Schema\Type\Type; use Soap\Engine\Metadata\Model\XsdType as EngineType; +use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeTypeNameDetector; +use Soap\WsdlReader\Metadata\Converter\Types\Detector\ElementTypeNameDetector; use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext; final class XmlTypeInfoConfigurator @@ -25,9 +29,16 @@ public function __invoke(EngineType $engineType, mixed $xsdType, TypesConverterC $targetNamespace = $xsdType->getSchema()->getTargetNamespace() ?? ''; $typeNamespace = $type?->getSchema()->getTargetNamespace() ?: $targetNamespace; + $parentContext = $context->parent()->unwrapOr(null); + $xmlTypeName = match(true) { + $parentContext && $item instanceof ElementItem => (new ElementTypeNameDetector())($item, $parentContext), + $parentContext && $item instanceof AttributeItem => (new AttributeTypeNameDetector())($item, $parentContext), + default => $typeName, + }; + return $engineType ->withXmlTargetNodeName($itemName ?: $typeName) - ->withXmlTypeName($typeName ?: $itemName ?: '') + ->withXmlTypeName($xmlTypeName) ->withXmlNamespace($typeNamespace) ->withXmlNamespaceName( $context->knownNamespaces->lookupNameFromNamespace($typeNamespace)->unwrapOr( diff --git a/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php b/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php index 3aab317..c523078 100644 --- a/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php +++ b/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php @@ -8,7 +8,6 @@ use GoetasWebservices\XML\XSDReader\Schema\SchemaItem; use GoetasWebservices\XML\XSDReader\Schema\Type\Type; use Psl\Option\Option; -use Soap\WsdlReader\Metadata\Converter\Types\ParentContext; use function Psl\Option\none; use function Psl\Option\some; @@ -50,18 +49,4 @@ public function __invoke(AttributeItem $item, ?SchemaItem $parent): Option return none(); } - - - /** - * @param Option $parentContext - * @return Option - */ - public static function detectWithParentContext(AttributeItem $item, Option $parentContext): Option - { - /** @var self $calculate */ - static $calculate = new self(); - - return $parentContext - ->andThen(static fn (ParentContext $context) => $calculate($item, $context->currentParent())); - } } diff --git a/src/Metadata/Converter/Types/Detector/AttributeTypeNameDetector.php b/src/Metadata/Converter/Types/Detector/AttributeTypeNameDetector.php new file mode 100644 index 0000000..a3de8cf --- /dev/null +++ b/src/Metadata/Converter/Types/Detector/AttributeTypeNameDetector.php @@ -0,0 +1,34 @@ +getType() : null; + $attributeRestriction = $attributeType?->getRestriction(); + $attributeTypeName = $attributeType?->getName(); + $attributeRestrictionName = ($attributeRestriction && !$attributeRestriction->getChecks()) ? $attributeRestriction->getBase()?->getName() : null; + + $typeName = $attributeTypeName ?: ($attributeRestrictionName ?: $attribute->getName()); + + // If a name cannot be determined from the type, we fallback to the attribute name: + // Prefix the attribute name with the parent element name resulting in a more unique type-name. + if (!$attributeTypeName && !$attributeRestrictionName) { + $typeName = (new AttributeDeclaringParentTypeDetector())($attribute, $parentContext->currentParent()) + ->andThen(static fn (Type $parent): Option => from_nullable($parent->getName())) + ->map(static fn (string $parentName): string => $parentName . ucfirst($typeName)) + ->unwrapOr($typeName); + } + + return $typeName; + } +} diff --git a/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php b/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php index 2ef860d..d746da2 100644 --- a/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php +++ b/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php @@ -5,19 +5,16 @@ use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeContainer; use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem; -use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeSingle; use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group; use GoetasWebservices\XML\XSDReader\Schema\Type\Type; -use Psl\Option\Option; use Soap\Engine\Metadata\Collection\PropertyCollection; use Soap\Engine\Metadata\Model\Property; use Soap\Engine\Metadata\Model\TypeMeta; use Soap\Engine\Metadata\Model\XsdType as EngineType; use Soap\WsdlReader\Metadata\Converter\Types\Configurator; -use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeDeclaringParentTypeDetector; +use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeTypeNameDetector; use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext; use function Psl\Fun\pipe; -use function Psl\Option\from_nullable; use function Psl\Result\wrap; use function Psl\Type\instance_of; use function Psl\Vec\flat_map; @@ -91,27 +88,7 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext return $this->parseAttributes($attribute, $context); } - // Detecting the type-name for an attribute is complex. - // We first try to use the type name, - // Next up is the base type of the restriction if there aren't any restriction checks configured. - // Finally there is a fallback to the attribute name - $attributeType = $attribute instanceof AttributeSingle ? $attribute->getType() : null; - $attributeRestriction = $attributeType?->getRestriction(); - $attributeTypeName = $attributeType?->getName(); - $attributeRestrictionName = ($attributeRestriction && !$attributeRestriction->getChecks()) ? $attributeRestriction->getBase()?->getName() : null; - - $typeName = $attributeTypeName ?: ($attributeRestrictionName ?: $attribute->getName()); - $engineType = EngineType::guess($typeName); - - // If a name cannot be determined from the type, we fallback to the attribute name: - // Prefix the attribute name with the parent element name resulting in a more unique type-name. - if (!$attributeTypeName && !$attributeRestrictionName) { - $engineType = AttributeDeclaringParentTypeDetector::detectWithParentContext($attribute, $context->parent()) - ->andThen(static fn (Type $parent): Option => from_nullable($parent->getName())) - ->map(static fn (string $parentName): EngineType => $engineType->copy($parentName . ucfirst($typeName))) - ->unwrapOr($engineType); - } - + $typeName = (new AttributeTypeNameDetector())($attribute, $context->parent()->unwrap()); $configure = pipe( static fn (EngineType $engineType): EngineType => (new Configurator\AttributeConfigurator())($engineType, $attribute, $context), ); @@ -119,7 +96,7 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext return new PropertyCollection( new Property( $attribute->getName(), - $configure($engineType) + $configure(EngineType::guess($typeName)) ) ); }