Create a SimpleType element

Hi!,

I’m migrating an API based on nusoap and I want to know, how I can create “SimpleType” elements with Laminas?

First, I create a soap server with nusoap:

require_once __DIR__ . "/vendor/autoload.php";
require_once __DIR__ . "/vendor/econea/nusoap/src/nusoap.php";

$namespace = "Soap.Service";
$server = new soap_server();
$server->configureWSDL("mysoap", $namespace);
$server->wsdl->schemaTargetNamespace = $namespace;

Then, I add the SimpleType’s. In this case, I need to create a SimpleType for only receive one of the array elements in a ComplexType:

$server->wsdl->addSimpleType(
    'tipoDocumento',
    'xsd:string',
    'SimpleType',
    'scalar',
    [
        'DNI',
        'LE',
        'LC',
        'CI',
        'PASSPORT'
    ]
);

Finally, I create the ComplexType’s where I need to use the SimpleType “tipoDocumento”:

$server->wsdl->addComplexType(
'persona',
'complexType',
'struct',
'sequence',
'',
array(
        'tipoDocumento' => array('name' => 'tipoDocumento', 'type' => 'xsd:tipoDocumento'),
        'nombre' => array('name' => 'nombre', 'type' => 'xsd:string', 'use' => 'required'),
        'email' => array('name' => 'email', 'type' => 'xsd:string', 'minOccurs' => '0'),
    )
);

This is the xml that I wanted:

<xsd:simpleType name="tipoDocumento">
    <xsd:restriction base="xsd:string">
        <xsd:enumeration value="DNI"/>
        <xsd:enumeration value="LE"/>
        <xsd:enumeration value="LC"/>
        <xsd:enumeration value="CI"/>
        <xsd:enumeration value="PASSPORT"/>
        </xsd:restriction>
</xsd:simpleType>

<xsd:complexType name="persona">
    <xsd:all>
        <xsd:element name="tipoDocumento" type="xsd:tipoDocumento"/>
        <xsd:element name="nombre" type="xsd:string" use="required"/>
        <xsd:element name="email" type="xsd:string" minOccurs="0" maxOccurs="2"/>
    </xsd:all>
</xsd:complexType>

Thanks!

Hi. Any update on this?
I see no responses. Did you manage on your own to achieve it?
I’m in the need of the same (and also combined with union types) and I see I’d have to rewrite several parts of Wsdl/AutoDiscover/ComplexTypeStrategy and I’d like to avoid embarking myself on that approach, at least initially.

Good morning @fernando2amigos ,

let me have a try. The last time I used NuSoap I used to write my code with PHP 4. If you really want to migrate from NuSoap to laminas/laminas-soap, you can say goodbye to most of the concepts that NuSoap offered you. From my point of view, it will be much easier for you in the future.

Let us begin with the complex type. Complex types are just PHP data objects you will use with your soap client. Before sending a request you 'll initialize them with the values you want.

<?php

declare(strict_types=1);

namespace Marcel\ComplexType;

readonly class Persona
{
    public function __construct(
        public string $tipoDocumento,
        public string $nombre,
        public array $email = [],
    ) {}
}

That 's all what we need for the complex type. As you can see the property $tipoDocumento is type hinted as string. That is not invalid, because your simple type restricts from a string base. So if you want to go the easy way this is all what you have to do. The PHP soap implementation will validate the given value for the $tipoDocumento property as seen as you send a request.

If you want it more precise we can define the simple type tipoDocumento as an enumeration. Since PHP 8 you can use the BackedEnum interface for this type of simple element.

<?php

declare(strict_types=1);

namespace Marcel\SimpleType;

enum TipoDocumento: string
{
    case DNI = 'DNI';
    case LE = 'LE';
    case LC = 'LC';
    case CI = 'CI';
    case PASSPORT = 'PASSPORT';
}

This would change the type hint in your complex type PHP data object as follows:

<?php

declare(strict_types=1);

namespace Marcel\ComplexType;

use Marcel\SimpleType\TipoDocumento;

readonly class Persona
{
    public function __construct(
        public TipoDocumento $tipoDocumento,
        public string $nombre,
        public array $email = [],
    ) {}
}

The PHP SoapClient class, which is used by laminas/laminas-soap knows nothing about backed enums. Requests and responses will definitely fail and potentially end up in a fault. So we have to teach the soap client how to handle enums. The PHP SoapClient has an classmap option. It is recommended to use this option when mapping complex types as PHP data objects.

