Skip to content

Commit 602b289

Browse files
authored
Merge pull request #602 from veewee/coding-standards
Introduce CodingStandardsStrategy for customizable naming conventions
2 parents 0c424f6 + 1bbb0d7 commit 602b289

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(new PrefixBasedTypeNamespaceStrategy($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(new PrefixBasedTypeNamespaceStrategy($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+
`TypeContext`, `PropertyContext`, `ClientMethodContext`, and `ClassMapContext` 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+
`TypeContext`, `PropertyContext`, `ClientMethodContext`, and `ClassMapContext` 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)