From dfa7fdee527aa58650822864d2fbe71e1051c20d Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Fri, 27 Sep 2024 13:49:05 +0200 Subject: [PATCH] Use external classes as property types. --- docs/drivers/metadata.md | 31 +++++++++++++++- .../CodeGenerator/Util/NormalizerSpec.php | 17 +++++++++ .../CodeGenerator/Model/Property.php | 21 +++++++++-- .../CodeGenerator/Util/Normalizer.php | 16 ++++++++ .../Unit/CodeGenerator/Model/PropertyTest.php | 37 ++++++++++++++++++- 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/docs/drivers/metadata.md b/docs/drivers/metadata.md index 68961d3d..5d8259bf 100644 --- a/docs/drivers/metadata.md +++ b/docs/drivers/metadata.md @@ -49,7 +49,7 @@ return Config::create() ### Type replacements -Depending on what XML encoders you configure, you might want to replace some types with other types. +Depending on what [SOAP encoders](https://github.com/php-soap/encoding#encoder) you configure, you might want to replace some types with other types. Take following example: By default, a "date" type from the XSD namespace `http://www.w3.org/2001/XMLSchema` will be converted to a `DateTimeImmutable` object. @@ -99,3 +99,32 @@ The TypeReplacers contain a default set of type replacements that are being used * `array` for SOAP 1.1 and 1.2 Arrays * `object` for SOAP 1.1 Objects * `array` for Apache Map types + + +#### Using an existing 3rd party class + +If you want to replace a type with an existing 3rd party class, you can copy the type with the fully qualified class name of the 3rd party class. +That way, the property type of the generated code will be set to the newly linked class. +This type replacer is only being used for generating code. +You will still need to implement the logic to convert the type to the 3rd party class in a [custom SOAP encoder](https://github.com/php-soap/encoding#encoder). + +```php +use Phpro\SoapClient\Soap\Metadata\Manipulators\TypeReplacer\TypeReplacer; +use Soap\Engine\Metadata\Model\XsdType; +use Soap\WsdlReader\Metadata\Predicate\IsOfType; + +class GuidTypeReplacer implements TypeReplacer +{ + public function __invoke(XsdType $xsdType): XsdType + { + $check = new IsOfType('http://microsoft.com/wsdl/types/', 'guid'); + if (!$check($xsdType)) { + return $xsdType; + } + + return $xsdType->copy(\Ramsey\Uuid\UuidInterface::class); + } +} +``` + +**Note**: If you want to use a built-in PHP class, you will need to prefix it with a backslash. For example: `\DateInterval`. diff --git a/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php b/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php index 666e1822..5db5e298 100644 --- a/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php +++ b/spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php @@ -4,6 +4,7 @@ use Phpro\SoapClient\CodeGenerator\Util\Normalizer; use PhpSpec\ObjectBehavior; +use Psl\Option\Option; /** * Class NormalizerSpec @@ -98,6 +99,13 @@ function it_gets_classname_from_fqn() $this->getClassNameFromFQN('Vendor\Namespace\MyClass')->shouldReturn('MyClass'); } + function it_gets_namespace_from_fqn() + { + $this->getNamespaceFromFQN('\Namespace\MyClass')->shouldReturn('Namespace'); + $this->getNamespaceFromFQN('Vendor\Namespace\MyClass')->shouldReturn('Vendor\Namespace'); + $this->getNamespaceFromFQN('ExistingPHPClass')->shouldReturn(''); + } + function it_gets_complete_use_statement() { $this->getCompleteUseStatement('Namespace\MyClass', @@ -105,4 +113,13 @@ function it_gets_complete_use_statement() $this->getCompleteUseStatement('Namespace\MyClass', null)->shouldReturn('Namespace\MyClass'); $this->getCompleteUseStatement('MyClass', '')->shouldReturn('MyClass'); } + + /** @test */ + public function it_knows_about_third_party_classes(): void + { + $this->isConsideredExistingThirdPartyClass('DateTime')->shouldReturn(false); + $this->isConsideredExistingThirdPartyClass('\DateTime')->shouldReturn(true); + $this->isConsideredExistingThirdPartyClass(Option::class)->shouldReturn(true); + $this->isConsideredExistingThirdPartyClass('Unkown\Class')->shouldReturn(false); + } } diff --git a/src/Phpro/SoapClient/CodeGenerator/Model/Property.php b/src/Phpro/SoapClient/CodeGenerator/Model/Property.php index 4a789426..6abcfe9c 100644 --- a/src/Phpro/SoapClient/CodeGenerator/Model/Property.php +++ b/src/Phpro/SoapClient/CodeGenerator/Model/Property.php @@ -30,7 +30,7 @@ class Property private $type; /** - * @var non-empty-string + * @var string */ private $namespace; @@ -45,7 +45,7 @@ class Property * * @param non-empty-string $name * @param non-empty-string $type - * @param non-empty-string $namespace + * @param string $namespace */ public function __construct(string $name, string $type, string $namespace, XsdType $xsdType) { @@ -63,12 +63,21 @@ public function __construct(string $name, string $type, string $namespace, XsdTy public static function fromMetaData(string $namespace, MetadataProperty $property) { $type = $property->getType(); + $typeName = $type->getName(); + + // This makes it possible to set FQCN as type names in the metadata through TypeReplacers. + if (Normalizer::isConsideredExistingThirdPartyClass($typeName)) { + $className = Normalizer::getClassNameFromFQN($typeName); + $type = $type->copy($className)->withBaseType($className); + $namespace = Normalizer::getNamespaceFromFQN($typeName); + } + $meta = $type->getMeta(); - $typeName = (new TypeNameCalculator())($type); + $calculatedTypeName = (new TypeNameCalculator())($type); return new self( non_empty_string()->assert($property->getName()), - non_empty_string()->assert($typeName), + non_empty_string()->assert($calculatedTypeName), $namespace, $type ); @@ -91,6 +100,10 @@ public function getType(): string return $this->type; } + if (!$this->namespace) { + return '\\'.Normalizer::normalizeClassname($this->type); + } + return '\\'.$this->namespace.'\\'.Normalizer::normalizeClassname($this->type); } diff --git a/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php b/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php index ef2e90f7..dcf7b831 100644 --- a/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php +++ b/src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php @@ -251,6 +251,17 @@ public static function getClassNameFromFQN(string $name): string return non_empty_string()->assert(array_pop($arr)); } + /** + * @param non-empty-string $name + */ + public static function getNamespaceFromFQN($name): string + { + $arr = explode('\\', ltrim($name, '\\')); + array_pop($arr); + + return implode('\\', $arr); + } + /** * @param non-empty-string $useName * @param non-empty-string|null $useAlias @@ -266,4 +277,9 @@ public static function getCompleteUseStatement(string $useName, string $useAlias return $use; } + + public static function isConsideredExistingThirdPartyClass(string $class) + { + return str_contains($class, '\\') && class_exists($class); + } } diff --git a/test/PhproTest/SoapClient/Unit/CodeGenerator/Model/PropertyTest.php b/test/PhproTest/SoapClient/Unit/CodeGenerator/Model/PropertyTest.php index 8259edb4..d2c867ab 100644 --- a/test/PhproTest/SoapClient/Unit/CodeGenerator/Model/PropertyTest.php +++ b/test/PhproTest/SoapClient/Unit/CodeGenerator/Model/PropertyTest.php @@ -4,7 +4,8 @@ use Phpro\SoapClient\CodeGenerator\Model\Property; use PHPUnit\Framework\TestCase; -use Soap\Engine\Metadata\Model\TypeMeta; +use Psl\Option\Option; +use Soap\Engine\Metadata\Model\Property as EngineProperty; use Soap\Engine\Metadata\Model\XsdType; /** @@ -23,4 +24,38 @@ public function it_returns_mixed_type_post_php8(): void self::assertEquals('mixed', $property->getPhpType()); self::assertEquals('mixed', $property->getType()); } + + /** @test */ + public function it_can_use_fqcn_to_3rd_party_classes_as_type_name(): void + { + $property = Property::fromMetaData( + 'MyApp', + new EngineProperty( + 'property', + $xsdType = XsdType::create(Option::class) + ) + ); + + self::assertEquals('\\' . Option::class, $property->getType()); + self::assertNotSame($xsdType, $property->getXsdType()); + self::assertSame('Option', $property->getXsdType()->getName()); + self::assertSame('Option', $property->getXsdType()->getBaseType()); + } + + /** @test */ + public function it_can_use_a_php_built_in_class_as_type_name(): void + { + $property = Property::fromMetaData( + 'MyApp', + new EngineProperty( + 'property', + $xsdType = XsdType::create('\\'. \DateInterval::class) + ) + ); + + self::assertEquals('\\' . \DateInterval::class, $property->getType()); + self::assertNotSame($xsdType, $property->getXsdType()); + self::assertSame('DateInterval', $property->getXsdType()->getName()); + self::assertSame('DateInterval', $property->getXsdType()->getBaseType()); + } }