<?php

declare(strict_types=1);

namespace Marcel;

use DOMDocument;
use SoapClient;
use SoapFault;

try {
    $doc = new DOMDocument();
    $wsdl = __DIR__ . '/path/to/your/webservice.wsdl';
    $client = new SoapClient($wsdl, [
        'classmap' => [
            'persona' => \Marcel\ComplexType\Persona::class,
        ],
        'exceptions' => true,
        'trace' => true,
        'typemap' => [
            [
                'type_ns' => 'http://www.namespace.uri',
                'type_name' => 'tipoDocumento',
                'from_xml' => function(string $xml) use ($doc): TipoDocumento {
                    $doc->loadXML($xml);
                    return TipoDocumento::tryFrom($doc->documentElement->nodeValue);
                }
                'to_xml' => function(TipoDocumento $enum) use ($doc): string {
                    $element = $doc->createElement('tipoDocumento', $enum->value);
                    return $doc->saveElement($element);
                }
            ],
        ],
    ]);
} catch (SoapFault $fault) {
    // error handling
}

Since we know the name of your simple type, we use the typemap option. Yeah, it looks messy and it is for sure not that type of encapsulation we usually want. But it will decode an xml string into a backed enum instance and vice versa.

If you don 't want all that mess in your client options there is a really useful hint in the PHP documentation for the classmap option.

Note that when creating a class, the constructor will not be called, but magic __set() and __get() methods for individual properties will be.

This is important, when you don 't want to use the typemap option. Just add a magic method in your complex type PHP data object.

<?php

declare(strict_types=1);

namespace Marcel\ComplexType;

use Marcel\SimpleType\TypoDocumento;

readonly class Persona
{
    public function __construct(
        public TypoDocumento $tipoDocumento,
        public string $nombre,
        public array $email = [],
    ) {}

    public function __get(string $name): mixed
    {
        if ($name === 'tipoDocumento') {
            return $this->tipoDocumento->value;
        }

        return $this->{$name};
    }

    public function __set($name, $value): void {
        if ($name === 'tipoDocumento') {
            $value = TipoDocument::tryFrom($value);
        }

        $this->{$name} = $value;
    }
}

As you can see, there are several ways to display simple types defined as enumerations for the PHP SoapClient. All of them are completely different from what you 're used to use with NuSoap.

2 Likes

Whoa, that’s a lot of info. Thanks so much Marcel @ezkimo!

I’m not coming from NuSoap at all. I’m just starting a project, that will run in SlimPHP (8.3) and needs to be compliant with this WSDL: https://nemsis.org/media/nemsis_v3/master/WSDL/NEMSIS_V3_core.wsdl

As you’ll see, there are some simpleTypes that laminas doesn’t handle at all, nor unions.
Also, several strings has length limitations, annotations, and I couldn’t find yet a proper docblock format to be able to render them like that.
In any case, I’ll still need to have validations inside the code.

That means that I’d have to do a lot of monkeyjob through the typemap definition in order for Laminas AutoDiscover to achieve the expected WSDL, which actually defeats its initial value.
I’ll need to weight if it’s worth the effort vs. just having the hardcoded WSDL and have a robust test suite to validate the logic in the server matches the WSDL.

Oh well … this one looks pretty complex. I 've never seen unions so far in a webservice definition.

In any case, I’ll still need to have validations inside the code.

It really depends on how detailed your code should look like. Actually the soap client does the validation based on the xsd / wsdl. It would throw a fault, when values occur, that do not fit into the defined range or somewhat else. The XSD simple types are based on scalars. Absolutely no need to write its equivalents in PHP. That 's the reason why the autodiscover doesn 't recognize simple types. Basically they are only scalars.

If you really want to validate the values before sending them via the soap client, I 'd suggest laminas/laminas-validator and then initializing the PHP data objects with the validated data.

1 Like

Thanks for the follow up.
Sorry because I wasn’t clear: I’m writing a server (actually a client as well, but that’s another story), that needs to be same as NEMSIS one, so current clients of NEMSIS can start sharing/consuming data to/from mine. That’s how it works.
So I’m concerned about validation anytime my server receives data.

I’m somewhat new to SOAP so excuse me if my question is naive, but I don’t see how the server would validate the data before creating objects upon request.
Even though the WSDL is pretty strict.
If you could give me some guidance on that matter based on your experience, I’d appreciate it.