Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

About sequence generation again #414

Closed
danaki opened this issue Jan 18, 2022 · 17 comments
Closed

About sequence generation again #414

danaki opened this issue Jan 18, 2022 · 17 comments

Comments

@danaki
Copy link

danaki commented Jan 18, 2022

Hello,

I have a service description like so:

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/QueryCriteria" elementFormDefault="qualified" targetNamespace="http://schemas.microsoft.com/dynamics/2006/02/documents/QueryCriteria">
    <xsd:element name="QueryCriteria" type="QueryCriteria" />
    <xsd:complexType name="QueryCriteria">
        <xsd:sequence minOccurs="1" maxOccurs="unbounded">
            <xsd:element name="CriteriaElement" type="CriteriaElement" />
        </xsd:sequence>
    </xsd:complexType>
    <xsd:complexType name="CriteriaElement">
        <xsd:sequence>
            <xsd:element name="DataSourceName" type="xsd:string" />
            <xsd:element name="FieldName" type="xsd:string" />
            <xsd:element name="Operator" type="Operator" />
            <xsd:element name="Value1" type="xsd:string" />
            <xsd:element minOccurs="0" name="Value2" type="xsd:string" />
        </xsd:sequence>
    </xsd:complexType>
    <xsd:simpleType name="Operator">
        <xsd:restriction base="xsd:string">
            <xsd:enumeration value="Equal" />
            <xsd:enumeration value="NotEqual" />
            <xsd:enumeration value="Greater" />
            <xsd:enumeration value="GreaterOrEqual" />
            <xsd:enumeration value="Less" />
            <xsd:enumeration value="LessOrEqual" />
            <xsd:enumeration value="Range" />
        </xsd:restriction>
    </xsd:simpleType>
</xsd:schema>

Using default generator I get following classes:

<?php

namespace App\Soap\Type;

class QueryCriteria
{
    /**
     * @var \App\Soap\Type\CriteriaElement
     */
    private $CriteriaElement;

    /**
     * @return \App\Soap\Type\CriteriaElement
     */
    public function getCriteriaElement()
    {
        return $this->CriteriaElement;
    }

    /**
     * @param \App\Soap\Type\CriteriaElement $CriteriaElement
     * @return QueryCriteria
     */
    public function withCriteriaElement($CriteriaElement)
    {
        $new = clone $this;
        $new->CriteriaElement = $CriteriaElement;

        return $new;
    }
}
<?php

namespace App\Soap\Type;

class CriteriaElement
{
    /**
     * @var string
     */
    private $DataSourceName;

    /**
     * @var string
     */
    private $FieldName;

    /**
     * @var string
     */
    private $Operator;

    /**
     * @var string
     */
    private $Value1;

    /**
     * @var string
     */
    private $Value2;

    /**
     * @return string
     */
    public function getDataSourceName()
    {
        return $this->DataSourceName;
    }

    /**
     * @param string $DataSourceName
     * @return CriteriaElement
     */
    public function withDataSourceName($DataSourceName)
    {
        $new = clone $this;
        $new->DataSourceName = $DataSourceName;

        return $new;
    }

    /**
     * @return string
     */
    public function getFieldName()
    {
        return $this->FieldName;
    }

    /**
     * @param string $FieldName
     * @return CriteriaElement
     */
    public function withFieldName($FieldName)
    {
        $new = clone $this;
        $new->FieldName = $FieldName;

        return $new;
    }

    /**
     * @return string
     */
    public function getOperator()
    {
        return $this->Operator;
    }

    /**
     * @param string $Operator
     * @return CriteriaElement
     */
    public function withOperator($Operator)
    {
        $new = clone $this;
        $new->Operator = $Operator;

        return $new;
    }

    /**
     * @return string
     */
    public function getValue1()
    {
        return $this->Value1;
    }

    /**
     * @param string $Value1
     * @return CriteriaElement
     */
    public function withValue1($Value1)
    {
        $new = clone $this;
        $new->Value1 = $Value1;

        return $new;
    }

    /**
     * @return string
     */
    public function getValue2()
    {
        return $this->Value2;
    }

    /**
     * @param string $Value2
     * @return CriteriaElement
     */
    public function withValue2($Value2)
    {
        $new = clone $this;
        $new->Value2 = $Value2;

        return $new;
    }
}

QueryCriteria class accepts only single CriteriaElement in the provided code. How do I make it accept a list?

P.S. If I just wrap a list of CriteriaElement[] inside QueryCriteria it results to an error "SOAP-ERROR: Encoding: object has no 'DataSourceName' property".

