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]);
+    }
 }