Skip to content

Commit 669f867

Browse files
committed
Introduce XMLNS namespace detetion during config setup
1 parent fbae697 commit 669f867

8 files changed

Lines changed: 233 additions & 77 deletions

File tree

src/Phpro/SoapClient/CodeGenerator/Config/TypeNamespaceMap.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Soap\Engine\Metadata\Model\XsdType;
66

77
/**
8-
* @psalm-type Strategy = \Closure(string $xmlns, Destination $fallback): Destination
8+
* @psalm-type Strategy = \Closure(string $xmlNamespace, string $xmlNamespaceName, Destination $fallback): Destination
99
*/
1010
final readonly class TypeNamespaceMap
1111
{
@@ -52,14 +52,14 @@ public function withStrategy(?\Closure $strategy): self
5252

5353
public function detectDestinationForType(XsdType $type): Destination
5454
{
55-
$xmlns = $type->getXmlNamespace(); // TODO : Is this the correct one?
55+
$xmlns = $type->getXmlNamespace();
5656
if ($xmlns && array_key_exists($xmlns, $this->map)) {
5757
return $this->map[$xmlns];
5858
}
5959

6060
$fallback = $this->fallback;
6161
if ($this->strategy !== null) {
62-
$fallback = ($this->strategy)($xmlns, $fallback);
62+
$fallback = ($this->strategy)($xmlns, $type->getXmlNamespaceName(), $fallback);
6363
}
6464

6565
return $fallback;

src/Phpro/SoapClient/CodeGenerator/ConfigGenerator.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Phpro\SoapClient\CodeGenerator\Config\Destination;
99
use Phpro\SoapClient\CodeGenerator\Config\TypeNamespaceMap;
1010
use Phpro\SoapClient\CodeGenerator\Context\ConfigContext;
11+
use Phpro\SoapClient\CodeGenerator\Util\Normalizer;
1112
use Laminas\Code\Generator\FileGenerator;
1213
use Phpro\SoapClient\Soap\DefaultEngineFactory;
1314
use Phpro\SoapClient\Soap\EngineOptions;
@@ -41,14 +42,42 @@ class ConfigGenerator implements GeneratorInterface
4142

4243
/**
4344
* Generate code for TypeNamespaceMap configuration
45+
*
46+
* @param array<string, string> $detectedXmlNamespaces
4447
*/
45-
private function generateTypeNamespaceMapCode(Destination $fallback): string
46-
{
47-
return sprintf(
48+
private function generateTypeNamespaceMapCode(
49+
Destination $fallback,
50+
array $detectedXmlNamespaces,
51+
string $indentation
52+
): string {
53+
$createCall = sprintf(
4854
'TypeNamespaceMap::create(new Destination(%s, %s))',
4955
var_export($fallback->path, true),
5056
var_export($fallback->namespace, true)
5157
);
58+
59+
if ($detectedXmlNamespaces === []) {
60+
return $createCall;
61+
}
62+
63+
$lines = [];
64+
$lines[] = GeneratorInterface::EOL . $indentation . $indentation . $createCall;
65+
foreach ($detectedXmlNamespaces as $xmlns => $prefix) {
66+
$segment = Normalizer::normalizeNamespaceSegment($prefix);
67+
$suggestedPath = $fallback->path . ($segment !== null ? '/' . $segment : '');
68+
$suggestedNamespace = $fallback->namespace . ($segment !== null ? '\\' . $segment : '');
69+
70+
$lines[] = sprintf(
71+
'%s%s// ->withMapping(%s, new Destination(%s, %s))',
72+
$indentation,
73+
$indentation,
74+
var_export($xmlns, true),
75+
var_export($suggestedPath, true),
76+
var_export($suggestedNamespace, true)
77+
);
78+
}
79+
80+
return implode(GeneratorInterface::EOL, $lines) . GeneratorInterface::EOL . $indentation;
5281
}
5382

5483
/**
@@ -115,10 +144,11 @@ public function generate(FileGenerator $file, $context): string
115144

116145
// Generate TypeNamespaceMap setter
117146
if ($typeDestination = $context->getTypeDestination()) {
147+
$detectedXmlNamespaces = $context->getDetectedXmlNamespaces();
118148
$body .= sprintf(
119149
"%s->setTypeNamespaceMap(%s)".GeneratorInterface::EOL,
120150
$file->getIndentation(),
121-
$this->generateTypeNamespaceMapCode($typeDestination)
151+
$this->generateTypeNamespaceMapCode($typeDestination, $detectedXmlNamespaces, $file->getIndentation())
122152
);
123153
}
124154

src/Phpro/SoapClient/CodeGenerator/Context/ConfigContext.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ final class ConfigContext implements ContextInterface
1313
private ?Destination $typeDestination = null;
1414
private ?ClientConfig $clientConfig = null;
1515
private ?ClassMapConfig $classMapConfig = null;
16+
/** @var array<string, string> */
17+
private array $detectedXmlNamespaces = [];
1618

1719
public function getWsdl(): string
1820
{
@@ -73,4 +75,18 @@ public function setClassMapConfig(ClassMapConfig $classMapConfig): self
7375

7476
return $this;
7577
}
78+
79+
/** @return array<string, string> */
80+
public function getDetectedXmlNamespaces(): array
81+
{
82+
return $this->detectedXmlNamespaces;
83+
}
84+
85+
/** @param array<string, string> $xmlNamespaces */
86+
public function setDetectedXmlNamespaces(array $xmlNamespaces): self
87+
{
88+
$this->detectedXmlNamespaces = $xmlNamespaces;
89+
90+
return $this;
91+
}
7692
}

src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,21 @@ public static function normalizeEnumCaseName(
239239
return $normalized;
240240
}
241241

242+
public static function normalizeNamespaceSegment(string $segment): ?string
243+
{
244+
if ($segment === '') {
245+
return null;
246+
}
247+
248+
$normalized = self::normalizeClassname($segment);
249+
250+
if (preg_match('/^[0-9]/', $normalized)) {
251+
$normalized = 'Ns' . $normalized;
252+
}
253+
254+
return $normalized;
255+
}
256+
242257
/**
243258
* @param non-empty-string $type
244259
*

src/Phpro/SoapClient/Console/Command/GenerateConfigCommand.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
use Phpro\SoapClient\CodeGenerator\Context\ConfigContext;
1010
use Phpro\SoapClient\CodeGenerator\Util\Normalizer;
1111
use Phpro\SoapClient\Console\Validator\NotBlankValidator;
12+
use Phpro\SoapClient\Soap\EngineOptions;
1213
use Phpro\SoapClient\Util\Filesystem;
14+
use Soap\WsdlReader\Model\Wsdl1;
15+
use Soap\WsdlReader\Wsdl1Reader;
1316
use Symfony\Component\Console\Command\Command;
1417
use Symfony\Component\Console\Input\InputInterface;
1518
use Symfony\Component\Console\Input\InputOption;
@@ -55,7 +58,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int
5558
);
5659
}
5760

58-
$context->setWsdl($io->ask('Wsdl location (URL or path to file)', null, $required));
61+
$wsdlUri = $io->ask('Wsdl location (URL or path to file)', null, $required);
62+
$context->setWsdl($wsdlUri);
63+
64+
$io->warning('Attempting to load WSDL... (this might take a while)');
65+
$wsdl = $this->loadWsdl($wsdlUri);
66+
67+
if (!$wsdl) {
68+
$io->warning('Could not load the provided WSDL with default engine options.');
69+
$io->info('Continuing generating configuration...');
70+
}
71+
72+
$context->setDetectedXmlNamespaces($wsdl?->namespaces->namespaceToNameMap ?? []);
5973
$context->setGenerateDocblocks($io->confirm('Should methods be generated with docblocks?', true));
6074
$name = $io->ask(
6175
'Generic name used to name this client (Results in <name>Client <name>Classmap etc.)',
@@ -81,6 +95,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8195
$this->filesystem->putFileContents($destination, $generator->generate(new FileGenerator(), $context));
8296
$io->success('Config has been written to ' . $destination);
8397

98+
if (!$wsdl) {
99+
$io->warning(
100+
'The WSDL could not be loaded with default options.' .
101+
'You may need to configure custom engine options or verify the WSDL file manually before continuing.'
102+
);
103+
104+
return self::FAILURE;
105+
}
106+
84107
return self::SUCCESS;
85108
}
109+
110+
private function loadWsdl(string $wsdl): ?Wsdl1
111+
{
112+
try {
113+
$options = EngineOptions::defaults($wsdl);
114+
$loader = $options->getWsdlLoader();
115+
116+
return (new Wsdl1Reader($loader))($wsdl);
117+
} catch (\Throwable) {
118+
return null;
119+
}
120+
}
86121
}

test/PhproTest/SoapClient/Unit/CodeGenerator/Config/TypeNamespaceMapTest.php

Lines changed: 13 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,6 @@ public function it_can_add_mapping_for_xml_namespace(): void
4646
$this->assertSame($customDestination, $destination);
4747
}
4848

49-
#[Test]
50-
public function it_can_add_multiple_mappings(): void
51-
{
52-
$fallback = new Destination('/src/Type', 'App\\Type');
53-
$custom1 = new Destination('/src/Custom1', 'App\\Custom1');
54-
$custom2 = new Destination('/src/Custom2', 'App\\Custom2');
55-
56-
$map = TypeNamespaceMap::create($fallback)
57-
->withMapping('http://custom1.example.com', $custom1)
58-
->withMapping('http://custom2.example.com', $custom2);
59-
60-
$type1 = XsdType::create('Type1')->withXmlNamespace('http://custom1.example.com');
61-
$type2 = XsdType::create('Type2')->withXmlNamespace('http://custom2.example.com');
62-
63-
$this->assertSame($custom1, $map->detectDestinationForType($type1));
64-
$this->assertSame($custom2, $map->detectDestinationForType($type2));
65-
}
66-
6749
#[Test]
6850
public function it_returns_fallback_when_type_has_no_xml_namespace(): void
6951
{
@@ -79,21 +61,6 @@ public function it_returns_fallback_when_type_has_no_xml_namespace(): void
7961
$this->assertSame($fallback, $destination);
8062
}
8163

82-
#[Test]
83-
public function it_returns_fallback_when_xml_namespace_is_not_mapped(): void
84-
{
85-
$fallback = new Destination('/src/Type', 'App\\Type');
86-
$custom = new Destination('/src/Custom', 'App\\Custom');
87-
88-
$map = TypeNamespaceMap::create($fallback)
89-
->withMapping('http://custom.example.com', $custom);
90-
91-
$type = XsdType::create('OtherType')->withXmlNamespace('http://other.example.com');
92-
$destination = $map->detectDestinationForType($type);
93-
94-
$this->assertSame($fallback, $destination);
95-
}
96-
9764
#[Test]
9865
public function it_is_immutable_when_adding_mappings(): void
9966
{
@@ -151,21 +118,6 @@ public function it_can_override_existing_mapping(): void
151118
$this->assertSame($custom2, $destination);
152119
}
153120

154-
#[Test]
155-
public function it_can_add_a_strategy(): void
156-
{
157-
$fallback = new Destination('/src/Type', 'App\\Type');
158-
$strategyDestination = new Destination('/src/Strategy', 'App\\Strategy');
159-
160-
$map = TypeNamespaceMap::create($fallback)
161-
->withStrategy(fn (string $xmlns, Destination $fallback) => $strategyDestination);
162-
163-
$type = XsdType::create('SomeType')->withXmlNamespace('http://example.com/schema');
164-
$destination = $map->detectDestinationForType($type);
165-
166-
$this->assertSame($strategyDestination, $destination);
167-
}
168-
169121
#[Test]
170122
public function it_does_not_call_strategy_when_xmlns_is_in_map(): void
171123
{
@@ -175,7 +127,7 @@ public function it_does_not_call_strategy_when_xmlns_is_in_map(): void
175127

176128
$map = TypeNamespaceMap::create($fallback)
177129
->withMapping('http://mapped.example.com', $mapped)
178-
->withStrategy(function (string $xmlns, Destination $fallback) use (&$strategyCalled) {
130+
->withStrategy(function (string $xmlns, string $xmlNamespaceName, Destination $fallback) use (&$strategyCalled) {
179131
$strategyCalled = true;
180132
return new Destination('/src/Strategy', 'App\\Strategy');
181133
});
@@ -197,7 +149,7 @@ public function it_calls_strategy_when_xmlns_is_not_in_map(): void
197149

198150
$map = TypeNamespaceMap::create($fallback)
199151
->withMapping('http://mapped.example.com', $mapped)
200-
->withStrategy(function (string $xmlns, Destination $fallback) use (&$strategyCalled, $strategyDestination) {
152+
->withStrategy(function (string $xmlns, string $xmlNamespaceName, Destination $fallback) use (&$strategyCalled, $strategyDestination) {
201153
$strategyCalled = true;
202154
return $strategyDestination;
203155
});
@@ -210,46 +162,38 @@ public function it_calls_strategy_when_xmlns_is_not_in_map(): void
210162
}
211163

212164
#[Test]
213-
public function it_passes_xmlns_and_fallback_to_strategy(): void
165+
public function it_passes_all_arguments_to_strategy(): void
214166
{
215167
$fallback = new Destination('/src/Type', 'App\\Type');
216168
$receivedXmlns = null;
169+
$receivedNamespaceName = null;
217170
$receivedFallback = null;
218171

219172
$map = TypeNamespaceMap::create($fallback)
220-
->withStrategy(function (string $xmlns, Destination $fb) use (&$receivedXmlns, &$receivedFallback) {
173+
->withStrategy(function (string $xmlns, string $xmlNamespaceName, Destination $fb) use (&$receivedXmlns, &$receivedNamespaceName, &$receivedFallback) {
221174
$receivedXmlns = $xmlns;
175+
$receivedNamespaceName = $xmlNamespaceName;
222176
$receivedFallback = $fb;
223177
return $fb;
224178
});
225179

226-
$type = XsdType::create('SomeType')->withXmlNamespace('http://example.com/schema');
180+
$type = XsdType::create('SomeType')
181+
->withXmlNamespace('http://example.com/schema')
182+
->withXmlNamespaceName('ex');
227183
$map->detectDestinationForType($type);
228184

229185
$this->assertSame('http://example.com/schema', $receivedXmlns);
186+
$this->assertSame('ex', $receivedNamespaceName);
230187
$this->assertSame($fallback, $receivedFallback);
231188
}
232189

233-
#[Test]
234-
public function it_returns_fallback_when_no_strategy_and_xmlns_not_in_map(): void
235-
{
236-
$fallback = new Destination('/src/Type', 'App\\Type');
237-
238-
$map = TypeNamespaceMap::create($fallback);
239-
240-
$type = XsdType::create('SomeType')->withXmlNamespace('http://example.com/schema');
241-
$destination = $map->detectDestinationForType($type);
242-
243-
$this->assertSame($fallback, $destination);
244-
}
245-
246190
#[Test]
247191
public function it_can_use_strategy_to_calculate_destination_based_on_xmlns(): void
248192
{
249193
$fallback = new Destination('/src/Type', 'App\\Type');
250194

251195
$map = TypeNamespaceMap::create($fallback)
252-
->withStrategy(function (string $xmlns, Destination $fallback): Destination {
196+
->withStrategy(function (string $xmlns, string $xmlNamespaceName, Destination $fallback): Destination {
253197
if (str_contains($xmlns, 'xoev.de')) {
254198
return new Destination('/src/Type/Xoev', 'App\\Type\\Xoev');
255199
}
@@ -273,7 +217,7 @@ public function it_can_remove_strategy_by_setting_null(): void
273217
$strategyDestination = new Destination('/src/Strategy', 'App\\Strategy');
274218

275219
$mapWithStrategy = TypeNamespaceMap::create($fallback)
276-
->withStrategy(fn (string $xmlns, Destination $fallback) => $strategyDestination);
220+
->withStrategy(fn (string $xmlns, string $xmlNamespaceName, Destination $fallback) => $strategyDestination);
277221

278222
$mapWithoutStrategy = $mapWithStrategy->withStrategy(null);
279223

@@ -290,7 +234,7 @@ public function it_is_immutable_when_adding_strategy(): void
290234
$strategyDestination = new Destination('/src/Strategy', 'App\\Strategy');
291235

292236
$map1 = TypeNamespaceMap::create($fallback);
293-
$map2 = $map1->withStrategy(fn (string $xmlns, Destination $fallback) => $strategyDestination);
237+
$map2 = $map1->withStrategy(fn (string $xmlns, string $xmlNamespaceName, Destination $fallback) => $strategyDestination);
294238

295239
$type = XsdType::create('SomeType')->withXmlNamespace('http://example.com/schema');
296240

0 commit comments

Comments
 (0)