From e6959a33824d62fd223d2bbdedb251ef174eb41b Mon Sep 17 00:00:00 2001 From: Toon Verwerft <toonverwerft@gmail.com> Date: Fri, 20 Dec 2024 08:27:35 +0100 Subject: [PATCH] Generate backed enums from XSD --- UPGRADING.md | 6 +- composer.json | 4 +- docs/code-generation/configuration.md | 128 ++++++++++++++---- .../CodeGenerator/Util/NormalizerSpec.php | 19 +++ .../Assembler/ClassMapAssembler.php | 70 +++++++--- .../CodeGenerator/ClassMapGenerator.php | 5 +- .../CodeGenerator/ClientFactoryGenerator.php | 11 +- .../CodeGenerator/ClientGenerator.php | 4 +- .../CodeGenerator/Config/Config.php | 47 ++++++- .../Config/EnumerationGenerationStrategy.php | 22 +++ .../CodeGenerator/ConfigGenerator.php | 5 +- .../CodeGenerator/EnumerationGenerator.php | 77 +++++++++++ .../CodeGenerator/GeneratorInterface.php | 6 +- .../Calculator/TypeNameCalculator.php | 5 +- .../TypeEnhancer/MetaTypeEnhancer.php | 8 +- .../CodeGenerator/TypeGenerator.php | 4 +- .../CodeGenerator/Util/Normalizer.php | 29 ++++ .../Console/Command/GenerateTypesCommand.php | 48 ++++--- src/Phpro/SoapClient/Soap/EngineOptions.php | 1 - .../Metadata/Detector/LocalEnumDetector.php | 36 +++++ .../TypeReplacer/AppendTypesManipulator.php | 26 ++++ .../LocalToGlobalEnumReplacer.php | 28 ++++ .../TypeReplacer/TypeReplacers.php | 17 ++- .../Assembler/ClassMapAssemblerTest.php | 22 ++- .../ClientFactoryGeneratorTest.php | 6 +- .../EnumerationGeneratorTest.php | 123 +++++++++++++++++ .../TypeEnhancer/MetaTypeEnhancerTest.php | 22 +-- .../Detector/LocalEnumDetectorTest.php | 54 ++++++++ .../AppendTypesManipulatorTest.php | 44 ++++++ .../LocalToGlobalEnumReplacerTest.php | 64 +++++++++ .../TypeReplacer/TypeReplacersTest.php | 24 ++++ 31 files changed, 854 insertions(+), 111 deletions(-) create mode 100644 src/Phpro/SoapClient/CodeGenerator/Config/EnumerationGenerationStrategy.php create mode 100644 src/Phpro/SoapClient/CodeGenerator/EnumerationGenerator.php create mode 100644 src/Phpro/SoapClient/Soap/Metadata/Detector/LocalEnumDetector.php create mode 100644 src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/AppendTypesManipulator.php create mode 100644 src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/LocalToGlobalEnumReplacer.php create mode 100644 test/PhproTest/SoapClient/Unit/CodeGenerator/EnumerationGeneratorTest.php create mode 100644 test/PhproTest/SoapClient/Unit/Soap/Metadata/Detector/LocalEnumDetectorTest.php create mode 100644 test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/AppendTypesManipulatorTest.php create mode 100644 test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/LocalToGlobalEnumReplacerTest.php diff --git a/UPGRADING.md b/UPGRADING.md index 79622bc0..1913f7d9 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -54,9 +54,9 @@ Change the engine inside your (generated) ClientFactory: $engine = DefaultEngineFactory::create( EngineOptions::defaults($wsdl) ->withEncoderRegistry( - EncoderRegistry::default()->addClassMapCollection( - CalcClassmap::getCollection() - ) + EncoderRegistry::default() + ->addClassMapCollection(CalcClassmap::types()) + ->addBackedEnumClassMapCollection(CalcClassmap::enums()) ) // If you want to enable WSDL caching: // ->withCache($yourPsr6CachePool) diff --git a/composer.json b/composer.json index 5465e394..b1d5ae07 100644 --- a/composer.json +++ b/composer.json @@ -17,9 +17,9 @@ "laminas/laminas-code": "^4.14.0", "php-soap/cached-engine": "~0.3", "php-soap/engine": "^2.14.0", - "php-soap/encoding": "~0.14", + "php-soap/encoding": "~0.15", "php-soap/psr18-transport": "^1.7", - "php-soap/wsdl-reader": "~0.20", + "php-soap/wsdl-reader": "~0.21", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/console": "~5.4 || ~6.0 || ~7.0", diff --git a/docs/code-generation/configuration.md b/docs/code-generation/configuration.md index c495a3ad..2762e193 100644 --- a/docs/code-generation/configuration.md +++ b/docs/code-generation/configuration.md @@ -6,20 +6,21 @@ The code generation commands require a configuration file to determine how the S <?php // my-soap-config.php -use Phpro\SoapClient\CodeGenerator\Config\Config; -use Phpro\SoapClient\CodeGenerator\Rules; use Phpro\SoapClient\CodeGenerator\Assembler; -use Phpro\SoapClient\Soap\DefaultEngineFactory; +use Phpro\SoapClient\CodeGenerator\Rules; +use Phpro\SoapClient\CodeGenerator\Config\Config; use Phpro\SoapClient\Soap\EngineOptions; -use Phpro\SoapClient\Soap\Metadata\Manipulators\DuplicateTypes\IntersectDuplicateTypesStrategy; -use Phpro\SoapClient\Soap\Metadata\MetadataOptions; -use Soap\Wsdl\Loader\FlatteningLoader; -use Soap\Wsdl\Loader\StreamWrapperLoader; +use Phpro\SoapClient\Soap\DefaultEngineFactory; return Config::create() ->setEngine(DefaultEngineFactory::create( - EngineOptions::defaults('wsdl.xml') + EngineOptions::defaults($wsdl) ->withWsdlLoader(new FlatteningLoader(new StreamWrapperLoader())) + ->withEncoderRegistry( + EncoderRegistry::default() + ->addClassMapCollection(SomeClassmap::types()) + ->addBackedEnumClassMapCollection(SomeClassmap::enums()) + ) )) ->setTypeDestination('src/SoapTypes') ->setTypeNamespace('SoapTypes') @@ -29,23 +30,39 @@ return Config::create() ->setClassMapNamespace('Acme\\Classmap') ->setClassMapDestination('src/acme/classmap') ->setClassMapName('AcmeClassmap') - ->setTypeMetadataOptions( - MetadataOptions::empty() - ->withTypesManipulator(new IntersectDuplicateTypesStrategy()) - ) - ->addRule(new Rules\AssembleRule(new Assembler\GetterAssembler( - (new Assembler\GetterAssemblerOptions()) - ->withReturnType() - ->withBoolGetters() + ->addRule(new Rules\AssembleRule(new Assembler\GetterAssembler(new Assembler\GetterAssemblerOptions()))) + ->addRule(new Rules\AssembleRule(new Assembler\ImmutableSetterAssembler( + new Assembler\ImmutableSetterAssemblerOptions() ))) - ->addRule(new Rules\TypenameMatchesRule( - new Rules\AssembleRule(new Assembler\RequestAssembler()), - '/Request$/' - )) - ->addRule(new Rules\TypenameMatchesRule( - new Rules\AssembleRule(new Assembler\ResultAssembler()), - '/Response$/' - )) + ->addRule( + new Rules\IsRequestRule( + $engine->getMetadata(), + new Rules\MultiRule([ + new Rules\AssembleRule(new Assembler\RequestAssembler()), + new Rules\AssembleRule(new Assembler\ConstructorAssembler(new Assembler\ConstructorAssemblerOptions())), + ]) + ) + ) + ->addRule( + new Rules\IsResultRule( + $engine->getMetadata(), + new Rules\MultiRule([ + new Rules\AssembleRule(new Assembler\ResultAssembler()), + ]) + ) + ) + ->addRule( + new Rules\IsExtendingTypeRule( + $engine->getMetadata(), + new Rules\AssembleRule(new Assembler\ExtendingTypeAssembler()) + ) + ) + ->addRule( + new Rules\IsAbstractTypeRule( + $engine->getMetadata(), + new Rules\AssembleRule(new Assembler\AbstractClassAssembler()) + ) + ) ; ``` @@ -64,6 +81,29 @@ and provide additional options like the preferred SOAP version. [Read more about engines.](https://github.com/php-soap/engine) +```php +use Phpro\SoapClient\Soap\EngineOptions; +use Phpro\SoapClient\Soap\DefaultEngineFactory; + +DefaultEngineFactory::create( + EngineOptions::defaults($wsdl) + ->withWsdlLoader(new FlatteningLoader(new StreamWrapperLoader())) + ->withEncoderRegistry( + EncoderRegistry::default() + ->addClassMapCollection(SomeClassmap::types()) + ->addBackedEnumClassMapCollection(SomeClassmap::enums()) + ) + // If you want to enable WSDL caching: + // ->withCache() + // If you want to use Alternate HTTP settings: + // ->withWsdlLoader() + // ->withTransport() + // If you want specific SOAP setting: + // ->withWsdlParserContext() + // ->withWsdlServiceSelectionCriteria() +); +``` + **type destination** String - REQUIRED @@ -128,3 +168,43 @@ Config::create() ) ) ``` + +**Metadata manipulations** + +The metadata manipulations are a set of strategies that can be applied to the metadata before the code generation starts. +You can read more about this in the documentation in the section [metadata](../drivers/metadata.md). + +Examples: + +```php +use Phpro\SoapClient\CodeGenerator\Config\Config; +use Phpro\SoapClient\Soap\Metadata\Manipulators\DuplicateTypes\IntersectDuplicateTypesStrategy; +use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\TypeReplacers; + +Config::create() + ->setDuplicateTypeIntersectStrategy(new IntersectDuplicateTypesStrategy()) + ->setTypeReplacementStrategy(TypeReplacers::defaults()->add(new MyDateReplacer())); +``` + +**Enumeration options** + +You can configure how the code generator deals with XSD enumeration types. +There are 2 type of XSD enumerations: + +- `global`: Are available as a global simpletype inside the XSD. +- `local`: Are configured as an internal type on an element or attribute and don't really have a name. + +The default behavior is to generate a PHP Enum for global enumerations only because +We want to avoid naming conflicts with other types for local enumerations. + +It is possible to opt-in into using these local enumerations as well: + +```php +use Phpro\SoapClient\CodeGenerator\Config\Config; +use Phpro\SoapClient\CodeGenerator\Config\EnumerationGenerationStrategy; + +Config::create() + ->setEnumerationGenerationStrategy(EnumerationGenerationStrategy::LocalAndGlobal); +``` + +**Note**: This will dynamically add some extra type replacements and type manipulations to the metadata before the code generation starts. diff --git a/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php b/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php index 5db5e298..2ac34b59 100644 --- a/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php +++ b/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php @@ -60,6 +60,25 @@ function it_noramizes_properties() $this->normalizeProperty('My-./final*prop_123')->shouldReturn('MyFinalProp_123'); } + function it_normalizes_enum_cases() + { + $this->normalizeEnumCaseName('')->shouldReturn('Empty'); + + $this->normalizeEnumCaseName('-1')->shouldReturn('Value_Minus_1'); + $this->normalizeEnumCaseName('0')->shouldReturn('Value_0'); + $this->normalizeEnumCaseName('1')->shouldReturn('Value_1'); + $this->normalizeEnumCaseName('10000')->shouldReturn('Value_10000'); + + $this->normalizeEnumCaseName('final')->shouldReturn('final'); + $this->normalizeEnumCaseName('Final')->shouldReturn('Final'); + $this->normalizeEnumCaseName('UpperCased')->shouldReturn('UpperCased'); + $this->normalizeEnumCaseName('my-./*prop_123')->shouldReturn('myProp_123'); + $this->normalizeEnumCaseName('My-./*prop_123')->shouldReturn('MyProp_123'); + $this->normalizeEnumCaseName('My-./final*prop_123')->shouldReturn('MyFinalProp_123'); + + $this->normalizeEnumCaseName('1 specific option')->shouldReturn('Value_1SpecificOption'); + } + function it_normalizes_datatypes() { $this->normalizeDataType('string')->shouldReturn('string'); diff --git a/src/Phpro/SoapClient/CodeGenerator/Assembler/ClassMapAssembler.php b/src/Phpro/SoapClient/CodeGenerator/Assembler/ClassMapAssembler.php index db051e25..a5f0abad 100644 --- a/src/Phpro/SoapClient/CodeGenerator/Assembler/ClassMapAssembler.php +++ b/src/Phpro/SoapClient/CodeGenerator/Assembler/ClassMapAssembler.php @@ -4,6 +4,7 @@ use Phpro\SoapClient\CodeGenerator\Context\ClassMapContext; use Phpro\SoapClient\CodeGenerator\Context\ContextInterface; +use Phpro\SoapClient\CodeGenerator\Model\Type; use Phpro\SoapClient\CodeGenerator\Model\TypeMap; use Phpro\SoapClient\Exception\AssemblerException; use Laminas\Code\Generator\ClassGenerator; @@ -48,31 +49,66 @@ public function assemble(ContextInterface $context) $file->setUse(ClassMapCollection::class); $file->setUse(ClassMap::class); $linefeed = $file::LINE_FEED; - $classMap = $this->assembleClassMap($typeMap, $linefeed, $file->getIndentation()); - $code = $this->assembleClassMapCollection($classMap, $linefeed).$linefeed; - $class->addMethodFromGenerator( - (new MethodGenerator('getCollection')) - ->setStatic(true) - ->setBody('return '.$code) - ->setReturnType(ClassMapCollection::class) - ); + $indentation = $file->getIndentation(); + + $class->addMethodFromGenerator($this->generateTypes($typeMap, $linefeed, $indentation)); + $class->addMethodFromGenerator($this->generateEnums($typeMap, $linefeed, $indentation)); } catch (\Exception $e) { throw AssemblerException::fromException($e); } } - /*** - * @param TypeMap $typeMap - * @param string $linefeed - * @param string $indentation - * - * @return string + private function generateTypes( + TypeMap $typeMap, + string $linefeed, + string $indentation, + ): MethodGenerator { + $classMap = $this->assembleClassMap( + $typeMap, + $linefeed, + $indentation, + static fn (Type $type) => !(new IsConsideredScalarType())($type->getMeta()) + ); + $code = $this->assembleClassMapCollection($classMap, $linefeed).$linefeed; + + return (new MethodGenerator('types')) + ->setStatic(true) + ->setBody('return '.$code) + ->setReturnType(ClassMapCollection::class); + } + + private function generateEnums( + TypeMap $typeMap, + string $linefeed, + string $indentation, + ): MethodGenerator { + $classMap = $this->assembleClassMap( + $typeMap, + $linefeed, + $indentation, + static fn (Type $type) => (new IsConsideredScalarType())($type->getMeta()) + && $type->getMeta()->enums()->isSome() + ); + $code = $this->assembleClassMapCollection($classMap, $linefeed).$linefeed; + + return (new MethodGenerator('enums')) + ->setStatic(true) + ->setBody('return '.$code) + ->setReturnType(ClassMapCollection::class); + } + + /** + * @param \Closure(Type): bool $predicate */ - private function assembleClassMap(TypeMap $typeMap, string $linefeed, string $indentation): string - { + private function assembleClassMap( + TypeMap $typeMap, + string $linefeed, + string $indentation, + \Closure $predicate + ): string { $classMap = []; foreach ($typeMap->getTypes() as $type) { - if ((new IsConsideredScalarType())($type->getMeta())) { + if (!$predicate($type)) { continue; } diff --git a/src/Phpro/SoapClient/CodeGenerator/ClassMapGenerator.php b/src/Phpro/SoapClient/CodeGenerator/ClassMapGenerator.php index 47032963..9dce91dc 100644 --- a/src/Phpro/SoapClient/CodeGenerator/ClassMapGenerator.php +++ b/src/Phpro/SoapClient/CodeGenerator/ClassMapGenerator.php @@ -4,14 +4,13 @@ use Phpro\SoapClient\CodeGenerator\Context\ClassMapContext; use Phpro\SoapClient\CodeGenerator\Context\FileContext; +use Phpro\SoapClient\CodeGenerator\Model\Type; use Phpro\SoapClient\CodeGenerator\Model\TypeMap; use Phpro\SoapClient\CodeGenerator\Rules\RuleSetInterface; use Laminas\Code\Generator\FileGenerator; /** - * Class ClassMapGenerator - * - * @package Phpro\SoapClient\CodeGenerator + * @template-implements GeneratorInterface<TypeMap> */ class ClassMapGenerator implements GeneratorInterface { diff --git a/src/Phpro/SoapClient/CodeGenerator/ClientFactoryGenerator.php b/src/Phpro/SoapClient/CodeGenerator/ClientFactoryGenerator.php index 913ef067..908392dc 100644 --- a/src/Phpro/SoapClient/CodeGenerator/ClientFactoryGenerator.php +++ b/src/Phpro/SoapClient/CodeGenerator/ClientFactoryGenerator.php @@ -7,6 +7,7 @@ use Phpro\SoapClient\Caller\EngineCaller; use Phpro\SoapClient\Caller\EventDispatchingCaller; use Phpro\SoapClient\CodeGenerator\Context\ClientFactoryContext; +use Phpro\SoapClient\CodeGenerator\Model\Type; use Phpro\SoapClient\Soap\DefaultEngineFactory; use Phpro\SoapClient\Soap\EngineOptions; use Soap\Encoding\EncoderRegistry; @@ -16,9 +17,7 @@ use Laminas\Code\Generator\MethodGenerator; /** - * Class ClientBuilderGenerator - * - * @package Phpro\SoapClient\CodeGenerator + * @template-implements GeneratorInterface<ClientFactoryContext> */ class ClientFactoryGenerator implements GeneratorInterface { @@ -26,9 +25,9 @@ class ClientFactoryGenerator implements GeneratorInterface \$engine = DefaultEngineFactory::create( EngineOptions::defaults(\$wsdl) ->withEncoderRegistry( - EncoderRegistry::default()->addClassMapCollection( - %2\$s::getCollection() - ) + EncoderRegistry::default() + ->addClassMapCollection(%2\$s::types()) + ->addBackedEnumClassMapCollection(%2\$s::enums()) ) // If you want to enable WSDL caching: // ->withCache() diff --git a/src/Phpro/SoapClient/CodeGenerator/ClientGenerator.php b/src/Phpro/SoapClient/CodeGenerator/ClientGenerator.php index fda2c2db..5aad26aa 100644 --- a/src/Phpro/SoapClient/CodeGenerator/ClientGenerator.php +++ b/src/Phpro/SoapClient/CodeGenerator/ClientGenerator.php @@ -15,9 +15,7 @@ use Laminas\Code\Generator\FileGenerator; /** - * Class ClientGenerator - * - * @package Phpro\SoapClient\CodeGenerator + * @template-implements GeneratorInterface<Client> */ class ClientGenerator implements GeneratorInterface { diff --git a/src/Phpro/SoapClient/CodeGenerator/Config/Config.php b/src/Phpro/SoapClient/CodeGenerator/Config/Config.php index fe36ef77..3eb4888e 100644 --- a/src/Phpro/SoapClient/CodeGenerator/Config/Config.php +++ b/src/Phpro/SoapClient/CodeGenerator/Config/Config.php @@ -9,17 +9,20 @@ use Phpro\SoapClient\CodeGenerator\Rules\RuleSetInterface; use Phpro\SoapClient\CodeGenerator\Util\Normalizer; use Phpro\SoapClient\Exception\InvalidArgumentException; +use Phpro\SoapClient\Soap\Metadata\Detector\LocalEnumDetector; use Phpro\SoapClient\Soap\Metadata\Manipulators\DuplicateTypes\IntersectDuplicateTypesStrategy; use Phpro\SoapClient\Soap\Metadata\Manipulators\MethodsManipulatorChain; +use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\LocalToGlobalEnumReplacer; +use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\AppendTypesManipulator; use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\ReplaceMethodTypesManipulator; use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\ReplaceTypesManipulator; -use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\TypeReplacer; use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\TypeReplacers; use Phpro\SoapClient\Soap\Metadata\Manipulators\TypesManipulatorChain; use Phpro\SoapClient\Soap\Metadata\Manipulators\TypesManipulatorInterface; use Phpro\SoapClient\Soap\Metadata\MetadataFactory; use Phpro\SoapClient\Soap\Metadata\MetadataOptions; use Soap\Engine\Engine; +use Soap\Engine\Metadata\Collection\TypeCollection; use Soap\Engine\Metadata\Metadata; final class Config @@ -56,9 +59,9 @@ final class Config protected TypesManipulatorInterface $duplicateTypeIntersectStrategy; - protected TypeReplacer $typeReplacementStrategy; + protected TypeReplacers $typeReplacementStrategy; - protected ?MetadataOptions $metadataOptions; + protected ?MetadataOptions $metadataOptions = null; /** * @var RuleSetInterface @@ -80,6 +83,8 @@ final class Config */ protected $classMapDestination; + protected EnumerationGenerationStrategy $enumerationGenerationStrategy; + public function __construct() { $this->typeReplacementStrategy = TypeReplacers::defaults(); @@ -89,6 +94,9 @@ public function __construct() // The resulting type will always be usable, but might contain some additional empty properties. $this->duplicateTypeIntersectStrategy = new IntersectDuplicateTypesStrategy(); + // By default, we only generate global enumerations to avoid naming conflicts. + $this->enumerationGenerationStrategy = EnumerationGenerationStrategy::default(); + $this->ruleSet = new RuleSet([ new Rules\AssembleRule(new Assembler\PropertyAssembler()), new Rules\AssembleRule(new Assembler\ClassMapAssembler()), @@ -270,15 +278,28 @@ public function setTypeDestination($typeDestination): self public function getMetadataOptions(): MetadataOptions { - return $this->metadataOptions ?? MetadataOptions::empty() + if ($this->metadataOptions) { + return $this->metadataOptions; + } + + $typeReplacementStrategy = new TypeReplacers(...$this->typeReplacementStrategy); + $appendTypes = static fn () => new TypeCollection(); + + if ($this->enumerationGenerationStrategy === EnumerationGenerationStrategy::LocalAndGlobal) { + $typeReplacementStrategy = $typeReplacementStrategy->add(new LocalToGlobalEnumReplacer()); + $appendTypes = new LocalEnumDetector(); + } + + return MetadataOptions::empty() ->withTypesManipulator( new TypesManipulatorChain( + new AppendTypesManipulator($appendTypes), $this->duplicateTypeIntersectStrategy, - new ReplaceTypesManipulator($this->typeReplacementStrategy), + new ReplaceTypesManipulator($typeReplacementStrategy), ) )->withMethodsManipulator( new MethodsManipulatorChain( - new ReplaceMethodTypesManipulator($this->typeReplacementStrategy) + new ReplaceMethodTypesManipulator($typeReplacementStrategy) ) ); } @@ -291,7 +312,7 @@ public function getManipulatedMetadata(): Metadata ); } - public function setTypeReplacementStrategy(TypeReplacer $typeReplacementStrategy): self + public function setTypeReplacementStrategy(TypeReplacers $typeReplacementStrategy): self { $this->typeReplacementStrategy = $typeReplacementStrategy; @@ -380,4 +401,16 @@ public function setClassMapDestination(string $classMapDestination): self return $this; } + + public function setEnumerationGenerationStrategy(EnumerationGenerationStrategy $enumerationGenerationStrategy): self + { + $this->enumerationGenerationStrategy = $enumerationGenerationStrategy; + + return $this; + } + + public function getEnumerationGenerationStrategy(): EnumerationGenerationStrategy + { + return $this->enumerationGenerationStrategy; + } } diff --git a/src/Phpro/SoapClient/CodeGenerator/Config/EnumerationGenerationStrategy.php b/src/Phpro/SoapClient/CodeGenerator/Config/EnumerationGenerationStrategy.php new file mode 100644 index 00000000..57e1e9b8 --- /dev/null +++ b/src/Phpro/SoapClient/CodeGenerator/Config/EnumerationGenerationStrategy.php @@ -0,0 +1,22 @@ +<?php + +namespace Phpro\SoapClient\CodeGenerator\Config; + +enum EnumerationGenerationStrategy +{ + + /** + * Only generates and uses globally accessible XSD enumerations. + */ + case GlobalOnly; + + /** + * Tries to find properties that have local XSD enumerations and copies them as global enumerations. + */ + case LocalAndGlobal; + + public static function default(): self + { + return self::GlobalOnly; + } +} diff --git a/src/Phpro/SoapClient/CodeGenerator/ConfigGenerator.php b/src/Phpro/SoapClient/CodeGenerator/ConfigGenerator.php index 26113b5c..4cf6bb9c 100644 --- a/src/Phpro/SoapClient/CodeGenerator/ConfigGenerator.php +++ b/src/Phpro/SoapClient/CodeGenerator/ConfigGenerator.php @@ -5,13 +5,12 @@ use Phpro\SoapClient\CodeGenerator\Config\Config; use Phpro\SoapClient\CodeGenerator\Context\ConfigContext; use Laminas\Code\Generator\FileGenerator; +use Phpro\SoapClient\CodeGenerator\Model\Type; use Phpro\SoapClient\Soap\DefaultEngineFactory; use Phpro\SoapClient\Soap\EngineOptions; /** - * Class ConfigGenerator - * - * @package Phpro\SoapClient\CodeGenerator + * @template-implements GeneratorInterface<ConfigContext> */ class ConfigGenerator implements GeneratorInterface { diff --git a/src/Phpro/SoapClient/CodeGenerator/EnumerationGenerator.php b/src/Phpro/SoapClient/CodeGenerator/EnumerationGenerator.php new file mode 100644 index 00000000..91fba42c --- /dev/null +++ b/src/Phpro/SoapClient/CodeGenerator/EnumerationGenerator.php @@ -0,0 +1,77 @@ +<?php + +namespace Phpro\SoapClient\CodeGenerator; + +use Laminas\Code\Generator\DocBlockGenerator; +use Phpro\SoapClient\CodeGenerator\Model\Type; +use Phpro\SoapClient\CodeGenerator\Util\Normalizer; +use Laminas\Code\Generator\EnumGenerator\EnumGenerator; +use Laminas\Code\Generator\FileGenerator; +use Soap\Engine\Metadata\Model\XsdType; +use function Psl\Dict\pull; +use function Psl\Type\int; + +/** + * @template-implements GeneratorInterface<Type> + */ +class EnumerationGenerator implements GeneratorInterface +{ + /** + * @param FileGenerator $file + * @param Type $context + * @return string + */ + public function generate(FileGenerator $file, $context): string + { + $file->setNamespace($context->getNamespace()); + $file->setBody($this->generateBody($context)); + + return $file->generate(); + } + + private function generateBody(Type $type): string + { + $xsdType = $type->getXsdType(); + $xsdMeta = $xsdType->getMeta(); + $enumType = match ($xsdType->getBaseType()) { + 'int', 'integer' => 'int', + default => 'string', + }; + + $body = EnumGenerator::withConfig([ + 'name' => Normalizer::normalizeClassname($type->getName()), + 'backedCases' => [ + 'type' => $enumType, + 'cases' => $this->buildCases($xsdType, $enumType), + ] + ])->generate(); + + if ($docs = $xsdMeta->docs()->unwrapOr('')) { + $docblock = (new DocBlockGenerator()) + ->setWordWrap(false) + ->setLongDescription($docs) + ->generate(); + $body = $docblock . $body; + } + + return $body; + } + + /** + * @param 'string'|'int' $enumType + * @return array<string, int|string> + */ + private function buildCases(XsdType $xsdType, string $enumType): array + { + $enums = $xsdType->getMeta()->enums()->unwrapOr([]); + + return pull( + $enums, + static fn(string $value): int|string => match ($enumType) { + 'int' => int()->coerce($value), + 'string' => $value, + }, + static fn(string $value): string => Normalizer::normalizeEnumCaseName($value) + ); + } +} diff --git a/src/Phpro/SoapClient/CodeGenerator/GeneratorInterface.php b/src/Phpro/SoapClient/CodeGenerator/GeneratorInterface.php index 52f43967..8e7d377f 100644 --- a/src/Phpro/SoapClient/CodeGenerator/GeneratorInterface.php +++ b/src/Phpro/SoapClient/CodeGenerator/GeneratorInterface.php @@ -8,6 +8,8 @@ * Interface GeneratorInterface * * @package Phpro\SoapClient\CodeGenerator + * + * @template Context */ interface GeneratorInterface { @@ -16,9 +18,9 @@ interface GeneratorInterface /** * @param FileGenerator $file - * @param mixed $model + * @param Context $context * * @return string */ - public function generate(FileGenerator $file, $model): string; + public function generate(FileGenerator $file, $context): string; } diff --git a/src/Phpro/SoapClient/CodeGenerator/TypeEnhancer/Calculator/TypeNameCalculator.php b/src/Phpro/SoapClient/CodeGenerator/TypeEnhancer/Calculator/TypeNameCalculator.php index e6d994e0..2cedaba3 100644 --- a/src/Phpro/SoapClient/CodeGenerator/TypeEnhancer/Calculator/TypeNameCalculator.php +++ b/src/Phpro/SoapClient/CodeGenerator/TypeEnhancer/Calculator/TypeNameCalculator.php @@ -13,9 +13,10 @@ public function __invoke(XsdType $type): string { $meta = $type->getMeta(); $isSimpleType = $meta->isSimple()->unwrapOr(false); + $isGlobalEnum = $isSimpleType && $meta->enums()->isSome() && !$meta->isLocal()->unwrapOr(false); - // For non-simple types, we always want to use the name of the type. - if (!$isSimpleType) { + // For non-simple types or backed enums, we always want to use the name of the type. + if (!$isSimpleType || $isGlobalEnum) { return $type->getName(); } diff --git a/src/Phpro/SoapClient/CodeGenerator/TypeEnhancer/MetaTypeEnhancer.php b/src/Phpro/SoapClient/CodeGenerator/TypeEnhancer/MetaTypeEnhancer.php index 0ec5f756..e9d107ed 100644 --- a/src/Phpro/SoapClient/CodeGenerator/TypeEnhancer/MetaTypeEnhancer.php +++ b/src/Phpro/SoapClient/CodeGenerator/TypeEnhancer/MetaTypeEnhancer.php @@ -22,9 +22,13 @@ public function __construct( */ public function asDocBlockType(string $type): string { + $isLocal = $this->meta->isLocal()->unwrapOr(false); + $isEnum = (bool) $this->meta->enums()->unwrapOr([]); + $isUnion = (bool) $this->meta->unions()->unwrapOr([]); + $type = match (true) { - (bool) $this->meta->enums()->unwrapOr([]) => (new EnumValuesCalculator())($this->meta), - (bool) $this->meta->unions()->unwrapOr([]) => (new UnionTypesCalculator())($this->meta), + $isLocal && $isEnum => (new EnumValuesCalculator())($this->meta), + $isUnion => (new UnionTypesCalculator())($this->meta), default => $type }; diff --git a/src/Phpro/SoapClient/CodeGenerator/TypeGenerator.php b/src/Phpro/SoapClient/CodeGenerator/TypeGenerator.php index f60f5ee2..fb6488cf 100644 --- a/src/Phpro/SoapClient/CodeGenerator/TypeGenerator.php +++ b/src/Phpro/SoapClient/CodeGenerator/TypeGenerator.php @@ -12,9 +12,7 @@ use Laminas\Code\Generator\FileGenerator; /** - * Class TypeGenerator - * - * @package Phpro\SoapClient\CodeGenerator + * @template-implements GeneratorInterface<Type> */ class TypeGenerator implements GeneratorInterface { diff --git a/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php b/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php index be483b8e..f73ef40e 100644 --- a/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php +++ b/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php @@ -2,6 +2,8 @@ namespace Phpro\SoapClient\CodeGenerator\Util; +use function Psl\Result\try_catch; +use function Psl\Type\int; use function Psl\Type\non_empty_string; /** @@ -209,6 +211,33 @@ public static function normalizeProperty(string $property): string return self::camelCase($property, '{[^a-z0-9_]+}i'); } + /** + * @param non-empty-string $emptyCase + * @param non-empty-string $conflictPrefix + * @return non-empty-string + */ + public static function normalizeEnumCaseName( + string $value, + string $emptyCase = 'Empty', + string $conflictPrefix = 'Value_', + string $minusPrefix = 'Minus_', + ): string { + if ($value === '') { + return $emptyCase; + } + + if (is_numeric($value)) { + return $conflictPrefix . str_replace('-', $minusPrefix, $value); + } + + $normalized = self::normalizeProperty($value); + if (preg_match('/^[0-9]/', $normalized)) { + $normalized = $conflictPrefix.$normalized; + } + + return $normalized; + } + /** * @param non-empty-string $type * diff --git a/src/Phpro/SoapClient/Console/Command/GenerateTypesCommand.php b/src/Phpro/SoapClient/Console/Command/GenerateTypesCommand.php index 058bbaba..36637b69 100644 --- a/src/Phpro/SoapClient/Console/Command/GenerateTypesCommand.php +++ b/src/Phpro/SoapClient/Console/Command/GenerateTypesCommand.php @@ -2,6 +2,9 @@ namespace Phpro\SoapClient\Console\Command; +use Phpro\SoapClient\CodeGenerator\Config\Config; +use Phpro\SoapClient\CodeGenerator\EnumerationGenerator; +use Phpro\SoapClient\CodeGenerator\GeneratorInterface; use Phpro\SoapClient\CodeGenerator\Model\Type; use Phpro\SoapClient\CodeGenerator\Model\TypeMap; use Phpro\SoapClient\CodeGenerator\TypeGenerator; @@ -77,12 +80,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int non_empty_string()->assert($config->getTypeNamespace()), $config->getManipulatedMetadata()->getTypes(), ); - $generator = new TypeGenerator($config->getRuleSet()); $typesDestination = non_empty_string()->assert($config->getTypeDestination()); foreach ($typeMap->getTypes() as $type) { $fileInfo = $type->getFileInfo($typesDestination); - if ($this->handleType($generator, $type, $fileInfo)) { + if ($this->handleType($config, $type, $fileInfo)) { $this->output->writeln( sprintf('Generated class %s to %s', $type->getFullName(), $fileInfo->getPathname()) ); @@ -94,18 +96,29 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + /** + * @return GeneratorInterface<Type>|null + */ + private function detectCodeGeneratorForType(Config $config, Type $type): ?GeneratorInterface + { + $isConsideredScalar = (new IsConsideredScalarType())($type->getMeta()); + + return match (true) { + $isConsideredScalar && $type->getMeta()->enums()->isSome() => new EnumerationGenerator(), + !$isConsideredScalar => new TypeGenerator($config->getRuleSet()), + default => null + }; + } + /** * Try to create a class for a type. - * - * @param TypeGenerator $generator - * @param Type $type - * @param SplFileInfo $fileInfo - * @return bool */ - protected function handleType(TypeGenerator $generator, Type $type, SplFileInfo $fileInfo): bool + protected function handleType(Config $config, Type $type, SplFileInfo $fileInfo): bool { - // Skip generation of simple types. - if ((new IsConsideredScalarType())($type->getMeta())) { + $generator = $this->detectCodeGeneratorForType($config, $type); + + // Skip generation of "simple" types without generator. + if (!$generator) { if ($this->output->isVeryVerbose()) { $this->output->writeln('<fg=yellow>Skipped scalar type : '.$type->getFullName().'</fg=yellow>'); } @@ -132,15 +145,14 @@ protected function handleType(TypeGenerator $generator, Type $type, SplFileInfo } /** - * Generates one type class - * - * @param FileGenerator $file - * @param TypeGenerator $generator - * @param Type $type - * @param SplFileInfo $fileInfo + * @param GeneratorInterface<Type> $generator */ - protected function generateType(FileGenerator $file, TypeGenerator $generator, Type $type, SplFileInfo $fileInfo) - { + protected function generateType( + FileGenerator $file, + GeneratorInterface $generator, + Type $type, + SplFileInfo $fileInfo + ): void { $code = $generator->generate($file, $type); $this->filesystem->putFileContents($fileInfo->getPathname(), $code); } diff --git a/src/Phpro/SoapClient/Soap/EngineOptions.php b/src/Phpro/SoapClient/Soap/EngineOptions.php index 4214cf3f..d63e7866 100644 --- a/src/Phpro/SoapClient/Soap/EngineOptions.php +++ b/src/Phpro/SoapClient/Soap/EngineOptions.php @@ -13,7 +13,6 @@ use Soap\Wsdl\Loader\StreamWrapperLoader; use Soap\Wsdl\Loader\WsdlLoader; use Soap\WsdlReader\Locator\ServiceSelectionCriteria; -use Soap\WsdlReader\Model\Definitions\SoapVersion; use Soap\WsdlReader\Parser\Context\ParserContext; use function Psl\Option\from_nullable; diff --git a/src/Phpro/SoapClient/Soap/Metadata/Detector/LocalEnumDetector.php b/src/Phpro/SoapClient/Soap/Metadata/Detector/LocalEnumDetector.php new file mode 100644 index 00000000..035cf84f --- /dev/null +++ b/src/Phpro/SoapClient/Soap/Metadata/Detector/LocalEnumDetector.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Phpro\SoapClient\Soap\Metadata\Detector; + +use Soap\Engine\Metadata\Collection\PropertyCollection; +use Soap\Engine\Metadata\Collection\TypeCollection; +use Soap\Engine\Metadata\Model\Type; + +/** + * Some XSD types like attributes contain local enums that are not globally available as a type. + * This method unwinds them into a separate list. + */ +final class LocalEnumDetector +{ + public function __invoke(TypeCollection $types): TypeCollection + { + $detected = []; + + foreach ($types as $type) { + foreach ($type->getProperties() as $property) { + $xsdType = $property->getType(); + $meta = $xsdType->getMeta(); + $isLocal = $meta->isLocal()->unwrapOr(false); + $isEnum = $meta->enums()->isSome(); + + if ($isLocal && $isEnum) { + $detected[] = new Type($xsdType, new PropertyCollection()); + } + } + } + + return new TypeCollection(...$detected); + } +} diff --git a/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/AppendTypesManipulator.php b/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/AppendTypesManipulator.php new file mode 100644 index 00000000..bd52db88 --- /dev/null +++ b/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/AppendTypesManipulator.php @@ -0,0 +1,26 @@ +<?php +declare(strict_types=1); + +namespace Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer; + +use Phpro\SoapClient\Soap\Metadata\Manipulators\TypesManipulatorInterface; +use Soap\Engine\Metadata\Collection\TypeCollection; + +final class AppendTypesManipulator implements TypesManipulatorInterface +{ + /** + * @param callable(TypeCollection): TypeCollection $buildAppendedTypes + */ + public function __construct( + private readonly mixed $buildAppendedTypes + ) { + } + + public function __invoke(TypeCollection $types): TypeCollection + { + return new TypeCollection( + ...$types, + ...($this->buildAppendedTypes)($types), + ); + } +} diff --git a/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/LocalToGlobalEnumReplacer.php b/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/LocalToGlobalEnumReplacer.php new file mode 100644 index 00000000..bd533d2e --- /dev/null +++ b/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/LocalToGlobalEnumReplacer.php @@ -0,0 +1,28 @@ +<?php + +namespace Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer; + +use Soap\Engine\Metadata\Model\XsdType; + +/** + * This replacer can be used to mark local enums as global. + * It will result in the enum type name being used in the generated code instead of a list of all possible enum values. + */ +final class LocalToGlobalEnumReplacer implements TypeReplacer +{ + public function __invoke(XsdType $xsdType): XsdType + { + $meta = $xsdType->getMeta(); + if (!$meta->isSimple()->unwrapOr(false) + || !$meta->isLocal()->unwrapOr(false) + || !$meta->enums()->isSome() + ) { + return $xsdType; + } + + return $xsdType->copy($xsdType->getName()) + ->withMeta( + static fn ($meta) => $meta->withIsLocal(false) + ); + } +} diff --git a/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/TypeReplacers.php b/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/TypeReplacers.php index 5fa37713..3b6d6929 100644 --- a/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/TypeReplacers.php +++ b/src/Phpro/SoapClient/Soap/Metadata/Manipulators/TypeReplacer/TypeReplacers.php @@ -4,10 +4,13 @@ namespace Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer; use Soap\Engine\Metadata\Model\XsdType; -use function Psl\Fun\pipe; +use Traversable; use function Psl\Iter\reduce; -final class TypeReplacers implements TypeReplacer +/** + * @template-implements \IteratorAggregate<int, TypeReplacer> + */ +final class TypeReplacers implements TypeReplacer, \IteratorAggregate { /** * @var list<TypeReplacer> @@ -17,7 +20,7 @@ final class TypeReplacers implements TypeReplacer /** * @no-named-arguments */ - private function __construct(TypeReplacer ...$replacers) + public function __construct(TypeReplacer ...$replacers) { $this->replacers = $replacers; } @@ -52,4 +55,12 @@ public function __invoke(XsdType $type): XsdType $type, ); } + + /** + * @return Traversable<int, TypeReplacer> + */ + public function getIterator(): Traversable + { + yield from $this->replacers; + } } diff --git a/test/PhproTest/SoapClient/Unit/CodeGenerator/Assembler/ClassMapAssemblerTest.php b/test/PhproTest/SoapClient/Unit/CodeGenerator/Assembler/ClassMapAssemblerTest.php index a8cc73bc..bf8974df 100644 --- a/test/PhproTest/SoapClient/Unit/CodeGenerator/Assembler/ClassMapAssemblerTest.php +++ b/test/PhproTest/SoapClient/Unit/CodeGenerator/Assembler/ClassMapAssemblerTest.php @@ -61,12 +61,19 @@ function it_assembles_a_classmap() class MyClassMap { - public static function getCollection() : \Soap\Encoding\ClassMap\ClassMapCollection + public static function types() : \Soap\Encoding\ClassMap\ClassMapCollection { return new ClassMapCollection( new ClassMap('http://my-namespace.com', 'MyType', Type\MyType::class), ); } + + public static function enums() : \Soap\Encoding\ClassMap\ClassMapCollection + { + return new ClassMapCollection( + new ClassMap('http://my-namespace.com', 'MyEnum', Type\MyEnum::class), + ); + } } @@ -94,6 +101,19 @@ private function createContext() (new XsdType('MyType')) ->withXmlNamespace('http://my-namespace.com') ), + new Type( + $namespace, + 'MyEnum', + 'MyEnum', + [], + (new XsdType('MyEnum')) + ->withXmlNamespace('http://my-namespace.com') + ->withMeta( + static fn (TypeMeta $meta) => $meta + ->withIsSimple(true) + ->withEnums(['value1', 'value2']) + ) + ), ]); return new ClassMapContext($file, $typeMap, 'MyClassMap', 'ClassMapNamespace'); diff --git a/test/PhproTest/SoapClient/Unit/CodeGenerator/ClientFactoryGeneratorTest.php b/test/PhproTest/SoapClient/Unit/CodeGenerator/ClientFactoryGeneratorTest.php index 55b4b3e4..1bb84c51 100644 --- a/test/PhproTest/SoapClient/Unit/CodeGenerator/ClientFactoryGeneratorTest.php +++ b/test/PhproTest/SoapClient/Unit/CodeGenerator/ClientFactoryGeneratorTest.php @@ -41,9 +41,9 @@ public static function factory(string \$wsdl) : \App\Client\Myclient \$engine = DefaultEngineFactory::create( EngineOptions::defaults(\$wsdl) ->withEncoderRegistry( - EncoderRegistry::default()->addClassMapCollection( - SomeClassmap::getCollection() - ) + EncoderRegistry::default() + ->addClassMapCollection(SomeClassmap::types()) + ->addBackedEnumClassMapCollection(SomeClassmap::enums()) ) // If you want to enable WSDL caching: // ->withCache() diff --git a/test/PhproTest/SoapClient/Unit/CodeGenerator/EnumerationGeneratorTest.php b/test/PhproTest/SoapClient/Unit/CodeGenerator/EnumerationGeneratorTest.php new file mode 100644 index 00000000..653d4a55 --- /dev/null +++ b/test/PhproTest/SoapClient/Unit/CodeGenerator/EnumerationGeneratorTest.php @@ -0,0 +1,123 @@ +<?php + +namespace PhproTest\SoapClient\Unit\CodeGenerator; + +use Phpro\SoapClient\CodeGenerator\ConfigGenerator; +use Phpro\SoapClient\CodeGenerator\Context\ConfigContext; +use Phpro\SoapClient\CodeGenerator\EnumerationGenerator; +use Phpro\SoapClient\CodeGenerator\Model\Property; +use Phpro\SoapClient\CodeGenerator\Model\Type; +use PHPUnit\Framework\TestCase; +use Laminas\Code\Generator\FileGenerator; +use Soap\Engine\Metadata\Model\Property as MetaProperty; +use Soap\Engine\Metadata\Model\TypeMeta; +use Soap\Engine\Metadata\Model\XsdType; + +class EnumerationGeneratorTest extends TestCase +{ + public function testStringBackedEnumGeneration(): void + { + $type = new Type( + 'MyNamespace', + 'MyType', + 'MyType', + [], + XsdType::create('MyType') + ->withBaseType('string') + ->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta + ->withIsSimple(true) + ->withEnums(['', 'Home', 'Office', 'Gsm']) + ) + ); + + $expected = <<<CODE + <?php + + namespace MyNamespace; + + enum MyType: string { + case Empty = ''; + case Home = 'Home'; + case Office = 'Office'; + case Gsm = 'Gsm'; + } + + CODE; + + + $generator = new EnumerationGenerator(); + $generated = $generator->generate(new FileGenerator(), $type); + self::assertEquals($expected, $generated); + } + + public function testIntBackedEnumGeneration(): void + { + $type = new Type( + 'MyNamespace', + 'MyType', + 'MyType', + [], + XsdType::create('MyType') + ->withBaseType('integer') + ->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta + ->withIsSimple(true) + ->withEnums(['0', '1', '2']) + ) + ); + + $expected = <<<CODE + <?php + + namespace MyNamespace; + + enum MyType: int { + case Value_0 = 0; + case Value_1 = 1; + case Value_2 = 2; + } + + CODE; + + $generator = new EnumerationGenerator(); + $generated = $generator->generate(new FileGenerator(), $type); + self::assertEquals($expected, $generated); + } + + public function testBackedEnumDocblockGeneration(): void + { + $type = new Type( + 'MyNamespace', + 'MyType', + 'MyType', + [], + XsdType::create('MyType') + ->withBaseType('string') + ->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta + ->withIsSimple(true) + ->withEnums([]) + ->withDocs('Type specific docs') + ) + ); + + $expected = <<<CODE + <?php + + namespace MyNamespace; + + /** + * Type specific docs + */ + enum MyType: string { + } + + CODE; + + + $generator = new EnumerationGenerator(); + $generated = $generator->generate(new FileGenerator(), $type); + self::assertEquals($expected, $generated); + } +} diff --git a/test/PhproTest/SoapClient/Unit/CodeGenerator/TypeEnhancer/MetaTypeEnhancerTest.php b/test/PhproTest/SoapClient/Unit/CodeGenerator/TypeEnhancer/MetaTypeEnhancerTest.php index bbd13456..3b3dc9be 100644 --- a/test/PhproTest/SoapClient/Unit/CodeGenerator/TypeEnhancer/MetaTypeEnhancerTest.php +++ b/test/PhproTest/SoapClient/Unit/CodeGenerator/TypeEnhancer/MetaTypeEnhancerTest.php @@ -63,26 +63,32 @@ public static function provideExpectations() 'null | array<int<0,max>, simple>', '?array', ]; - yield 'enum' => [ - (new TypeMeta())->withEnums(['a', 'b']), + yield 'global-enum' => [ + (new TypeMeta())->withEnums(['a', 'b'])->withIsLocal(false), + 'string', + "string", + 'string', + ]; + yield 'local-enum' => [ + (new TypeMeta())->withEnums(['a', 'b'])->withIsLocal(true), 'string', "'a' | 'b'", 'string', ]; - yield 'enum-list' => [ - (new TypeMeta())->withEnums(['a', 'b'])->withIsList(true), + yield 'local-enum-list' => [ + (new TypeMeta())->withEnums(['a', 'b'])->withIsList(true)->withIsLocal(true), 'string', "array<int<0,max>, 'a' | 'b'>", 'array', ]; - yield 'nullable-enum-list' => [ - (new TypeMeta())->withEnums(['a', 'b'])->withIsList(true)->withIsNullable(true), + yield 'local-nullable-enum-list' => [ + (new TypeMeta())->withEnums(['a', 'b'])->withIsList(true)->withIsNullable(true)->withIsLocal(true), 'string', "null | array<int<0,max>, 'a' | 'b'>", '?array', ]; - yield 'nullable-enum' => [ - (new TypeMeta())->withEnums(['a', 'b'])->withIsNullable(true), + yield 'local-nullable-enum' => [ + (new TypeMeta())->withEnums(['a', 'b'])->withIsNullable(true)->withIsLocal(true), 'string', "null | 'a' | 'b'", '?string', diff --git a/test/PhproTest/SoapClient/Unit/Soap/Metadata/Detector/LocalEnumDetectorTest.php b/test/PhproTest/SoapClient/Unit/Soap/Metadata/Detector/LocalEnumDetectorTest.php new file mode 100644 index 00000000..52dd9313 --- /dev/null +++ b/test/PhproTest/SoapClient/Unit/Soap/Metadata/Detector/LocalEnumDetectorTest.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace PhproTest\SoapClient\Unit\Soap\Metadata\Detector; + +use Phpro\SoapClient\Soap\Metadata\Detector\LocalEnumDetector; +use PHPUnit\Framework\TestCase; +use Soap\Engine\Metadata\Collection\PropertyCollection; +use Soap\Engine\Metadata\Collection\TypeCollection; +use Soap\Engine\Metadata\Model\Property; +use Soap\Engine\Metadata\Model\Type; +use Soap\Engine\Metadata\Model\TypeMeta; +use Soap\Engine\Metadata\Model\XsdType; + +class LocalEnumDetectorTest extends TestCase +{ + /** @test */ + public function it_can_detect_local_enums(): void + { + $markAsEnum = static fn(XsdType $type, bool $local) => $type->withMeta( + static fn (TypeMeta $meta) => $meta->withIsSimple(true)->withEnums(['a'])->withIsLocal($local) + ); + + $detector = new LocalEnumDetector(); + $types = new TypeCollection( + new Type(XsdType::create('a'), new PropertyCollection()), + new Type(XsdType::create('b'), new PropertyCollection()), + new Type($markAsEnum(XsdType::create('global'), local: false), new PropertyCollection()), + new Type(XsdType::create('local1_wrapper'), new PropertyCollection( + new Property('global', $markAsEnum(XsdType::create('global'), local: false)), + new Property('local1', $local1 = $markAsEnum(XsdType::create('local1'), local: true)), + new Property('local2', $local2 = $markAsEnum(XsdType::create('local2'), local: true)), + )), + new Type($markAsEnum(XsdType::create('local2_wrapper'), local: true), new PropertyCollection( + new Property('local1', $local1_other = $markAsEnum(XsdType::create('local1'), local: true)), + new Property('local3', $local3 = $markAsEnum(XsdType::create('local3'), local: true)), + )), + + ); + + $detected = $detector($types); + + self::assertEquals( + new TypeCollection( + new Type($local1, new PropertyCollection()), + new Type($local2, new PropertyCollection()), + new Type($local1_other, new PropertyCollection()), + new Type($local3, new PropertyCollection()), + ), + $detected + ); + } +} diff --git a/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/AppendTypesManipulatorTest.php b/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/AppendTypesManipulatorTest.php new file mode 100644 index 00000000..02822aca --- /dev/null +++ b/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/AppendTypesManipulatorTest.php @@ -0,0 +1,44 @@ +<?php +declare(strict_types=1); + +namespace PhproTest\SoapClient\Unit\Soap\Metadata\Manipulators\TypeReplacer; + +use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\AppendTypesManipulator; +use PHPUnit\Framework\TestCase; +use Soap\Engine\Metadata\Collection\PropertyCollection; +use Soap\Engine\Metadata\Collection\TypeCollection; +use Soap\Engine\Metadata\Model\Property; +use Soap\Engine\Metadata\Model\Type; +use Soap\Engine\Metadata\Model\XsdType; + +class AppendTypesManipulatorTest extends TestCase +{ + /** @test */ + public function it_can_append_types(): void + { + $int = XsdType::create('int'); + $string = XsdType::create('string'); + + $types = new TypeCollection( + $type1 = new Type(XsdType::create('object1'), new PropertyCollection(new Property('property1', $int))), + $type2 = new Type(XsdType::create('object2'), new PropertyCollection(new Property('property1', $string))), + ); + + $append = new AppendTypesManipulator(static fn (TypeCollection $original) => new TypeCollection( + new Type( + $original->fetchFirstByName('object1')->getXsdType()->copy('object3'), + new PropertyCollection() + ), + )); + $actual = $append($types); + + self::assertEquals( + new TypeCollection( + $type1, + $type2, + new Type(XsdType::create('object3'), new PropertyCollection()), + ), + $actual + ); + } +} diff --git a/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/LocalToGlobalEnumReplacerTest.php b/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/LocalToGlobalEnumReplacerTest.php new file mode 100644 index 00000000..c022476d --- /dev/null +++ b/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/LocalToGlobalEnumReplacerTest.php @@ -0,0 +1,64 @@ +<?php +declare(strict_types=1); + +namespace PhproTest\SoapClient\Unit\Soap\Metadata\Manipulators\TypeReplacer; + +use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\LocalToGlobalEnumReplacer; +use PHPUnit\Framework\TestCase; +use Soap\Engine\Metadata\Model\TypeMeta; +use Soap\Engine\Metadata\Model\XsdType; +use Soap\WsdlReader\Model\Definitions\EncodingStyle; + +final class LocalToGlobalEnumReplacerTest extends TestCase +{ + /** + * @test + * @dataProvider provideTestCases + */ + public function it_can_replace_local_enums(XsdType $in, XsdType $expected): void + { + self::assertEquals($expected, (new LocalToGlobalEnumReplacer())($in)); + } + + public static function provideTestCases() + { + yield 'regular-type' => [ + $baseType = XsdType::create('object'), + $baseType + ]; + + yield 'implied-global-enum' => [ + $baseType = XsdType::create('object') + ->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta + ->withIsSimple(true) + ->withEnums(['a', 'b', 'c']) + ), + $baseType + ]; + + yield 'explicit-global-enum' => [ + $baseType = XsdType::create('object') + ->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta + ->withIsSimple(true) + ->withEnums(['a', 'b', 'c']) + ->withIsLocal(false) + ), + $baseType + ]; + + yield 'local-enum' => [ + $baseType = XsdType::create('object') + ->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta + ->withIsSimple(true) + ->withEnums(['a', 'b', 'c']) + ->withIsLocal(true) + ), + $baseType->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta->withIsLocal(false) + ) + ]; + } +} diff --git a/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/TypeReplacersTest.php b/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/TypeReplacersTest.php index 91436a48..df5c1d08 100644 --- a/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/TypeReplacersTest.php +++ b/test/PhproTest/SoapClient/Unit/Soap/Metadata/Manipulators/TypeReplacer/TypeReplacersTest.php @@ -37,4 +37,28 @@ public function __invoke(XsdType $type): XsdType self::assertSame('hello_replaced_again', $actual->getName()); } + + /** @test */ + public function it_can_iterate_over_internal_types(): void + { + $replace = TypeReplacers::empty() + ->add( + $replacer1 = new class() implements TypeReplacer { + public function __invoke(XsdType $type): XsdType + { + return $type; + } + } + ) + ->add( + $replacer2 = new class() implements TypeReplacer { + public function __invoke(XsdType $type): XsdType + { + return $type; + } + } + ); + + self::assertSame([$replacer1, $replacer2], [...$replace]); + } }