# Zend_Amf_Server(日本語)

Zend_Amf_Server は RPC スタイルのサーバで、 Adobe Flash Player からの AMF プロトコルによるリクエストを処理します。 他の Zend Framework のサーバクラス群と同様に SoapServer API にしたがっており、 サーバを作成するための習得しやすいインターフェイスを提供します。

基本的な AMF サーバ

さまざまな public メソッドを持つクラス Foo を作ったものとしましょう。AMF サーバを作成するためのコードは次のようになります。

    $server = new Zend_Amf_Server();
    $server->setClass('Foo');
    $response = $server->handle();
    echo $response;

もうひとつの方法として、単純な関数をコールバックとしてアタッチすることもできます。

    $server = new Zend_Amf_Server();
    $server->addFunction('myUberCoolFunction');
    $response = $server->handle();
    echo $response;

複数のクラスや関数を混ぜて使用することもできます。 その場合は、それぞれに名前空間を指定してメソッド名の衝突を回避させることをおすすめします。 名前空間を指定するには、addFunction() あるいは setClass() の 2 番目の引数に文字列を指定します。

    $server = new Zend_Amf_Server();
    $server->addFunction('myUberCoolFunction', 'my')
           ->setClass('Foo', 'foo')
           ->setClass('Bar', 'bar');
    $response = $server->handle();
    echo $response;

Zend Amf Server は、 指定したディレクトリパスから動的にサービスに読み込ませることもできます。 好きなだけのディレクトリをサーバに指定することが可能です。 サーバに後から追加したディレクトリから順に (LIFO: 後入れ先出し) 検索を行い、クラスにマッチするディレクトリを探します。 ディレクトリの追加は addDirectory() メソッドで行います。

    $server->addDirectory(dirname(__FILE__) .'/../services/');
    $server->addDirectory(dirname(__FILE__) .'/../package/');

リモートサービスをコールする際には、アンダースコア ("_") およびドット (".") をディレクトリ区切り文字として使用します。 アンダースコアを使用すると、PEAR や Zend Framework のクラス命名規約に従った形式となります。 つまり、サービス com_Foo_Bar をコールした場合は、 インクルードされたパスのどこかにある `com/Foo/Bar.php` を探します。ドット記法を使用してリモートサービスを `com.Foo.Bar` のように指定すると、 インクルードされたパスの最後に `com/Foo/Bar.php` を追加して `Bar.php` を自動的に読み込みます。

スクリプトに送られたすべての AMF リクエストがサーバで処理され、 その結果の AMF レスポンスが返されます。

アタッチされるすべてのメソッドや関数には docblock が必要

Zend Framework の他のサーバコンポーネント群と同様、クラスのメソッドには PHP docblock 形式のドキュメントが必要です。 少なくとも必須引数と返り値についてのアノテーションが必要となります。 次の例をごらんください。

    // アタッチする関数

    /**
     * @param  string $name
     * @param  string $greeting
     * @return string
     */
    function helloWorld($name, $greeting = 'Hello')
    {
        return $greeting . ', ' . $name;
    }

    // アタッチするクラス

    class World
    {
        /**
         * @param  string $name
         * @param  string $greeting
         * @return string
         */
        public function hello($name, $greeting = 'Hello')
        {
            return $greeting . ', ' . $name;
        }
    }

その他のアノテーションを使用することもできますが、無視されます。

## サーバへの Flex からの接続

Zend_Amf_Server に Flex プロジェクトから接続するのはきわめて簡単です。 エンドポイントの URI を Zend_Amf_Server スクリプトに指定するだけでよいのです。

たとえば、作成したサーバをアプリケーションルートに `server.php` という名前で配置したとしましょう。URI は `http://example.com/server.php` となります。 この場合は、services-config.xml ファイルを編集して、 チャンネルのエンドポイント URI 属性をこの値に変更します。

まだ `service-config.xml` ファイルを作っていない場合は、 まずナビゲータウィンドウでプロジェクトを開きます。 そしてプロジェクト名のところを右クリックして 'プロパティ' を選択します。 プロジェクトのプロパティダイアログで 'Flex ビルドパス' を選択し、 'ライブラリパス' タブで '`rpc.swc`' ファイルがプロジェクトに追加されていることを確認したら、 OK を押してウィンドウを閉じます。

