Skip to content

Commit f66a2ae

Browse files
committed
Introduce CodingStandardsStrategy for customizable naming conventions
Add CodingStandardsStrategyInterface to allow users to customize naming conventions for generated code (type names, method names, namespace segments, enum cases, property accessors, parameter names). Introduce CodeGeneratorContext as a value object bundling TypeNamespaceMap and CodingStandardsStrategyInterface, replacing TypeNamespaceMap as the configuration carrier through models, contexts, assemblers, and rules.
1 parent 0c424f6 commit f66a2ae

99 files changed

Lines changed: 1396 additions & 712 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

UPGRADING.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
11
# V4 to V5
22

3+
## Coding standards strategy
4+
5+
A new `CodingStandardsStrategyInterface` lets you customize naming conventions for all generated code.
6+
By default, `DefaultCodingStandardsStrategy` is used, which matches the existing `Normalizer` behavior -- no changes required for existing users.
7+
8+
Configure it via `Config::setCodingStandards()`:
9+
10+
```php
11+
use Phpro\SoapClient\CodeGenerator\CodingStandards\CodingStandardsStrategyInterface;
12+
use Phpro\SoapClient\CodeGenerator\Config\Config;
13+
14+
return ($config = Config::create())
15+
->setCodingStandards(new MyCodingStandards())
16+
->setEngine(...)
17+
->setTypeNamespaceMap(...)
18+
// ...
19+
```
20+
21+
[Read more about coding standards customization.](/docs/code-generation/coding-standards.md)
22+
23+
### Context classes require CodeGeneratorContext
24+
25+
`TypeContext`, `PropertyContext`, `ClientMethodContext`, and `ClassMapContext` now require `CodeGeneratorContext` as an additional last constructor parameter and expose `getCodeGeneratorContext(): CodeGeneratorContext`.
26+
27+
### New convenience methods on Property
28+
29+
`Property` has new convenience methods that use the configured coding standards:
30+
31+
- `$property->methodName(string $prefix)`: Generates an accessor method name (e.g. `methodName('with')` returns `withFirstName`).
32+
- `$property->parameterName()`: Normalizes the property name for use as a PHP parameter name.
33+
34+
### Generated config pattern
35+
36+
The generated configuration now uses `return ($config = Config::create())` so that `$config` can be referenced in nested calls (e.g. `$config->getCodingStandards()`).
37+
338
## Configuration overhaul
439

540
The configuration system has been significantly reworked.
@@ -102,7 +137,7 @@ A built-in `PrefixBasedTypeNamespaceStrategy` derives a sub-namespace from the X
102137
use Phpro\SoapClient\CodeGenerator\TypeNamespaceMap\Strategy\PrefixBasedTypeNamespaceStrategy;
103138

