PHP5でWSDL自動生成、Document/Literal・RPC/Encoded 両対応のSOAPサーバを実装する

#概要
Java と PHP5 間で互いに SOAP で連携する Web システムを開発・運用していましたが、先日システムの更新をする際に、SOAP 周りで苦労した事がありましたのでメモを残しておきます。
PHP5 の SOAP extension が SOAP サーバを書くことはできますが、WSDL を自前で用意する必要があります。WSDL の自動生成をうたう外部ライブラリはいくつか出ていますが、2種類の SOAP ([RPC/Encoded と Document/Literal](http://itpro.nikkeibp.co.jp/members/NBY/techsquare/20050713/164546/)) に対応したものは、結局のところ Zend Framework2 の Zend\Soap しかなさそう、という結論に至りました。そこでその実装方法を解説します。
また、担当しているシステムの PHP 側は Zend Framework2 を使っていない独自のシステムだったため、ここから Zend\Soap を外部ライブラリとして利用する方法も合わせて解説します。
なお、PHP のバージョンは 5.6、Java のバージョンは JDK8 を使用しました。

#Zend Framework2 (zend-soapのみ) の取得
Zend Framwork2 のうち必要なのは Zend\Soap だけなので、composer を使って zend-soap と依存するモジュールだけを取得します。

```shell-session
% curl -sS https://getcomposer.org/installer | php
% php composer.phar require zendframework/zend-soap
```
これで vendor/zendframework 以下にダウンロードされます。

#init_autoloader.php の作成
Zend Framework の外側から Zend\Soap を呼び出せるように、init_autoloader.php を作成します。見ての通り [ZendSkeletonApplication](https://github.com/zendframework/ZendSkeletonApplication/) の [init_autoloader.php](https://github.com/zendframework/ZendSkeletonApplication/blob/master/init_autoloader.php) からのパクリですが、zend-soap モジュールだけをインストールした状態では Zend\Loader\AutoloaderFactory が存在しないためクラスの存在チェックの行を修正する必要があります。

```php:init_autoloader.php
<?php
if (file_exists('vendor/autoload.php')) {
    $loader = include 'vendor/autoload.php';
}

if (class_exists('Zend\Soap\Server')) {
    return;
} else {
    throw new RuntimeException('Unable to load ZF2. Run `php composer.phar install` or define a ZF2_PATH environment variable.');
}
```

#SOAPサービスを書く
公開する Web サービスを記述します。ここでは簡単に足し算をするサービス TestSoap を作成します。クラスとしてはインターフェースクラス TestSoapInterface、その実装クラスとして TestSoap を定義します。この程度ならインターフェースと実装に分離する必要もないのですが、規模が大きくなってくると外部に公開しないユーティリティー的なメソッドまで WSDL に書き出されてしまいますので、分けておくべきだと思います。
また、WSDL の作成に必要な引数や返り値の型の情報は、JavaDoc 形式で記述しておきます。

```php:TestSoapInterface.php
<?php
class TestSoapInterface
{
    /**
     * Returns added value.
     * @param string $val1
     * @param string $val2
     * @return string
     */
    public function add($a, $b)
    {
        // Nothing to be implemented.
    }
}
```

```php:TestSoap.php
<?php
require("TestSoapInterface.php");

class TestSoap extends TestSoapInterface
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}
```

#RPC/Encoded・Document/Literal 両対応の SOAP サーバクラスを書く
上で作成した Web サービスを公開する SOAP サーバを書いていきますが、その大部分は RPC/Encoded の場合と Document/Literal の場合とで共通となるため、コンストラクタにフラグを与えて動作を切り替えられるような共通クラスを実装します。

```php:ZendSoapServer.php
<?php
class ZendSoapServer
{
    protected $uri;
    protected $nameService;
    protected $nameClassSoap;
    protected $objSoap;
    protected $isSoapDocumentLiteral = false;

    public function __construct($uri, $nameService, $nameClassSoap, $objSoap)
    {
        $this->uri = $uri;
        $this->nameService = $nameService;
        $this->nameClassSoap = $nameClassSoap;
        $this->objSoap = $objSoap;
    }

    public function setIsSoapDocumentLiteral($isSoapDocumentLiteral)
    {
        $this->isSoapDocumentLiteral = $isSoapDocumentLiteral;
    }

    public function getIsSoapDocumentLiteral()
    {
        return $this->isSoapDocumentLiteral;
    }

    public function handle()
    {
        header("Content-Type: application/xml");
        if (array_key_exists("wsdl", $_REQUEST)) {
            $discover = new Zend\Soap\AutoDiscover();
            if ($this->isSoapDocumentLiteral) {
                $discover->setBindingStyle(array("style" => "document"));
                $discover->setOperationBodyStyle(array("use" => "literal"));
            }
            $discover->setClass($this->nameClassSoap);
            $discover->setUri($this->uri);
            $discover->setServiceName($this->nameService);

            echo $discover->toXml();
        } else {
            $serviceSoap = new Zend\Soap\Server($this->uri . "?wsdl");
            $serviceSoap->setDebugMode(true);
            $obj = $this->objSoap;
            if ($this->isSoapDocumentLiteral) {
                $obj = new Zend\Soap\Server\DocumentLiteralWrapper($obj);
            }
            $serviceSoap->setObject($obj);
            $serviceSoap->handle();
        }
    }
}
```

#RPC/Encoded用SOAPサーバ
上で書いた共通SOAPサーバクラスを使い、RPC/Encoded 版の SOAP サーバを書きます。

```php:testZendSoapServerRpcEncoded.php
<?php
require("init_autoloader.php");
require_once("ZendSoapServer.php");
require_once("TestSoap.php");

$uri = "http://localhost/soaptestzf2-autoloader/" . basename(__FILE__);

$serverSoapZend = new ZendSoapServer($uri, "TestSoap", "TestSoapInterface", new TestSoap);
$serverSoapZend->setIsSoapDocumentLiteral(false);
$serverSoapZend->handle();
```
#Document/Literal用SOAPサーバ
同様に Document/Litaral 版の SOAP サーバも書きます。

```php:testZendSoapServerRpcEncoded.php
<?php
require("init_autoloader.php");
require_once("ZendSoapServer.php");
require_once("TestSoap.php");

$uri = "http://localhost/soaptestzf2-autoloader/" . basename(__FILE__);

$serverSoapZend = new ZendSoapServer($uri, "TestSoap", "TestSoapInterface", new TestSoap);
$serverSoapZend->setIsSoapDocumentLiteral(true);
$serverSoapZend->handle();
```

#動作確認(RPC/Encoded)
動作確認用の SOAP クライアントを書きます。
PHP5 の SoapClient を使った場合、Zend Framwork2 の Zend\Soap\Client を使った場合の 2 通りを書いていますが、いずれも RPC/Encoded しか対応していないようです。

```php:testPhp5SoapClient.php
<?php
$uri = "http://localhost/soaptestzf2-autoloader/testZendSoapServerRpcEncoded.php";

// use php5 SoapClient
$client = new SoapClient($uri . "?wsdl", array("trace" => 1, "cache_wsdl" => WSDL_CACHE_NONE));
try {
    $result = $client->add(12, 45);
} catch (SoapFault $e) {
    echo "Caught SoapFault: " . print_r($e, true) . "\n";
} catch (Exception $e) {
    echo "Caught exception: " . print_r($e, true) . "\n";
}

echo "Result: " . $result . "\n";
```

```php:testZendSoapClint.php
require_once("init_autoloader.php");

$uri = "http://localhost/soaptestzf2-autoloader/testZendSoapServerRpcEncoded.php";
$client = new Zend\Soap\Client($uri . "?wsdl", array( "cache_wsdl" => WSDL_CACHE_NONE));

if (!$client) {
    echo "Failed in creating client.\n";
}

try {
    $result = $client->add(12, 45);
    echo "Result: " . $result . "\n";
} catch (SoapFault $e) {
    echo "Caught SoapFault: " . print_r($e, true) . "\n";
} catch (Zend\Exception $e) {
    echo "Caught exception: " . print_r($e, true) . "\n";
}
```

上記クライアントを実行して動作確認します。

```shell-session
% php testPhp5SoapClient.php
Result: 57
% php php:testZendSoapClint.php
Result: 57
```
#動作確認(Document/Literal)
Document/Literal 版の動作確認には Java を使います。WSDLからJava用のクライアントコードを生成するため、JDK8に含まれる wsimport を使います。

```shell-session
% $JAVA_HOME/bin/wsimport -s . -verbose http://localhost/soaptestzf2-autoloader/testZendSoapServerDocumentLiteral.php?wsdl
```

これでカレントディレクトリ直下に localhost/soaptestzf2_autoloader/testzendsoapserverdocumentliteral というディレクトリが作成され、その下に TestSoapService.java, TestSoapPort.java を含むソース一式が作成されます。

次に、テストコードを書きます。

```java:Hoge/TestSoapClient.java
package Hoge;

import localhost.soaptestzf2_autoloader.testzendsoapserverdocumentliteral.TestSoapService;
import localhost.soaptestzf2_autoloader.testzendsoapserverdocumentliteral.TestSoapPort;

public class TestSoapClient
{
    public static void main(String[] args)
    {
        try {
            TestSoapService service = new TestSoapService();
            TestSoapPort client = service.getTestSoapPort();
            String result = client.add("12", "45");

            System.out.println("Reuslt: " + result);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
```

コンパイル・実行して動作確認を行います。
```shell-session
% javac Hoge/TestSoapClient.java
% java Hoge.TestSoapClient
Result: 57
```

.NET でもできるようなので、気になる方は[こちらの記事](http://help.exacttarget.com/ja/technical_library/web_service_guide/getting_started_developers_and_the_exacttarget_api/connecting_to_the_web_service_api_using_wcf/)も参考にしてください。

#まとめ
- SOAP には旧形式 RPC/Encoded・新形式 Document/Literal がある。
- PHP5 で両方に対応する SOAP サーバを書くには、Zend Framework2 の Zend\Soap を使うしかなさそうなので、書いてみた。
- PHP5 で SOAP クライアントを書くと RPC/Encoded しか使えない。(＜なんでやねん!)
- Document/Literal な SOAP クライアントを書くには Java (か .NET) を使いましょう。

まあそうまでして何故 SOAP を使い続けるねん、というツッコミは甘んじて受ける所存です…。orz