また、リモートオブジェクトのエンドポイントを探す際に `service-config.xml` を使用することをコンパイラに指定する必要もあります。 ナビゲータのプロジェクトフォルダを右クリックしてプロパティを選択し、 もういちどプロジェクトのプロパティパネルを開きます。 そこで 'Flex コンパイラ' を選択して、 `-services "services-config.xml"` を追加します。 適用、そして OK を押してオプションを更新します。 これで結局何をやったのかというと、実行時の変数を `services-config.xml` から読み込んで RemotingObject クラスで使うよう Flex コンパイラに指示したということです。

それから、リモートメソッドへの接続の際に使用するサービス設定ファイルを Flex に教えてやる必要があります。そこで、Flex プロジェクトの src フォルダに '`services-config.xml`' ファイルを新たに作成します。 プロジェクトフォルダで右クリックして '新規作成' 'ファイル' を選択すると新しいウィンドウが開きます。プロジェクトフォルダを選択し、 ファイル名を '`services-config.xml`' と指定して終了を押します。

Flex は新しい `services-config.xml` ファイルを作成し、それを開きます。 次の例のとおりに `services-config.xml` ファイルを作成してください。 エンドポイントの部分はあなたが使用するサーバに書き換えます。 そしてファイルを保存することを忘れないようにしましょう。

    <?xml version="1.0" encoding="UTF-8"?>
    <services-config>
        <services>
            <service id="zend-service"
                class="flex.messaging.services.RemotingService"
                messageTypes="flex.messaging.messages.RemotingMessage">
                <destination id="zend">
                    <channels>
                        <channel ref="zend-endpoint"/>
                    </channels>
                    <properties>
                        <source>*</source>
                    </properties>
                </destination>
            </service>
        </services>
        <channels>
            <channel-definition id="zend-endpoint"
                class="mx.messaging.channels.AMFChannel">
                <endpoint uri="http://example.com/server.php"
                    class="flex.messaging.endpoints.AMFEndpoint"/>
            </channel-definition>
        </channels>
    </services-config>

この例にはポイントがふたつあります。まず AMF チャネルを作成し、そしてエンドポイントの URL を Zend_Amf_Server に指定します。

    <channel-definition id="zend-endpoint"
        <endpoint uri="http://example.com/server.php"
            class="flex.messaging.endpoints.AMFEndpoint"/>
    </channel-definition>

このチャネルの ID として "zend-endpoint" を指定したことに注意しましょう。 この例では、このチャネルを指すサービスを作成して、その ID を指定しました。 この場合の ID は "zend" となります。

Flex の MXML ファイルで、 RemoteObject をサービスにバインドしなければなりません。 MXML で次のように記述します。

    <mx:RemoteObject id="myservice"
        fault="faultHandler(event)"
        showBusyCursor="true"
        destination="zend">

ここでは、新しいリモートオブジェクトに "myservice" という名前をつけ、 さきほど `services-config.xml` で定義した "zend" にそれをバインドしています。ActionScript からメソッドをコールするには、 "myservice.<method>" とするだけです。例を示します。

    myservice.hello("Wade");

名前空間を使う場合は "myservice.<namespace>.<method>" のようにします。

    myservice.world.hello("Wade");

Flex RemoteObject の実行についてのより詳細な情報は Adobe Flex 3 のヘルプサイト をごらんください。

## エラー処理

デフォルトでは、アタッチしたクラスや関数からスローされた例外はすべて捕捉され、 AMF ErrorMessage として返されます。しかし、この ErrorMessage オブジェクトの中身は、サーバが "production" モード (デフォルトの状態) であるか否かによって異なります。

production モードの場合は、例外コードのみが返されます。 production モードを無効にする (これはテスト時にしか行ってはいけません) と、例外についての詳細が返されるようになり、 例外メッセージや行番号、バックトレースがすべて返されます。

production モードを無効にするには、次のようにします。

    $server->setProduction(false);

再度有効にするには、`TRUE` を渡します。

    $server->setProduction(true);

production モードの無効化は慎重に!