104139
TypeNamespaceMap::create(new Destination('src/Type', 'App\\Type'))
105-
->withStrategy(new PrefixBasedTypeNamespaceStrategy())
140+
->withStrategy(PrefixBasedTypeNamespaceStrategy::create($config->getCodingStandards()))
106141
```
107142

108143
With this strategy, a type in the `gml` xmlns prefix is automatically placed in `src/Type/Gml` with namespace `App\Type\Gml`.
@@ -112,14 +147,14 @@ Explicit `withMapping()` entries always take precedence over the strategy. You c
112147
```php
113148
TypeNamespaceMap::create(new Destination('src/Type', 'App\\Type'))
114149
->withMapping('http://special.example.com', new Destination('src/Type/Special', 'App\\Type\\Special'))
115-
->withStrategy(new PrefixBasedTypeNamespaceStrategy())
150+
->withStrategy(PrefixBasedTypeNamespaceStrategy::create($config->getCodingStandards()))
116151
```
117152

118153
You can also implement a custom strategy via `TypeNamespaceMapStrategyInterface` or pass any callable.
119154

120155
### Duplicate type strategies are now namespace-aware
121156

122-
The `IntersectDuplicateTypesStrategy` and `RemoveDuplicateTypesStrategy` now use a factory pattern (`create()`) that accepts the `TypeNamespaceMap`. When types map to different PHP namespaces, they are not considered duplicates.
157+
The `IntersectDuplicateTypesStrategy` and `RemoveDuplicateTypesStrategy` now use a factory pattern (`create()`) that accepts the `CodeGeneratorContext`. When types map to different PHP namespaces, they are not considered duplicates.
123158

124159
## Interactive config generator improvements
125160

docs/code-generation/assemblers.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,4 +613,16 @@ Possible contexts:
613613
- `ClassMapContext`: Triggered during the `generate:classmap` command.
614614
- `TypeContext`: Triggered during the `generate:types` command for every type in the SOAP scheme.
615615
- `PropertyContext`: Triggered during the `generate:types` command for every property in a SOAP type.
616-
- 'FileContext': Triggered during every `generate:*` command.
616+
- `ClientMethodContext`: Triggered during the `generate:client` command for every method.
617+
- `FileContext`: Triggered during every `generate:*` command.
618+
619+
All assembler contexts expose a `getCodeGeneratorContext(): CodeGeneratorContext` method.
620+
Through the `CodeGeneratorContext`, you can access the `TypeNamespaceMap` and `CodingStandardsStrategyInterface`:
621+
622+
```php
623+
// Access the namespace map:
624+
$context->getCodeGeneratorContext()->typeNamespaceMap;
625+
626+
// Access the coding standards:
627+
$context->getCodeGeneratorContext()->codingStandards;
628+
```
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Coding standards
2+
3+
The `CodingStandardsStrategyInterface` lets you customize how names are normalized during code generation.
4+
This is useful when your project has specific naming conventions that differ from the defaults.
5+
6+
By default, `DefaultCodingStandardsStrategy` is used, which delegates to the built-in `Normalizer` methods.
7+
This matches the existing behavior, so no changes are needed for existing users.
8+
9+
## Interface
10+
11+
```php
12+
use Phpro\SoapClient\CodeGenerator\CodingStandards\CodingStandardsStrategyInterface;
13+
14+
interface CodingStandardsStrategyInterface
15+
{
16+
/**
17+
* Convert SOAP type name to PHP class/enum name (e.g. "my-type" -> "MyType")
18+
*/
19+
public function normalizeTypeName(string $name): string;
20+
21+
/**
22+
* Convert SOAP operation name to PHP method name (e.g. "GetUsers" -> "getUsers")
23+
*/
24+
public function normalizeOperationName(string $method): string;
25+
26+
/**
27+
* Normalize XML namespace segment for PHP namespace (e.g. "my-prefix" -> "MyPrefix")
28+
* Return null to skip the segment.
29+
*/
30+
public function normalizeNamespaceSegment(string $segment): ?string;
31+
32+
/**
33+
* Normalize enum value to PHP enum case name (e.g. "some-value" -> "SomeValue")
34+
*/
35+
public function normalizeEnumCaseName(string $value): string;
36+
37+
/**
38+
* Generate accessor method name from prefix + property name
39+
* (e.g. "get" + "firstName" -> "getFirstName")
40+
*/
41+
public function generatePropertyAccessorMethodName(string $prefix, string $property): string;
42+
43+
/**
44+
* Normalize a parameter name for constructors and setters.
45+
* Input is the fixed property name. Output is the parameter name for PHP code.
46+
* This enables named arguments: new Foo(fooBar: 'x') while property stays $foo_bar.
47+
*/
48+
public function normalizeParameterName(string $propertyName): string;
49+
}
50+
```
51+
52+
## Configuration
53+
54+
Configure coding standards via `Config::setCodingStandards()`.
55+
The generated config uses `return ($config = Config::create())` so that `$config` can be referenced in nested calls:
56+
57+
```php
58+
use Phpro\SoapClient\CodeGenerator\Config\Config;
59+
use Phpro\SoapClient\CodeGenerator\Config\Destination;
60+
use Phpro\SoapClient\CodeGenerator\Config\TypeNamespaceMap;
61+
use Phpro\SoapClient\CodeGenerator\TypeNamespaceMap\Strategy\PrefixBasedTypeNamespaceStrategy;
62+
63+
return ($config = Config::create())
64+
->setCodingStandards(new MyCodingStandards())
65+
->setEngine(...)
66+
->setTypeNamespaceMap(
67+
TypeNamespaceMap::create(new Destination('src/Type', 'App\\Type'))
68+
->withStrategy(new PrefixBasedTypeNamespaceStrategy($config->getCodingStandards()))
69+
)
70+
->setClient(...)
71+
->setClassMap(...)
72+
;
73+
```
74+
75+
When using `PrefixBasedTypeNamespaceStrategy`, pass the coding standards to its constructor so that namespace segments are normalized using the same strategy.
76+
77+
## Creating a custom strategy
78+
79+
Implement `CodingStandardsStrategyInterface` to define your own naming conventions:
80+
81+
```php
82+
use Phpro\SoapClient\CodeGenerator\CodingStandards\CodingStandardsStrategyInterface;
83+
84+
class MyCodingStandards implements CodingStandardsStrategyInterface
85+
{
86+
public function normalizeTypeName(string $name): string
87+
{
88+
// e.g. prefix all types
89+
return 'Soap' . ucfirst($name);
90+
}
91+
92+
public function normalizeOperationName(string $method): string
93+
{
94+
return lcfirst($method);
95+
}
96+
97+
public function normalizeNamespaceSegment(string $segment): ?string
98+
{
99+
return ucfirst($segment);
100+
}
101+
102+
public function normalizeEnumCaseName(string $value): string
103+
{
104+
return strtoupper($value);
105+
}
106+
107+
public function generatePropertyAccessorMethodName(string $prefix, string $property): string
108+
{
109+
return $prefix . ucfirst($property);
110+
}
111+
112+
public function normalizeParameterName(string $propertyName): string
113+
{
114+
return lcfirst($propertyName);
115+
}
116+
}
117+
```
118+
119+
## Accessing coding standards in custom assemblers
120+
121+
Inside a custom assembler, the `CodeGeneratorContext` is available through the assembler context:
122+
123+
```php
124+
use Phpro\SoapClient\CodeGenerator\Assembler\AssemblerInterface;
125+
use Phpro\SoapClient\CodeGenerator\Context\ContextInterface;
126+
use Phpro\SoapClient\CodeGenerator\Context\PropertyContext;
127+
128+
class MyAssembler implements AssemblerInterface
129+
{
130+
public function canAssemble(ContextInterface $context): bool
131+
{
132+
return $context instanceof PropertyContext;
133+
}
134+
135+
public function assemble(ContextInterface $context): void
136+
{
137+
assert($context instanceof PropertyContext);
138+
139+
$codingStandards = $context->getCodeGeneratorContext()->codingStandards;
140+
$typeNamespaceMap = $context->getCodeGeneratorContext()->typeNamespaceMap;
141+
142+
// Use coding standards to generate a custom method name:
143+
$methodName = $codingStandards->generatePropertyAccessorMethodName(
144+
'has',
145+
$context->getProperty()->getName()
146+
);
147+
148+
// Or use the convenience methods on Property:
149+
$getterName = $context->getProperty()->methodName('get');
150+
$paramName = $context->getProperty()->parameterName();
151+
152+
// ...
153+
}
154+
}
155+
```

docs/code-generation/configuration.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use Phpro\SoapClient\CodeGenerator\TypeNamespaceMap\Strategy\PrefixBasedTypeName
1717
use Phpro\SoapClient\Soap\EngineOptions;
1818
use Phpro\SoapClient\Soap\DefaultEngineFactory;
1919

20-
return Config::create()
20+
return ($config = Config::create())
2121
->setEngine(DefaultEngineFactory::create(
2222
EngineOptions::defaults($wsdl)
2323
->withWsdlLoader(new FlatteningLoader(new StreamWrapperLoader()))
@@ -33,7 +33,7 @@ return Config::create()
3333
->withMapping('http://www.xmlns.mapping', new Destination('src/Type/OtherDir', 'App\\Type\\OtherDir'))
3434
// Or use a strategy to automatically resolve destinations from xmlns prefixes:
3535
// This strategy will only be called for XML namespaces that don't have an explicit mapping configured.
36-
->withStrategy(new PrefixBasedTypeNamespaceStrategy())
36+
->withStrategy(new PrefixBasedTypeNamespaceStrategy($config->getCodingStandards()))
3737
)
3838
->setClient(new ClientConfig('MySoapClient', new Destination('SoapClient', 'src/SoapClient')))
3939
->setClassMap(new ClassMapConfig('AcmeClassmap', new Destination('Acme\\Classmap', 'src/acme/classmap')))
@@ -201,9 +201,16 @@ Config::create()
201201
```
202202