Thank you

@veewee
Copy link
Contributor

veewee commented Jan 18, 2022

Hello,

The way to go here would be to provide an array of said type through the with* function - even though the doc locks state to only accept the class instead of a list.

You need to use the iterator assembler as well
https://github.com/phpro/soap-client/blob/master/docs/code-generation/assemblers.md#iteratorassembler

@danaki
Copy link
Author

danaki commented Jan 18, 2022

Yes, I've tried to provide an array like this:

$criterias = [
    (new CriteriaElement())
                    ->withDataSourceName("PurchLotRegister")
                    ->withFieldName($key)
                    ->withOperator("Equal")
                    ->withValue1($value)
];

$criteria = new QueryCriteria();
$criteria = $criteria->withCriteriaElement($criterias);

and getting the above error. I also tried to generate QueryCriteria using IteratorAssembler without luck (no effect).

@veewee
Copy link
Contributor

veewee commented Jan 19, 2022

The error you see is an internal encoding error from phps SoapClient. According to the part of the wsdl you shared, it should work this way. However, php has a bug that doesn't allow this:

https://bugs.php.net/bug.php?id=79210

A possible fix might be to add the max occurs to the element instead of the sequence?

@danaki
Copy link
Author

danaki commented Jan 19, 2022

A possible fix might be to add the max occurs to the element instead of the sequence?

Do I have to do that on the server side? Because after inspecting exp-soap-engine I can see it calls \SoapClient with WSDL URL that loads it and I see no way to put my code between to patch XML contents (at least at types generation stage).

@veewee
Copy link
Contributor

veewee commented Jan 19, 2022

There are some ways to do so which all have their pros and cons.

  • Having it changed on the server might be the best approach, but also the one that is most likely out of your control.
  • You could downoad the wsdl locally and make the changes manually. You can use the local wsdl instead of the remote one. However, updates on the service won't come through. So this one is error-prone. You could use this technique to validate if this fixes the issue for you.
  • You could use HTTP middleware to manipulate your WSDL before it is loaded by SOAP. This one is a bit harder to write, since you need to parse the XML and change that specific property. Also : it currently does not work well with includes / imports (YET)

This needs to be done during runtime stage and won't have much affect on generation stage.
This is because ext-soap encodes the request when asked for.

@danaki
Copy link
Author

danaki commented Jan 19, 2022

I'll try to go with a local proxy since that part of xml I showed you is an include from a bigger service description and I don't want to sync it locally everytime it changes.

@veewee
Copy link
Contributor

veewee commented Jan 19, 2022

That would also be a good solution.

I am finishing up a flattening WSDL loader. Once it is done, that would probably be the best solution:

php-soap/wsdl#1

It would allow you to decorate loaders so that you could apply changes to a specific file and still end up with the fully downoaded WSDL.

That version can then be cached on the filesystem or reloaded by applying the same loaders by using the WSDL providers that already exist.

@danaki
Copy link
Author

danaki commented Jan 24, 2022

I have a crazy idea, please correct me if I'm wrong. I assume that error "SOAP-ERROR: Encoding: object has no 'DataSourceName' property" is thrown by the php-soap before even reaching the server and it complains that I'm passing an array of CriteriaElement[]s while given wrongly parsed WSDL it accepts just one CriteriaElement.

I've done some experiments and it seems that standard \SoapClient() accepts null as the the first $wsdl argument and actual service URL to make requests can be passed in $options (uri and location keys). Is it possible that passing null as $wsdl parameter disables parsing service configuration (while having generated class hierarchy) and it just skips checks and just serializes objects and passes it to the service? Can't check it right now because ExtSoapOptions doesnt allow null for $wsdl. Does it anyway work this way?

@veewee
Copy link
Contributor

veewee commented Jan 24, 2022

Not sure how it behaves in that case.
It might be quirky, since it would not know if it needs to map specific tags to arrays of elements, regular elements or maybe some other complex type.

You could validate this by using the AbusedClient constructor directly?
That way you can pass null as wsdl and the options you want.

@danaki
Copy link
Author

danaki commented Jan 25, 2022