production モードを無効にするのは、開発時のみにすることを推奨します。 例外メッセージやバックトレースにはシステムに関する重大な情報が含まれる可能性があり、 外部からアクセスされることは好ましくありません。 AMF はバイナリ形式ではありますが、その仕様は公開されています。 つまり、誰でもメッセージを解読できる可能性があるということです。

もうひとつ、特に注意を要するのが PHP のエラーです。 INI 設定 display_errors が有効になっていると、 エラー報告レベルに応じてあらゆる PHP のエラーが直接出力されてしまいます。 これは、AMF のレスポンスを壊してしまう可能性があります。 運用時には display_errors を無効にし、 この問題を回避することを推奨します。

## AMF レスポンス

レスポンスオブジェクトを操作したくなることもあるかもしれません。 メッセージヘッダを追加したい場合などが考えられます。サーバの handle() メソッドはレスポンスオブジェクトを返すので、これが利用できます。

AMF レスポンスへのメッセージヘッダの追加

この例では、'foo' という MessageHeader に値 'bar' を設定したものをレスポンスに追加してからそれを返します。

    $response = $server->handle();
    $response->addAmfHeader(new Zend_Amf_Value_MessageHeader('foo', true, 'bar'))
    echo $response;

## 型付きオブジェクト

SOAP と同様、 AMF でもクライアントとサーバの間でオブジェクトをやりとりできます。 これにより、クライアントとサーバの間での柔軟性と一貫性を確保できます。

Zend_Amf には、 ActionScript と PHP オブジェクトを関連付けるための 3 つのメソッドが用意されています。

- まず、サーバ側で明示的なバインドを行うには setClassMap() メソッドを使用します。 最初の引数は ActionScript クラス名で、2 番目の引数は関連付ける PHP クラス名となります。

      // ActionScript クラス 'ContactVO' と PHP クラス 'Contact' を関連付けます
      $server->setClassMap('ContactVO', 'Contact');

- 次に、PHP クラス内で public プロパティ `$_explicitType` を設定する方法があります。 ここには、関連付けたい ActionScript クラス名を指定します。

      class Contact
      {
          public $_explicitType = 'ContactVO';
      }

- 3 番目の方法として、PHP クラスの public メソッド getASClassName() を使用することもできます。 このメソッドは、適切な ActionScript クラスを返すようにしなければなりません。

      class Contact
      {
          public function getASClassName()
          {
              return 'ContactVO';
          }
      }

サーバ側で ContactVO を作成したら、 サーバオブジェクトに対応するクラスを AS3 で書かなければなりません。

Flex プロジェクトの src フォルダを右クリックし、新規作成 -> ActionScript ファイルを選択します。ファイルに ContactVO という名前をつけて終了を押すと、新しいファイルがあらわれます。 次のコードをコピーして、クラスを作成しましょう。

    package
    {
        [Bindable]
        [RemoteClass(alias="ContactVO")]
        public class ContactVO
        {
            public var id:int;
            public var firstname:String;
            public var lastname:String;
            public var email:String;
            public var mobile:String;
            public function ProductVO():void {
            }
        }
    }

このクラスは、同名の PHP のクラスと構文的に同等となります。 変数名はまったく同じで、大文字小文字もあわせておかなければ正しく動作しません。 このクラスでは、AS3 独特のメタタグが 2 つ用いられています。 最初のタグは bindable で、これは更新時に change イベントを発火させます。 2 番目のタグは RemoteClass で、このクラスがリモートオブジェクトを保持できること、 そのエイリアス名が (ここでは) ContactVO であることを定義します。 このタグに設定される値は、PHP のクラスと正確に一致していなければなりません。

    [Bindable]
    private var myContact:ContactVO;

    private function getContactHandler(event:ResultEvent):void {
        myContact = ContactVO(event.result);
    }

サービスコールの後の result イベントは即時に Flex の ContactVO にキャストされます。 myContact にバインドされているすべての内容は、返された ContactVO データで更新されます。

## リソース

Zend_Amf には、サービスクラスが返すリソース型を ActionScript で使用可能な形式のデータにマッピングするツールが用意されています。