203203
The duplicate type strategies use a factory pattern (`create()`) that returns a closure.
204-
This closure receives the `TypeNamespaceMap` from the configuration, enabling namespace-aware duplicate detection.
204+
This closure receives the `CodeGeneratorContext` from the configuration, enabling namespace-aware duplicate detection.
205205
When types map to different PHP namespaces, they are not considered duplicates.
206206

207+
**Coding standards**
208+
209+
Use `setCodingStandards(CodingStandardsStrategyInterface)` to customize how names are normalized in generated code.
210+
By default, `DefaultCodingStandardsStrategy` is used, which matches the existing behavior.
211+
212+
[Read more about coding standards customization.](coding-standards.md)
213+
207214
**Enumeration options**
208215

209216
You can configure how the code generator deals with XSD enumeration types.

docs/code-generation/rules.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,4 +261,7 @@ Possible contexts:
261261
- `ClassMapContext`: Triggered during the `generate:classmap` command.
262262
- `TypeContext`: Triggered during the `generate:types` command for every type in the SOAP scheme.
263263
- `PropertyContext`: Triggered during the `generate:types` command for every property in a SOAP type.
264+
- `ClientMethodContext`: Triggered during the `generate:client` command for every method.
264265
- `FileContext`: Triggered during every generate command.
266+
267+
All contexts expose `getCodeGeneratorContext(): CodeGeneratorContext` for accessing the `TypeNamespaceMap` and `CodingStandardsStrategyInterface`.