So this is how it went:

        $transport =  Psr18Transport::createForClient(
            new PluginClient(
                Psr18ClientDiscovery::find(),
                [new AuthenticationPlugin(new BasicAuth($user, $pass))]
            )
        );

        $metadataOptions ??= MetadataOptions::empty()->withTypesManipulator(
            new IntersectDuplicateTypesStrategy()
        );

        $client = new AbusedClient(null, [
            'uri' => 'http://tempuri.org/PurchLotRegister01Service', # from wireshark
            'location' => $wsdl,
            'trace' => true,
            'exceptions' => true,
            'keep_alive' => true,
            'cache_wsdl' => WSDL_CACHE_DISK,
            'features' => SOAP_SINGLE_ELEMENT_ARRAYS,
        ]);

        $driver = ExtSoapDriver::createFromClient(
            $client,
            MetadataFactory::manipulated(
                new ExtSoapMetadata($client),
                $metadataOptions
            )
        );

        $engine = new SimpleEngine($driver, $transport);

        $eventDispatcher = new EventDispatcher();
        $caller = new EventDispatchingCaller(new EngineCaller($engine), $eventDispatcher);

        return new PurchLotRegisterClient($caller);

and then I had to patch ExtSoapEncoder because SOAPAction header produced was in form:
http://tempuri.org/PurchLotRegister01Service#find while according to Wireshark it should be http://tempuri.org/PurchLotRegister01Service/find:

$this->client->__soapCall($method, $arguments, ['soapaction' => 'http://tempuri.org/PurchLotRegister01Service/find']);

So finally my non-wsdl call succeeded but caused an error on the server like:

OperationFormatter encountered an invalid Message body. Expected to find node type 'Element' with name 'PurchLotRegister01ServiceFindRequest' and namespace 'http://tempuri.org'. Found node type 'Element' with name 'ns1:find' and namespace 'http://tempuri.org/PurchLotRegister01Service'

@veewee
Copy link
Contributor

veewee commented Jan 25, 2022

Yeah makes sense ... it doesnt know what namespace to bind it to etc. since you didn't provide a WSDL.

@danaki
Copy link
Author

danaki commented Jan 25, 2022

What if I download all the WSDL, manually patch them like this:

        <xsd:sequence minOccurs="1" maxOccurs="unbounded">
            <xsd:element minOccurs="1" maxOccurs="unbounded" name="CriteriaElement" type="CriteriaElement" />
        </xsd:sequence>

and try to regenerate classes and provide this patched wsdl files later to SoapClient?

The problem is that PermanentWsdlLoaderProvider somehow caches only the root wsdl file which contains links to other wsdl's (where one of them I want to alter)? Do I have to download them manually too and update the links to point to local files? Will it work? Or do I need to somehow merge them into on file like yours flattening WSDL loader does? Is it worth trying this loader?

@veewee
Copy link
Contributor

veewee commented Jan 25, 2022

Regenerating classes wouldn't make a big difference, it is inside the encoding section (during runtime).
I think this would be the best solution yes.

The PermanentWsdlLoaderProvider accepts a wsdl loader.
Current available loaders indeed only work on the main entry point.
Therefore I am building the flattening loader.

You can always play around with it, but I noticed it still requires schema grouping - otherwise the file can becoma way too big. Meaning it needs some rewriting before it is in a usable state.

In the meantime, you might want to use https://github.com/pkielgithub/SchemaLightener to download the flattened WSDL locally and next change that file?

@danaki
Copy link
Author

danaki commented Jan 25, 2022

I can see no option to make SchemaLightener download wsdl's, "Select Wsdl to flatten" opens a file selection dialog.

@veewee
Copy link
Contributor

veewee commented Jan 28, 2022

Hello @danaki,

Haven't used that tool myself - sorry :)
I finished the flattening loader. Feel free to test it out!
Today, I am also going to create a cli tool inside the wsdl package for stuff like this. Stay tuned for more info! :)

@danaki
Copy link
Author

danaki commented Jan 28, 2022

Meanwhile I made this proxy:

from requests import Session
from requests.auth import HTTPBasicAuth
import zeep
from zeep.transports import Transport
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel


class Request(BaseModel):
    wsdl: str
    username: str
    password: str
    method: str
    params: dict


app = FastAPI()

@app.post("/")
def root(r: Request):
    session = Session()
    session.auth = HTTPBasicAuth(r.username, r.password)

    client = zeep.Client(r.wsdl, transport=Transport(session=session))

    return client.service[r.method](r.params)

Which accepts json and produces json and that fully solves my problem.

I'll test the cli once you finish it.

@veewee
Copy link
Contributor

veewee commented Jan 28, 2022

@danaki Looks nice! Since the bug will still remain in PHP - using python to bypass the issue is most likely the smoothest solution ;p

The CLI flatting tool is ready:
php-soap/wsdl#4

You can provide a custom loader so that you can transform the WSDL on the fly during flattening.
Feel free to test it out!

@veewee veewee closed this as completed Jan 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants