API指向なPHPフレームワークBEAR.Sunday

こんにちは。サイオステクノロジー技術部の武井です。今回は、API指向なPHPフレームワークBEAR.Sundayをご紹介したいと思います。

API指向とは?

BEAR.Sundayとは、Restful APIの開発を前提としたPHPアプリケーションフレームワークです。最近のアプリケーション開発では、よくAPIっていう言葉が出てきます。今までのように、1つの巨大なアプリケーションに、画面もビジネスロジックも全て詰め込むのではなく、なるべく細かい単位で分割して開発する手法が流行っています。

私のよく利用するJava言語で言えば、今までは、1つのWARファイルの中にJSPやら、ServletやらBeanやらビジネスロジックやら全てが詰まっていました。でもこれだと、じゃぁ、クライアントがスマホになったときにどうするんだとか、別のサービスから利用する場合にも不都合が発生します。ビジネスロジックはServletやJSPから利用することが前提で作成されているので、外部に切り出せません。

なので、ものすごく細かい単位(例えば掲示板サービスで言えば、記事一覧表示とか記事削除とか)で、HTTPでアクセスできるような形式で、処理を提供するのが望ましいとされています。HTTPでリクエストして、HTTPでレスポンスを受け取ります。HTTPはご存知の通り、Webブラウジングにも利用されいてる汎用的なプロトコルなので、どこでも利用できます。FireWallなどのインフラ要因で使えないというケースも少ないです。

そういったAPIをRestful APIと呼びます。このRestful APIには4つの原則があり、これをなるべくわかりやすく私なりに説明してみたいと思います。

アドレス可能性(Addressability)
一つ一つのAPIをわかりやすいURLで一意に表現できることを示します。例えば掲示板のサービスがあったとします。このサービスには、記事作成、記事更新、記事一覧表示、記事詳細表示、記事削除という処理があったとします。これらのAPIを提供するURLは以下のように表現します。

■記事作成
URL:https://hoge.com/messages
Method:POST

■記事更新
URL:https://hoge.com/messages/123
Method:PUT
※123は記事を一意に識別するIDです

■記事一覧取得
URL:https://hoge.com/messages
Method:GET

■記事詳細取得
URL:https://hoge.com/messages/123
Method:GET
※123は記事を一意に識別するIDです

■記事削除
URL:https://hoge.com/messages/123
Method:DELETE
※123は記事を一意に識別するIDです

なんとなくわかるとは思うのですが、それぞれの処理がURLでわかりやすく一意に表現されています。これがアドレス可読性(Addressability)です。

わかりにくいURLの例として、記事削除APIのURLのアンチパターンを書いてみます。

URL:https://hoge.com/servlet/bbs.do?action=delete&messageid=123

なんだかURLからでは記事を削除する処理だというのが類推しにくいですし、何と言ってもこれはJava ServletやJavaアプリケーションフレームワークStruts特有のURLです。例えば、アーキテクチャの変更などで、実装言語が変わったときには、わざわざJava ServletやStruts特有のめんどくさいURLに合わせるという意味のない作業が発生します。

ステートレス性(Stateless)
一つ一つのAPIが、それ単体で呼び出せるようでなければなりません。平たく言ってしまえば、セッションを使わない実装のことを言います。セッションを使った実装は以下のようなイメージになります。
Screen Shot 2018-02-20 at 7.48.07

アプリケーションサーバーはセッションを発行し、セッションIDとそれに紐づく値(ユーザー名など)を保存します。それと同時にアプリケーションサーバーはユーザーにセッションIDのCookieを発行します。次回のリクエストからは、ユーザーはHTTPリクエストにセッションIDが格納されたCookieをリクエストし、アプリケーションサーバー側では、そのCookieにあるセッションIDから、アプリケーションサーバー側にあるユーザー情報を取得して、アクセスしてきたユーザーを識別します。

しかし、これだと不都合が生じる時があります。アプリケーションサーバーの負荷が高くなり、サーバーの台数を増やす(スケールアウト)したときは、以下のような構成になります。

Screen Shot 2018-02-20 at 7.56.51

どちらのアプリケーションサーバーにアクセスしたときも同じセッション情報を取得できるようにしなければなりません。そのためには、セッション情報をアプリケーションサーバー間で共有する仕組みが必要になります。セッション情報をデータベースに格納したり、セッション情報をサーバー間でレプリケーションしたりと手法は色々ありますが、どれも大掛かりな構成となり非常に手間がかかります。これでは簡単にスケールアウトできません。これがステートレスでないことのデメリットです。

一方、ステートレスの場合、以下のような構成となります。

Screen Shot 2018-02-20 at 8.08.26

認証にJSON Web Token(詳細はこちら)を用いた例です。アプリケーションサーバー側から発行されたJSON Web TokenをクライアントのWebストレージに保存して、リクエストごとにAuthorizationヘッダにJSON Web Tokenを乗せています。JSON Web Tokenはその性質上、サーバー側に情報を持たなくてよい(JSON Web Tokenの中に情報がある)ので、インフラ構成がシンプルになります。

APIを用いたシステムの場合、APIはその汎用性を保つため、非常に細かい機能に分割され、様々なシステムから利用することが予想されます。そのため、負荷も高くなり、必要に応じてスケールアウトすることが必然とされますが、ステートレスであるということはスケールアウトしやすいということになります。

これがステートレスであるということのメリットです。

接続性(Connectability)

1つの情報の中に別の情報へのリンクを含めることを言います。

例えば、記事情報一覧のAPIをコールした結果、返されるJSONが以下であるとします。記事の件数は10000件あったとします。

{
  [
    {
      id: "00001",
      title: "こんにちは",
      body: "お腹すいた"
    },
    {
      id: "00002",
      title: "こんばんは",
      body: "喉乾いた"
    },
    ...以下、あと9998件分続く...
  ]
}

一度のリクエストで10000件近い情報をやり取りするのは効率がよくありません。Restful APIはHTTPで利用されることが多く、HTTPの多くはインターネット経由でやり取りされます。回線が不安定で遅いインターネットでは、なるべくデータはコンパクトにする必要があります。なので、以下のようにします。

{
  [
    {
      id: "00001",
      title: "こんにちは",
      body: "お腹すいた"
    },
    {
      id: "00002",
      title: "こんばんは",
      body: "喉乾いた"
    },
  ],
  nextLink: "https://hoge.com/messages?next=2&sessid=iw20gmb04kjwamv3ew"
}

nextLinkというフィールドを追加しました。これは次の2件分を取得するためのリンクです(一度に2件というのはいささか少なすぎですが、説明を簡略化するためにそうしてます)。sessidというのは、このリンク有効期限を表す情報で、一定期間すぎるとこのリンクが無効になるようにします。

統一インターフェース(Uniform Interface)

リソースの取得、作成、更新、削除にはそれぞれHTTPメソッドGET、POST、PUT、DELETEを用いましょうということになります。これは、先程ご説明した「アドレス可能性(Addressability)」のところで少々説明しております。

■記事作成
URL:https://hoge.com/messages
Method:POST

■記事更新
URL:https://hoge.com/messages/123
Method:PUT
※123は記事を一意に識別するIDです

■記事一覧取得
URL:https://hoge.com/messages
Method:GET

■記事詳細取得
URL:https://hoge.com/messages/123
Method:GET
※123は記事を一意に識別するIDです

■記事削除
URL:https://hoge.com/messages/123
Method:DELETE
※123は記事を一意に識別するIDです

ご覧の通り、取得、作成、更新、削除にはそれぞれHTTPメソッドGET、POST、PUT、DELETEに対応しております。

このようにルールを決めておけば、APIを発行するときに迷わなくて済みます。このAPIはどのメソッドを使うんだっけと文献を漁る必要もなく、取得だったらGET、作成だったらPOSTでよいのです。

BEAR.Sundayとは?

BEAR.Sundayとは、このRestful APIの開発を容易にするフレームワークです。

https://bearsunday.github.io

以下の特徴があります。

リソース指向
先程ご説明したRestful API4原則を満たす考え方です。