docs/drivers/metadata.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ return Config::create()
7171
// ...
7272
```
7373

74-
The `create()` factory method returns a closure that receives the `TypeNamespaceMap` from the configuration,
74+
The `create()` factory method returns a closure that receives the `CodeGeneratorContext` from the configuration,
7575
enabling the strategy to determine target PHP namespaces for each type.
7676

7777
### Type replacements

spec/Phpro/SoapClient/CodeGenerator/ClassMapGeneratorSpec.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
namespace spec\Phpro\SoapClient\CodeGenerator;
44

55
use Phpro\SoapClient\CodeGenerator\ClassMapGenerator;
6+
use Phpro\SoapClient\CodeGenerator\CodingStandards\DefaultCodingStandardsStrategy;
67
use Phpro\SoapClient\CodeGenerator\Config\ClassMapConfig;
78
use Phpro\SoapClient\CodeGenerator\Config\Destination;
89
use Phpro\SoapClient\CodeGenerator\Config\TypeNamespaceMap;
910
use Phpro\SoapClient\CodeGenerator\Context\ClassMapContext;
11+
use Phpro\SoapClient\CodeGenerator\Context\CodeGeneratorContext;
1012
use Phpro\SoapClient\CodeGenerator\Context\FileContext;
1113
use Phpro\SoapClient\CodeGenerator\GeneratorInterface;
1214
use Phpro\SoapClient\CodeGenerator\Model\TypeMap;
@@ -45,8 +47,10 @@ function it_generates_classmaps(RuleSetInterface $ruleSet, FileGenerator $file)
4547
$ruleSet->applyRules(Argument::type(ClassMapContext::class))->shouldBeCalled();
4648
$ruleSet->applyRules(Argument::type(FileContext::class))->shouldBeCalled();
4749
$file->generate()->willReturn('code');
50+
$namespaceMap = TypeNamespaceMap::create(new Destination('/app', 'App\\Mynamespace'));
51+
$codeGeneratorContext = new CodeGeneratorContext($namespaceMap, new DefaultCodingStandardsStrategy());
4852
$this->generate($file, TypeMap::fromMetadata(
49-
TypeNamespaceMap::create(new Destination('/app', 'App\\Mynamespace')),
53+
$codeGeneratorContext,
5054
new TypeCollection(),
5155
))->shouldReturn('code');
5256
}

spec/Phpro/SoapClient/CodeGenerator/ClientGeneratorSpec.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
use Laminas\Code\Generator\Exception\ClassNotFoundException;
66
use Phpro\SoapClient\CodeGenerator\ClassMapGenerator;
77
use Phpro\SoapClient\CodeGenerator\ClientGenerator;
8+
use Phpro\SoapClient\CodeGenerator\CodingStandards\DefaultCodingStandardsStrategy;
89
use Phpro\SoapClient\CodeGenerator\Config\ClientConfig;
910
use Phpro\SoapClient\CodeGenerator\Config\Destination;
1011
use Phpro\SoapClient\CodeGenerator\Config\TypeNamespaceMap;
1112
use Phpro\SoapClient\CodeGenerator\Context\ClientContext;
1213
use Phpro\SoapClient\CodeGenerator\Context\ClientMethodContext;
14+
use Phpro\SoapClient\CodeGenerator\Context\CodeGeneratorContext;
1315
use Phpro\SoapClient\CodeGenerator\Context\FileContext;
1416
use Phpro\SoapClient\CodeGenerator\GeneratorInterface;
1517
use Phpro\SoapClient\CodeGenerator\Model\Client;
@@ -52,11 +54,12 @@ function it_is_a_generator()
5254
function it_generates_clients(RuleSetInterface $ruleSet, FileGenerator $file, ClassGenerator $class)
5355
{
5456
$typeNamespaceMap = TypeNamespaceMap::create(new Destination('/app', ''));
57+
$codeGeneratorContext = new CodeGeneratorContext($typeNamespaceMap, new DefaultCodingStandardsStrategy());
5558
$method = new ClientMethod(
5659
'Test',
57-
[new Parameter('parameters', 'Test', $typeNamespaceMap, XsdType::create('Test'))],
58-
ReturnType::fromMetaData($typeNamespaceMap, XsdType::create('TestResponse')),
59-
$typeNamespaceMap,
60+
[new Parameter('parameters', 'Test', $codeGeneratorContext, XsdType::create('Test'))],
61+
ReturnType::fromMetaData($codeGeneratorContext, XsdType::create('TestResponse')),
62+
$codeGeneratorContext,
6063
new MethodMeta()
6164
);
6265
$ruleSet->applyRules(Argument::type(ClientMethodContext::class))->shouldBeCalled();
@@ -77,11 +80,12 @@ function it_generates_clients(RuleSetInterface $ruleSet, FileGenerator $file, Cl
7780
function it_generates_clients_for_file_without_classes(RuleSetInterface $ruleSet, FileGenerator $file, ClassGenerator $class)
7881
{
7982
$typeNamespaceMap = TypeNamespaceMap::create(new Destination('/app', ''));
83+
$codeGeneratorContext = new CodeGeneratorContext($typeNamespaceMap, new DefaultCodingStandardsStrategy());
8084
$method = new ClientMethod(
8185
'Test',
82-
[new Parameter('parameters', 'Test', $typeNamespaceMap, XsdType::create('Test'))],
83-
ReturnType::fromMetaData($typeNamespaceMap, XsdType::create('TestResponse')),
84-
$typeNamespaceMap,
86+
[new Parameter('parameters', 'Test', $codeGeneratorContext, XsdType::create('Test'))],
87+
ReturnType::fromMetaData($codeGeneratorContext, XsdType::create('TestResponse')),
88+
$codeGeneratorContext,
8589
new MethodMeta()
8690
);
8791

spec/Phpro/SoapClient/CodeGenerator/Context/ClassMapContextSpec.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
namespace spec\Phpro\SoapClient\CodeGenerator\Context;
44

5+
use Phpro\SoapClient\CodeGenerator\CodingStandards\DefaultCodingStandardsStrategy;
56
use Phpro\SoapClient\CodeGenerator\Config\ClassMapConfig;
67
use Phpro\SoapClient\CodeGenerator\Config\Destination;
78
use Phpro\SoapClient\CodeGenerator\Config\TypeNamespaceMap;
89
use Phpro\SoapClient\CodeGenerator\Context\ClassMapContext;
10+
use Phpro\SoapClient\CodeGenerator\Context\CodeGeneratorContext;
911
use Phpro\SoapClient\CodeGenerator\Context\ContextInterface;
1012
use Phpro\SoapClient\CodeGenerator\Model\TypeMap;
1113
use PhpSpec\ObjectBehavior;
@@ -25,12 +27,13 @@ function let(FileGenerator $fileGenerator)
2527
{
2628
$typeDestination = new Destination('src/type', 'App\\Mynamespace');
2729
$namespaceMap = TypeNamespaceMap::create($typeDestination);
28-
$this->typeMap = new TypeMap($namespaceMap, []);
30+
$context = new CodeGeneratorContext($namespaceMap, new DefaultCodingStandardsStrategy());
31+
$this->typeMap = new TypeMap($context, []);
2932

3033
$classMapDestination = new Destination('src/classmap', 'App\\Mynamespace');
3134
$classMapConfig = new ClassMapConfig('ClassMap', $classMapDestination);
3235

33-
$this->beConstructedWith($fileGenerator, $this->typeMap, $classMapConfig);
36+
$this->beConstructedWith($fileGenerator, $this->typeMap, $classMapConfig, $context);
3437
}
3538

3639
function it_is_initializable()

0 commit comments

Comments
 (0)