Skip to content

Commit 8e37325

Browse files
committed
Prefix attribute type-names with parent type name.
1 parent 1128124 commit 8e37325

File tree

7 files changed

+199
-6
lines changed

7 files changed

+199
-6
lines changed

src/Metadata/Converter/SchemaToTypesConverter.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use GoetasWebservices\XML\XSDReader\Schema\Schema;
88
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
99
use Soap\Engine\Metadata\Collection\TypeCollection;
10+
use Soap\WsdlReader\Metadata\Converter\Types\ParentContext;
1011
use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext;
1112
use Soap\WsdlReader\Metadata\Converter\Types\Visitor\ElementVisitor;
1213
use Soap\WsdlReader\Metadata\Converter\Types\Visitor\TypeVisitor;
@@ -22,15 +23,24 @@ public function __invoke(Schema $schema, TypesConverterContext $context): TypeCo
2223
...filter_nulls([
2324
...flat_map(
2425
$schema->getTypes(),
25-
static fn (Type $type): TypeCollection => (new TypeVisitor())($type, $context)
26+
static fn (Type $type): TypeCollection => (new TypeVisitor())(
27+
$type,
28+
$context->onParent(ParentContext::create($type))
29+
)
2630
),
2731
...flat_map(
2832
$schema->getElements(),
29-
static fn (ElementDef $element): TypeCollection => (new ElementVisitor())($element, $context)
33+
static fn (ElementDef $element): TypeCollection => (new ElementVisitor())(
34+
$element,
35+
$context->onParent(ParentContext::create($element))
36+
)
3037
),
3138
...flat_map(
3239
$schema->getSchemas(),
33-
fn (Schema $childSchema): TypeCollection => $this->__invoke($childSchema, $context)
40+
fn (Schema $childSchema): TypeCollection => $this->__invoke(
41+
$childSchema,
42+
$context->onParent(null)
43+
)
3444
)
3545
])
3646
);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Metadata\Converter\Types\Detector;
4+
5+
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeContainer;
6+
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
7+
use GoetasWebservices\XML\XSDReader\Schema\Item;
8+
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
9+
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
10+
use Psl\Option\Option;
11+
use Soap\WsdlReader\Metadata\Converter\Types\ParentContext;
12+
use function Psl\Option\none;
13+
use function Psl\Option\some;
14+
15+
final class AttributeDeclaringParentTypeDetector
16+
{
17+
/**
18+
* This class detects the declaring parent type of an attribute.
19+
* It can be used together with the ParentContext and works as followed
20+
*
21+
* - If the parent is an AttributeContainer, it will check if the parent has the attribute
22+
* - If the parent is not declaring the attribute, it will check if the parent is extending another type and test this extended type.
23+
*
24+
* @return Option<Type>
25+
*/
26+
public function __invoke(AttributeItem $item, ?SchemaItem $parent): Option
27+
{
28+
$parent = match(true) {
29+
$parent instanceof Item => $parent->getType(),
30+
default => $parent,
31+
};
32+
33+
if (!$parent instanceof Type) {
34+
return none();
35+
}
36+
37+
if ($parent instanceof AttributeContainer) {
38+
foreach ($parent->getAttributes() as $parentAttribute) {
39+
if ($parentAttribute->getName() === $item->getName()) {
40+
/** @var Option<Type> */
41+
return some($parent);
42+
}
43+
}
44+
}
45+
46+
$extensionBase = $parent->getExtension()?->getBase();
47+
if ($extensionBase) {
48+
return $this->__invoke($item, $extensionBase);
49+
}
50+
51+
return none();
52+
}
53+
54+
55+
/**
56+
* @return Option<Type>
57+
*/
58+
public static function detectWithParentContext(AttributeItem $item, Option $parentContext): Option
59+
{
60+
/** @var self $calculate */
61+
static $calculate = new self();
62+
63+
return $parentContext
64+
->andThen(static fn (ParentContext $context) => $calculate($item, $context->currentParent()));
65+
}
66+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\WsdlReader\Metadata\Converter\Types;
4+
5+
use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
6+
use function Psl\Iter\first;
7+
use function Psl\Iter\last;
8+
9+
final class ParentContext
10+
{
11+
/**
12+
* @param non-empty-list<SchemaItem> $items
13+
*/
14+
private function __construct(
15+
public readonly array $items,
16+
) {
17+
}
18+
19+
public static function create(SchemaItem $item): self
20+
{
21+
return new self([$item]);
22+
}
23+
24+
public function withNextParent(SchemaItem $item): self
25+
{
26+
return new self([...$this->items, $item]);
27+
}
28+
29+
public function rootParent(): SchemaItem
30+
{
31+
return first($this->items);
32+
}
33+
34+
public function currentParent(): SchemaItem
35+
{
36+
return last($this->items);
37+
}
38+
}

src/Metadata/Converter/Types/TypesConverterContext.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
namespace Soap\WsdlReader\Metadata\Converter\Types;
55

66
use GoetasWebservices\XML\XSDReader\Schema\Schema;
7+
use Psl\Option\Option;
78
use Soap\Engine\Metadata\Collection\TypeCollection;
89
use Soap\WsdlReader\Model\Definitions\Namespaces;
910
use Soap\WsdlReader\Parser\Definitions\SchemaParser;
11+
use function Psl\Option\from_nullable;
12+
use function Psl\Option\none;
1013

1114
final class TypesConverterContext
1215
{
@@ -20,9 +23,15 @@ final class TypesConverterContext
2023
*/
2124
private array $visited = [];
2225

26+
/**
27+
* @var Option<ParentContext>
28+
*/
29+
private Option $parentContext;
30+
2331
private function __construct(
2432
public readonly Namespaces $knownNamespaces
2533
) {
34+
$this->parentContext = none();
2635
}
2736

2837
public static function default(Namespaces $knownNamespaces): self
@@ -57,4 +66,19 @@ public function visit(Schema $schema, callable $visitor): TypeCollection
5766

5867
return $visitor($schema);
5968
}
69+
70+
public function onParent(?ParentContext $parentContext): self
71+
{
72+
$this->parentContext = from_nullable($parentContext);
73+
74+
return $this;
75+
}
76+
77+
/**
78+
* @return Option<ParentContext>
79+
*/
80+
public function parent(): Option
81+
{
82+
return $this->parentContext;
83+
}
6084
}