DI
JavaアプリケーションフレームワークSpringやSeasarでも採用されている技法で、インスタンスを作成する(つまりnewする)処理をバラバラに記述するのではなく、一か所に集めて、プログラム同士の結びつきを弱くしましょうという考え方です。

AOP
メソッドの前と後で行われる定型的な処理(認証処理やロギングなど)を自動化しましょうという考え方です。

非常に多機能で、ここでは全てを紹介しきれないのですが、一つ目のリソース指向の部分だけ簡単にまとめてみました。

025c3b03547d7836a73ec85cac80241f

BEAR.Sundayは大きく分けて、「App層」「Page層」の二つに分かれています。「App層」はいろいろなところから共通で利用するAPIのようなイメージです。「Page層」は表現の形式をつかさどる部分でApp層で作成したAPIを利用し、JSON形式で表現したり、HTML形式で表現したりします。

なので、アプリケーションが動作する順番は以下のようになります。ブラウザでユーザーがアクセスしたことを前提とします。

  1. ユーザーはブラウザでPHPのプログラムにアクセスする。
  2. HTTPのメソッドに応じたPage層のメソッドが呼ばれる。(※参照)
  3. Page層からApp層のメソッドを呼ぶ。(HTTPのgetメソッドでリクエストがあった場合は、App層のクラスのonGetメソッドを呼ぶ)
  4. App層のメソッドからJSONのレスポンスが返る。
  5. Page層のメソッドはApp層から返されたJSONのレスポンスを受け取って、HTMLに整形してHTTPレスポンスとして返す。

※ Page層、App層ともに4つのメソッドがあります。それぞれ「onGet「onPost」「onPut」「onDelete」です。これはHTTPメソッドに対応していて「Get「Post」「Put」「Delete」のときにそれぞれのメソッドが呼ばれます。

簡単なアプリケーションを作ってみる

名前を指定すると挨拶を返してくれるアプリケーションを作成してみます。

https://ホスト名/Hello?name=nori

にアクセスすると、ブラウザに以下のように表示されるアプリケーションを作成してみます。

Hello nori!!

まずプロジェクトを作成します。そのためにはComposerというパッケージ管理ツールが必要です。インストールは以下で一発です。

# curl -sS https://getcomposer.org/installer | php

そしてBEAR.Sundayのプロジェクトのひな形を作成します。

# composer create-project bear/skeleton /var/tutorial

すると以下のような構成のディレクトリが出来上がります。

tutorial
    bootstrap
        api.php → App層で作成したスクリプトをコマンドラインで試すためのもの
        web.php → Page層で作成したスクリプトをコマンドラインで試すためのもの
    var
        log → ログが出力されるディレクトリ
        www
            index.php → すべてのHTTPリクエストが集約されるPHPスクリプト
    src
        Resource
            App → App層で作成したスクリプトを格納するディレクトリ
            Page → Page層で作成したスクリプトを格納するディレクトリ
        Module
            AppModule.php → Seasarでいうところのdiconファイル(後述)
    Vendor → 外部のライブラリが格納されるディレクトリ

まず、App層のファイルを作成します。/var/tutorial/src/Resource/AppのディレクトリにHello.phpというファイルを以下のように作成します。このプログラムについて、説明します。

<?php
namespace Vendor\Tutorial\Resource\App; // (1)

use BEAR\Resource\ResourceObject; // (2)

class Hello extends ResourceObject // (3)
{
    public function onGet($name) // (4)
    {
        $greeting = "Hello ".$name; // (5)
        $this['greeting'] = $greeting; // (6)

        return $this; // (7)
    }
}
?>

上記のソースコードの詳細は以下のとおりです。

namespace Vendor\Tutorial\Resource\App; // (1)

名前空間を指定します。Javaでいうところのpackageで、名前空間が違えば、同じクラス名を使っても問題ないです。実際クラス名はよくかぶります。

use BEAR\Resource\ResourceObject; // (2)

Javaでいうところのimportになります。事前に指定した名前空間のクラスを利用するときに使います。

class Hello extends ResourceObject // (3)

