diff --git a/composer.json b/composer.json index 5465e394..3004669b 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "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", "psr/event-dispatcher": "^1.0", diff --git a/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php b/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php index 5db5e298..ab993e50 100644 --- a/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php +++ b/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php @@ -60,6 +60,24 @@ function it_noramizes_properties() $this->normalizeProperty('My-./final*prop_123')->shouldReturn('MyFinalProp_123'); } + function it_normalizes_enum_cases() + { + $this->normalizeEnumCaseName('')->shouldReturn('Empty'); + + $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/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 */ class ClassMapGenerator implements GeneratorInterface { diff --git a/src/Phpro/SoapClient/CodeGenerator/ClientFactoryGenerator.php b/src/Phpro/SoapClient/CodeGenerator/ClientFactoryGenerator.php index 913ef067..69fbca96 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 */ class ClientFactoryGenerator implements GeneratorInterface { 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 */ class ClientGenerator implements GeneratorInterface { 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 */ 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 @@ + + */ +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 + */ + 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/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 */ class TypeGenerator implements GeneratorInterface { diff --git a/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php b/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php index be483b8e..ad2c55d4 100644 --- a/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php +++ b/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php @@ -209,6 +209,32 @@ 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 { + if ($value === '') { + return $emptyCase; + } + + if (is_numeric($value)) { + return $conflictPrefix.$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|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('Skipped scalar type : '.$type->getFullName().''); } @@ -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 $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/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 @@ +withBaseType('string') + ->withMeta( + static fn (TypeMeta $meta): TypeMeta => $meta + ->withIsSimple(true) + ->withEnums(['', 'Home', 'Office', 'Gsm']) + ) + ); + + $expected = <<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 = <<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 = <<generate(new FileGenerator(), $type); + self::assertEquals($expected, $generated); + } +}