src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeSingle;
99
use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group;
1010
use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
11+
use Psl\Option\Option;
1112
use Soap\Engine\Metadata\Collection\PropertyCollection;
1213
use Soap\Engine\Metadata\Model\Property;
1314
use Soap\Engine\Metadata\Model\TypeMeta;
1415
use Soap\Engine\Metadata\Model\XsdType as EngineType;
1516
use Soap\WsdlReader\Metadata\Converter\Types\Configurator;
17+
use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeDeclaringParentTypeDetector;
1618
use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext;
1719
use function Psl\Fun\pipe;
20+
use function Psl\Option\from_nullable;
1821
use function Psl\Result\wrap;
1922
use function Psl\Type\instance_of;
2023
use function Psl\Vec\flat_map;
@@ -88,8 +91,26 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext
8891
return $this->parseAttributes($attribute, $context);
8992
}
9093

94+
// Detecting the type-name for an attribute is complex.
95+
// We first try to use the type name,
96+
// Next up is the base type of the restriction if there aren't any restriction checks configured.
97+
// Finally there is a fallback to the attribute name
9198
$attributeType = $attribute instanceof AttributeSingle ? $attribute->getType() : null;
92-
$typeName = $attributeType?->getName() ?: $attribute->getName();
99+
$attributeRestriction = $attributeType?->getRestriction();
100+
$attributeTypeName = $attributeType?->getName();
101+
$attributeRestrictionName = ($attributeRestriction && !$attributeRestriction->getChecks()) ? $attributeRestriction->getBase()?->getName() : null;
102+
103+
$typeName = $attributeTypeName ?: ($attributeRestrictionName ?: $attribute->getName());
104+
$engineType = EngineType::guess($typeName);
105+
106+
// If a name cannot be determined from the type, we fallback to the attribute name:
107+
// Prefix the attribute name with the parent element name resulting in a more unique type-name.
108+
if (!$attributeTypeName && !$attributeRestrictionName) {
109+
$engineType = AttributeDeclaringParentTypeDetector::detectWithParentContext($attribute, $context->parent())
110+
->andThen(static fn (Type $parent): Option => from_nullable($parent->getName()))
111+
->map(static fn (string $parentName): EngineType => $engineType->copy($parentName . ucfirst($typeName)))
112+
->unwrapOr($engineType);
113+
}
93114

94115
$configure = pipe(
95116
static fn (EngineType $engineType): EngineType => (new Configurator\AttributeConfigurator())($engineType, $attribute, $context),
@@ -98,7 +119,7 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext
98119
return new PropertyCollection(
99120
new Property(
100121
$attribute->getName(),
101-
$configure(EngineType::guess($typeName))
122+
$configure($engineType)
102123
)
103124
);
104125
}

src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ private function detectInlineTypes(ElementItem $element, TypesConverterContext $
6262
return new TypeCollection();
6363
}
6464

65-
return $elementVisitor($element, $context);
65+
return $elementVisitor($element, $context->onParent(
66+
$context->parent()->unwrap()->withNextParent($element)
67+
));
6668
}
6769
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
SOAP XML Schema 1001: Prepend element name before attribute type names for more unique type-names.
3+
--FILE--
4+
<?php
5+
include __DIR__."/test_schema.inc";
6+
$schema = <<<EOF
7+
<complexType name="VehicleCoreType">
8+
<sequence>
9+
<element name="VehType" minOccurs="0" type="string" />
10+
</sequence>
11+
<attribute name="DriveType" use="optional">
12+
<simpleType>
13+
<restriction base="NMTOKEN">
14+
<enumeration value="AWD" />
15+
<enumeration value="4WD" />
16+
<enumeration value="Unspecified" />
17+
</restriction>
18+
</simpleType>
19+
</attribute>
20+
</complexType>
21+
EOF;
22+
test_schema($schema,'type="tns:VehicleCoreType"');
23+
?>
24+
--EXPECT--
25+
Methods:
26+
> test(VehicleCoreType $testParam): void
27+
28+
Types:
29+
> http://test-uri/:VehicleCoreType {
30+
?string $VehType
31+
@?VehicleCoreTypeDriveType in (AWD|4WD|Unspecified) $DriveType
32+
}

0 commit comments

Comments
 (0)