クラスの定義です。App層のクラスを作成する場合には、必ずResourceObjectというクラスを継承します。これは、このフレームワークの決まりです。先ほどの「use」で宣言したクラスは、このResourceObjectを宣言しています。Abstractなクラスなので、単体ではnewできないようです。そして、いろんなインターフェースを実装しているようです。このクラスを継承すると、HTTPリクエストの内容がこのクラスのインスタンス自身に設定されるようです。

    public function onGet($name) // (4)

onGetというメソッドを定義しています。publicはJavaと同じで、外部からこのメソッドが利用できるという意味です。そして、このonGetメソッドはPage層から呼び出されます。この辺りはPage層の話になるので、後述します。

そして、引数として、名前を定義しています。こちらもPage層から指定するのですが、これも後述します。

        $greeting = "Hello ".$name; // (5)

指定された名前に「Hello」という文字列を付与しています。

        $this['greeting'] = $greeting; // (6)

JSONのレスポンスの出力形式を定義してます。

        return $this; // (7)

最後にこのクラスのインスタンスをreturnします。フレームワーク側では、Page層からアクセスがあると、ResouceObjectを継承したクラスのインスタンスが返されることを期待しているようです。

では、上記のスクリプトの動作確認をしてみます。動作確認はbootstrapディレクトリにあるapi.phpで行います。以下のコマンドを発行して下さい、

# php /var/tutorial/bootstrap/api.php get '/Hello?name=nori'

以下のようなレスポンスが返ってきます。

200 OK
content-type: application/hal+json

{
    "gretting": "Hello nori!!",
    "_links": {
        "self": {
            "href": "/Hello?name=nori"
        }
    }
}

1つ目のフィールドのキーは、先ほどApp層のクラスHelloで定義した’$this[‘greeting’] = $greeting;’とマッチしています。上記のように記述すると、JSONのキーと値の形式で出力されます。これは、フレームワークが裏側でそのように処理してくれています。

次に、先ほどのApp層で作成したクラスを利用するPage層のクラスを作成します。

BEAR.SundayはHTMLやJSONに出力するためのテンプレートエンジンを自由に切り替えることができます。テンプレートエンジンとは、ServletでいえばJSPに相当する部分です。JSPはJSP独自の書き方でServletから受け取った値をHTMLに出力することができます。

Servletもそうですが、こういったものはJavaの世界でもJSPのほかにVelocityなどたくさんのテンプレートエンジンがあります。

JSPだとServletから受け取った変数の記述方法は<%= name %>と書きますが、Velocityでは${name}とかきます。そのほかにもいろいろと異なる部分があり、どちらも一長一短ありで、ケースバイケースで使い分けます。

そして、PHPの世界にもたくさんのテンプレートエンジンがあり、その一つはTwigで、一番メジャーですので、今回もTwigを使います。

そして、BEAR.Sundayはこのテンプレートエンジンの切り替えを簡単に行うことができます。StrutsなどはJSP固定ですが、BEAR.Sundayは、自分の好みに合わせて、商用やOSSの色々なテンプレートエンジンを利用できます。

テンプレートエンジンの切り替え方法については、Ray.DIなどの知識が必要となるため、詳細は後程説明します。なので、ここでは、Twigへの出力方法は機械的に覚えてしまって構いません。

では先ほどのApp層で作成したクラスを利用するPage層のクラスを作成します。Hello.phpというファイルを/var/tutorial/src/Resource/Pageに作成します。

<?php
namespace Vendor\Tutorial\Resource\Page;

use BEAR\Resource\ResourceObject;

class Hello extends ResourceObject // (1)
{
    public function onGet($name) // (2)
    {
        $this['name'] = $name; // (3)
        $this['greeting'] = $this->resource
            ->get // (4)
            ->uri('app://self/hello') // (5)
            ->withQuery(['name' => $name]) // (6)
            ->request();

        return $this; // (7)
    }
}

上記のソースコードの詳細は以下のとおりです。

class Hello extends ResourceObject // (1)

App層と同様にResouceObjectを継承します。

    public function onGet($name) // (2)