リソース型を扱うには、そのリソース名に対応する名前のプラグインクラスを作成する必要があります。 クラス名は、リソース名の単語の先頭を大文字化してスペースを除去したものとなり (つまり、リソース型 "mysql result" の場合は MysqlResult となります)、 それに何らかのプレフィックスをつけてたとえば My_MysqlResult のようになります。このクラスは parse() メソッドを実装しなければなりません。 このメソッドはリソースを引数として受け取り、ActionScript に送信する値を返します。 このクラスを記述するファイル名は、クラス名の最後の部分と同じ名前にしなければなりません。 たとえば先ほどの例の場合は `MysqlResult.php` となります。

リソース用のプラグインを含むディレクトリは、 Zend_Amf 型ローダーで登録しなければなりません。

    Zend_Amf_Parse_TypeLoader::addResourceDirectory(
        "My",
        "application/library/resources/My"
    );

プラグインのロードに関する詳細は、 プラグインローダー を参照ください。

Zend_Amf リソース用のデフォルトディレクトリは自動的に登録されており、 現在ここには "mysql result" リソースおよび "stream" リソースのプラグインが格納されています。

    // Example class implementing handling resources of type mysql result
    class Zend_Amf_Parse_Resource_MysqlResult
    {
        /**
         * Parse resource into array
         *
         * @param resource $resource
         * @return array
         */
        public function parse($resource) {
            $result = array();
            while($row = mysql_fetch_assoc($resource)) {
                $result[] = $row;
            }
            return $result;
        }
    }

未知のリソース型 (つまり、処理用プラグインが存在しない型) を返そうとすると、例外が発生します。

## サーバへの Flash からの接続

Zend_Amf_Server に Flash プロジェクトから接続する方法は、 Flex からの場合とは多少異なります。しかし、いったん接続してしまえば Zend_Amf_Server は flex の場合と同じように動作します。 次の例は Flex AS3 ファイルからでも使用できます。 同じ Zend_Amf_Server 設定ファイルを用い、 World クラスを用いて接続します。

Flash CS を開き、新規 Flash ファイル (ActionScript 3) を作成します。 そのドキュメントに `ZendExample.fla` という名前をつけ、 このサンプルを使用するフォルダに保存します。 次に、同じディレクトリに新規 AS3 ファイルを作成し、 `Main.as` という名前をつけます。 そして両方のファイルをエディタで開きます。 これから、ドキュメントクラスを通じてふたつのファイルをつないできます。 ZendExample を選択し、ステージ上でクリックします。 ステージのプロパティパネルで、ドキュメントクラスを Main に変更します。 これで、ActionScript ファイル `Main.as` が `ZendExample.fla` のユーザインターフェイスとつながります。 Flash ファイル ZendExample を実行すると、 `Main.as` クラスが実行されるようになるのです。 次に、AMF をコールする ActionScript を追加します。

それでは、Main クラスを作成していきましょう。 これを用いてデータをサーバに送信し、結果を表示します。 次のコードを `Main.as` にコピーしましょう。これから、 このコードの中身を追いかけながら何をやっているのかを説明していきます。

    package {
      import flash.display.MovieClip;
      import flash.events.*;
      import flash.net.NetConnection;
      import flash.net.Responder;

      public class Main extends MovieClip {
        private var gateway:String = "http://example.com/server.php";
        private var connection:NetConnection;
        private var responder:Responder;

        public function Main() {
          responder = new Responder(onResult, onFault);
          connection = new NetConnection;
          connection.connect(gateway);
        }

        public function onComplete( e:Event ):void{
          var params = "Sent to Server";
          connection.call("World.hello", responder, params);
        }

        private function onResult(result:Object):void {
          // Display the returned data
          trace(String(result));
        }
        private function onFault(fault:Object):void {
          trace(String(fault.description));
        }
      }
    }

まず、さまざまな作業をするための ActionScript ライブラリをインポートする必要があります。 ひとつめが NetConnection で、これはクライアントとサーバの間でパイプのような働きをします。 もうひとつは Responder オブジェクトで、 これはコールが成功したかどうかなどのサーバからの返り値を処理します。

    import flash.net.NetConnection;
    import flash.net.Responder;