ブラウザからはgetメソッドで呼ばれる想定なので、onGetメソッドを作成します。そしてURLのクエリパラメータに指定される引数がname=noriの場合は、onGetのメソッドの引数名を$nameとします。これはフレームワークの決まりです。

        $this['name'] = $name; // (3)

ブラウザに出力したい値を$this[‘xxx’]の形式で記載します。このように書くとTwigのテンプレートエンジンから{name}といった書式でこの値を取得できます。

            ->get // (4)

App層で作成したメソッドを呼び出している処理です。getはApp層で作成したonGetメソッドを呼び出すという意味です。ここがpostと書いてあれば、onPostメソッドが呼び出されます。

            ->uri('app://self/hello') // (5)

App層で定義したHelloというクラスを呼び出しますよという意味です。前の記述合わせるとApp層のHelloというクラスのonGetメソッドを呼び出しますよという意味になります。

            ->withQuery(['name' => $name]) // (6)

onGetメソッドの引数nameに$nameを入れますよという意味になります。

        return $this; // (7)

このクラスのインスタンス自信をreturnします。これはこのフレームワークの決まりです

「/var/tutorial/var/www/index.php」の「app」を以下のように「html-app」に変更します。

<?php

$context = 'html-app';
require dirname(dirname(__DIR__)) . '/bootstrap/bootstrap.php';

この記述をすることによりTwigのテンプレートエンジンを使う準備ができました。正確にいうと、この記述をすることにより、HtmlModule.phpに記載した内容をもとに、TwigのRendereをBEAR.SundayのRenderInterfacleにBindするのですが、詳細は後述します。

次にいよいよテンプレートエンジンTwigModuleをインストールします。BEAR.Sundayはシンプルなフレームワークで、データベースへ接続するためのモジュールなどはほとんど入っていません。必要なものは適宜インストールする必要があります。

これはComposerでインストールします。

# cd /var/tutorial
# composer require madapaja/twig-module

/var/tutotial/vendor配下にmadapajaというディレクトリが作成されます。そこにTwig関連のモジュールが入っています。

次にModuleファイルを記述します。ここでは、とりあえず、おまじないだと思い以下のファイルを作成してください。(詳細はRay.DIに関する記事(を書く予定)で説明します。)

/var/tutorial/src/Module/HtmlModule.phpを以下の内容で作成してください。

<?php
namespace Vendor\Tutorial\Module;

use Ray\Di\AbstractModule;
use Madapaja\TwigModule\TwigModule;
use Madapaja\TwigModule\Annotation\TwigPaths;

class HtmlModule extends AbstractModule
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {

        $this->install(new TwigModule); // (1)
        $appDir = dirname(dirname(__DIR__)); // (1)
        $paths = [$appDir . '/var/twig']; // (1)
        $this->bind()->annotatedWith(TwigPaths::class)->toInstance($paths); // (1)
    }
}

上記のソースコードは、今の段階ではおまじないでよいのですが、一点だけ説明します。

        $this->install(new TwigModule); // (1)
        $appDir = dirname(dirname(__DIR__)); // (1)
        $paths = [$appDir . '/var/twig']; // (1)
        $this->bind()->annotatedWith(TwigPaths::class)->toInstance($paths); // (1)

ここで、テンプレートを配置する場所(Servletでいうところのjspの配置場所)をアプリケーションディレクトリ/var/twigとしています。つまり今回の場合は、/var/tutorial/ /var/twigとしています。

次にTwigのテンプレートを作成します。

/var/tutorial/var/twig/Hello.html.twigを以下の内容で作成してください。

<!DOCTYPE html>
<html>
<body>
Hello {{ name }}!!
</body>
</html>

先ほど/var/tutorial/src/Resource/Page/Hello.phpで記載した$this[‘name’] を、Twigのテンプレートファイルで{{name}}という形式で取り出すことができます。

これで準備ができました。ブラウザに以下のURLを入力すると・・・

https://ホスト名/Hello?name=nori

以下の内容が表示されます。

Hello nori!!

以上がリソース指向の説明でした。おまじない的な部分が多かったのですが、そこについては次回以降で説明します。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

1人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です