クラスの中で 3 つの変数を用意します。これらがそれぞれ NetConnection、Responder そして Zend_Amf_Server へのゲートウェイ URL をあらわします。

    private var gateway:String = "http://example.com/server.php";
    private var connection:NetConnection;
    private var responder:Responder;

Main のコンストラクタでレスポンダを作成し、また Zend_Amf_Server エンドポイントへの新規接続も作成します。 レスポンダでは、サーバからのレスポンスを処理するメソッドが 2 つ定義されています。 わかりやすくするため、それぞれ onResult および onFault と名づけます。

    responder = new Responder(onResult, onFault);
    connection = new NetConnection;
    connection.connect(gateway);

onComplete 関数は、コンストラクタの処理が終わった直後に実行されます。 ここで、データをサーバに送信します。 Zend_Amf_Server World->hello 関数をコールするコードを 1 行追加しています。

    connection.call("World.hello", responder, params);

responder を作成した際に、サーバからのレスポンスを処理する関数として onResult と onFault を定義しました。サーバから正しい結果が返ってきたとき用の関数を追加します。 成功時のイベントハンドラは、サーバへの接続が正しく処理されるたびに毎回実行されます。

    private function onResult(result:Object):void {
        // Display the returned data
        trace(String(result));
    }

onFault 関数は、サーバから無効な結果が返ってきたときにコールされます。 たとえば、サーバからエラーが返された場合、サーバへの URL が無効な場合、 リモート側にサービスやメソッドが存在しない場合など、 接続時に問題が発生した場合にコールされることになります。

    private function onFault(fault:Object):void {
        trace(String(fault.description));
    }

これで、ActionScript 内でのリモート接続処理は完成しました。 ZendExample を実行すると、 Zend_Amf へ接続されるようになります。 ここまでを振り返ってみましょう。まず最初にリモートサーバへの接続に必要な変数を追加し、 サーバからのレスポンスを受け取ったときに使用するメソッドを定義し、 そして最後に返された結果を trace() で出力しました。

## 認証

Zend_Amf_Server では、認証と認可のフックを指定して サービスへのアクセスを制御できます。この仕組みは、 Zend_Auth および Zend_Acl コンポーネントが提供するものを使用しています。

認証を定義するには、抽象クラス Zend_Amf_Auth_Abstract を継承した認証アダプタを作成します。 このアダプタは、通常の 認証アダプタ と同様に authenticate() メソッドを実装しなければなりません。

アダプタでの認証の際には、親クラス Zend_Amf_Auth_Abstract のプロパティ _username および _password を使用しなければなりません。 AMF リクエストヘッダに認証情報が含まれていれば、 authenticate() がコールされる前にサーバが setCredentials() メソッドを用いてその情報を設定します。

アダプタが返す識別情報は、role プロパティを含むオブジェクトでなければなりません。 これを用いて ACL アクセス制御が動作します。

認証に失敗した場合はそれ以降のリクエストの処理は行われず、 失敗したというメッセージとその理由を結果として返します。

アダプタとサーバを関連づけるには setAuth() メソッドを使用します。

    $server->setAuth(new My_Amf_Auth());

アクセス制御を行うには setAcl() メソッドで設定した Zend_Acl オブジェクトを使用します。

    $acl = new Zend_Acl();
    createPermissions($acl); // パーミッション情報を作成します
    $server->setAcl($acl);

ACL オブジェクトが設定され、コールされるクラスで initAcl() メソッドが定義されていれば、 ACL オブジェクトを引数としてこのメソッドがコールされます。 このクラスは追加の ACL ルールを作成して `TRUE` を返すこともできますし、 このクラスでのアクセス制御が不要な場合は `FALSE` を返すこともできます。

ACL の準備が終わると、 サーバ側で「指定されたロールで、指定されたクラス (関数コールの際は null) のリソース、指定された関数の実行権限があるかどうか」を調べます。 認証情報が提供されていない場合は anonymous ロールが定義されているかどうかを調べ、 定義されている場合はそれを使用します。定義されていない場合はアクセスを拒否します。

    if($this->_acl->isAllowed($role, $class, $function)) {
        return true;
    } else {
        require_once 'Zend/Amf/Server/Exception.php';
        throw new Zend_Amf_Server_Exception("Access not allowed");
    }