VOYAGE GROUP エンジニアブログ

voyagegroup_techのブログ
VOYAGE GROUPエンジニアブログです。

PHP

Symfony2のススメ2 ~認証とともに~

こんにちは、底辺プログラマの高橋です。

今回で最後になりますが、Symfony2でデータベースを利用した場合のユーザ認証の実装について触れていきたいと思います。

(6)認証設定(doctrine利用の認証の場合)
「Symfony2のススメ1」(5)の記述から「encoders」と「providers」の項目を変更します。
「chain_provider」を指定する事で前回の設定を行った「user」アカウントでの設定を有効にしつつ追加設定を行っています。
このように複数のエンコーダー、プロバイダーを定義できる点 もこの認証機能のすぐれた点となります。

$ vi app/config/security.yml
security:
    encoders:
main1:
class: Symfony\Component\Security\Core\User\User
algorithm: plaintext
        main2:
            class:     Acme\SecurityBundle\Entity\User
            algorithm: sha1
    providers:
chain_provider:
providers: [in_memory, main]
in_memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
        main:
            entity:
                class: Acme\SecurityBundle\Entity\User
                property: username

(7)エンティティクラスの作成
symfony1系だと「lib/model」の下にモデルクラスが作成されていましたが、
Symfony2系はバンドルディレクトリの「Entity」下に作成する事になると思います。(※1)
設定ファイルからのクラスの自動生成も可能ですが、今回は手動で以下のファイルを作成します。

$ vi src/Acme/SecurityBundle/Entity/User.php
<?php
namespace Acme\SecurityBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
  * @orm\Entity(repositoryClass="Acme\SecurityBundle\Repository\UserRepository")
  * @orm\Table(name="user")
  */
class User implements UserInterface {
    /**
     * @orm\Id
     * @orm\Column(type="integer")
     * @orm\GeneratedValue(strategy="AUTO")
     */
    protected $id;
    /**
     * @orm\Column(name="username",unique="true", nullable="true")
     */
    protected $username;
    /**
     * @orm\Column(name="password",length="128")
     */
    protected $password;
     /**
      * @orm\Column(name="salt", length="16")
      */
    protected $salt;
    public function getRoles(){
        return array("ROLE_USER");
    }
    public function getPassword(){
        return $this->password;
    }
    public function getSalt(){
        return $this->salt;
    }
    public function getUsername(){
        return $this->username;
    }
    public function eraseCredentials(){
    }
    public function equals(UserInterface $user){
        return $this->getUsername() == $user->getUsername();
    }
}

コマンドを実行するとコメントの定義を元にセッター/ゲッターメソッドを「User.php」に自動生成してくれます。
コメントの「@orm~」の部分がデータベースの定義の記述となります。 

php app/console doctrine:generate:entities AcmeSecurityBundle:User
下記コマンドで
コメントの定義を元にデータベースにテーブルを作成します。
php app/console doctrine:schema:update --force

ユーザ管理に利用するテーブルのエンティティクラスは「UserInterface」をimplementsする必要があります。
そのため、Interfaceに定義されている下記メソッドを実装しています。

getRoles():      # ユーザの権限を返す
getPassword():   # ユーザのパスワードを返す
getSalt():       # ユーザのサルトを返す
getUsername():   # ユーザを識別するIDを返す
eraseCredentials(): # 重要なデータが含まれている場合の削除処理
equals():        # 同じユーザであるとの判定処理

(8)レポジトリクラスの作成
Symfony1系では「lib/model」の下に自動生成されていた「XXXXTable.class.php」(doctrineの場合)
「XXXXXPeer.php」(Propelの場合)と同じような用途のクラスとなります。(※1)
このクラスも自分で作成する必要があります。

$ vi src/Acme/SecurityBundle/Repository/UserRepository.php
<?php
namespace Acme\SecurityBundle\Repository;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\EntityRepository;
use Acme\SecurityBundle\Entity\User;

class UserRepository extends EntityRepository implements UserProviderInterface {
    public function loadUserByUsername($username) {
        return $this->findOneBy(array('username' => $username));
    }
    public function supportsClass($class)
    {
        return true;
    }
    public function refreshUser(UserInterface $user)
    {
        if (!$user->getUsername()) {
            throw new Exception(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }
        return $this->loadUserByUsername($user->getUsername());
    }
}
「UserProviderInterface」をimplementsする必要があるので、その実装をしています。
loadUserByUsername($username)     # usernameが与えられた時のユーザのロード処理
supportsClass($class)             # classの型の妥当性
refreshUser(UserInterface $user)  # ユーザのリフレッシュ処理

以上でユーザ認証する処理の実装は完了となります。
ユーザを作成する処理の実装については割愛します。Userテーブルに以下SQLでデータを入れると「hoge/hoge」でログインできるようになります
(今回の設定だと「sha1→base64」のストレッチングをデフォルトで5000回行なうのでこの値となります)

insert into user(id, username, password, salt) values(1, 'hoge', 'E5GwPYtO3mEBCJ3mTVn2VTu8p40=', '');


※1 エンティティとリポジトリについて
エンティティ/リポジトリという用語が登場しますが、このフレーズそのものはPofEAAやDDDで
使用されているパターン名になります。
自分の理解だと(怪しいですがw)下のような意味を持つようです。
エンティティは同一性をもったオブジェクトで、同一性とは5歳の自分と20歳の自分は属性情報が異なるが
同一性を持っておりエンティティとなる。逆に同じ属性情報をもっていても同一性がないと値オブジェクトとなる。
リポジトリはオブジェクトにアクセスするため手段で、データへのアクセス手段をカプセル化してドメイン-モデルレイヤ間の明確な分離を行う。
DDDのエッセンス:http://www.ogis-ri.co.jp/otc/hiroba/technical/DDDEssence/chap2.html
Repository(PofEAA):http://capsctrl.que.jp/kdmsnr/wiki/PofEAA/?Repository


今回実装した認証処理は自分が確認した時点では、リリースされたばかりのせいか
本家のドキュメントを眺めても纏まっておらずグーグル先生に尋ねたり、ソースを見るなど少しがんばる必要がありました。
ただユーザ会の方々がドキュメントを翻訳されてますし、今後も利用ケースが増えてくれば情報は充実していくと思います。
php5.3を利用する要件の場合、選択肢の一つになってくると思いますので
機会があったら一度試してみてはいかがでしょうか?
また最後になりますが、下書き段階の文章にご感想いただいた@brtriverさんありがとうございました!

Symfony2のススメ1.5 ~コンポーネントちょい話~

こんにちは、底辺プログラマの高橋です。
今回は「コンポーネント」と「Symfony2 Standard Edition」について簡単に触れます。

Symfony Component
Symfony2の各機能は Symfony Componentという単体でも利用可能なライブラリ群で構成されてます。

Symfony Component一覧 http://symfony.com/components

「Symfony2のススメ1」で簡単な認証機能の実装例を示しましたが、
認証機能はSecurityコンポーネントというライブラリを利用することで実装しています。
この認証機能はSymfony2だけに限らず単独の認証機能のライブラリとしても利用可能です。
(単独での利用は勉強不足で試せてないですが)
pearからのインストールも可能です。

$ sudo pear channel-discover pear.symfony.com
$ sudo pear remote-list -c symfony2
Channel symfony2 Available packages:
====================================
Package             Version
BrowserKit          2.0.0
ClassLoader         2.0.0
Config              2.0.0
Console             2.0.0
CssSelector         2.0.0
~略~
Symfony2 PEAR Channel http://symfony.com/blog/symfony2-pear-channel


Symfony2 Standard Editionについて


Symfony公式よりtarballをダウンロードすると「Symfony2 Standard Edition」を利用する事になります。
tarballの「Symfony2 Standard Edition」はコア機能+Doctione2等の外部ライブラリを纏めたファイルとなります。
しかし、gitレポジトリでは起動用ファイル/デモ機能/アプリケーションの殻の構成となっており、
gitからインストールしようとすると他レポジトリのインストール操作が必要になります。

$ git clone http://github.com/symfony/symfony-standard.git
$ cd symfony-standard
$ rm -rf .git
$ php bin/vendors install
Symfony2 Download:http://symfony.com/download
Symfony2 Standard Edition git レポジトリ:http://github.com/symfony/symfony-standard

「php bin/vendors install」では「deps」ファイルに記載されている各gitレポジトリを
「vender/」ディレクトリ下に展開しています。
ログ出力のようなコアに近い機能も「monolog」として独立したライブラリを利用してたりします。

depsファイルの中身
[symfony]
    git=http://github.com/symfony/symfony.git
    version=v2.0.0
[twig]
    git=http://github.com/fabpot/Twig.git
    version=v1.1.1
[monolog]
    git=http://github.com/Seldaek/monolog.git
    version=1.0.0
~略~

ちなみに「
Symfony Component」はhttps://github.com/symfony/symfony/tree/master/src/Symfony/Componentに存在してます。
以上、今回は機能の独立性が高い構造となってますね! という話でした。

Symfony2のススメ1 ~認証とともに~

株式会社ECナビ傘下のPeXで底辺のプログラマをやってます。高橋と言います。
底辺の実力と引き出しの少なさから何を書こうか道に迷っていたのですが
7/28にPHP5.3対応のフレームワークであるSymfony2(※1)がリリースされたという事で
Symfony2のユーザ認証まわりと簡単なカスタマイズを触れていきたいと思います。

※1 Symfony2とは何ぞやという人は下記ページをご参考にしてください。
Symfony2 公式 http://symfony.com/
日本Symfonyユーザー会 http://docs.symfony.gr.jp/
Symfony2 ドキュメント日本語版 http://docs.symfony.gr.jp/symfony2/index.html

今回の環境
今回の記事を書くにあたって下記構成で動作確認を行いました。

CentOS release 5.6
PHP 5.3.6
mysql 5.5.11
Symfony Standard Edition 2.0.0

認証と認可
最初にSymfony2に限った事ではないのですが、認証と認可について簡単に知っとく必要があります。
Symfony2では認証を「Firewalls」、 認可を「Access Controls」で設定します。

認証とは
Aさん、Bさんである本人確認。
認可とは
Aさんはシステム権限なので「/admin」にアクセスできる、Bさんはユーザ権限なので「/user」にアクセスできるといった権限の確認。

auth

では、最初に簡単なフォーム認証を作成してみます。
今回はSymfony2のインストールと設定については省いて認証の設定だけ載せますが
フレームワークのルートディレクトリ以下の構成と修正を行なうファイルは下記となります。 

app/                                         # 設定、キャッシュ、ログなどアプリケーションのディレクトリ
 |-config/                               # アプリケーション全体の設定
    |- routing.yml # ルーティングの設定ファイル
    |- security.yml # セキュリティ機能の設定ファイル
src/ # ユーザが作成したコードを設置するディレクトリ
  |- Acme/SecurityBundle/ # 今回作成するログイン機能のバンドル
    |- Controller/SecurityController.php # コントローラ
    |- Resources/views/Security/login.html.twig # テンプレート
    |- Entity/User.php # ORMクラス
    |- Repository/UserRepository.php
vendor/ # 外部のソースを置くディレクトリ
web/ # ドキュメントルート 

(1)認証用バンドルを作成
Symfony2では「バンドル」を作成してその配下にコードを実装します。
バンドルの殻はコマンドを実行して作成します。
色々選択肢が出ますが今回はすべてデフォルトで問題ないです。 

$ php app/console generate:bundle --namespace=Acme/SecurityBundle
$ php app/console generate:bundle --namespace=Acme/SiteBundle 

(2)ルーティングの設定
ルーティングとはアクセスパスと対応するコントローラなどの設定になります。。
「login」は「Acme/SecurityBundleバンドル」の「Securityコントローラ」の「loginアクション」が動作する設定となります。
「logout」、「login_check」はルーティング設定を書くだけで、あとでSymfony2がよろしくやってくれます。
「user」は認証エリアのコンテンツという事で特に実装は行いません。
$ vi app/config/routing.yml
login:
    pattern:   /user/login
    defaults:  { _controller: AcmeSecurityBundle:Security:login }
login_check:
    pattern:   /user/login_check
logout:
    pattern:   /user/logout
user:
pattern:   /user/{name}
defaults: { _controller: AcmeSiteBundle:Default:index }

(3)コントローラを修正
コントローラではログイン処理は実装せずに、テンプレートへの値の引渡し処理のみを実装します。

$ vi src/Acme/SecurityBundle/Controller/SecurityController.php
<?php
namespace Acme\SecurityBundle\Controller;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class SecurityController extends Controller
{
    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();
        // get the login error if there is one
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
        }
        return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        ));
    }
}

(4)テンプレートの修正
Symfony2からTwig(※2)というテンプレートエンジンが採用されています。
ユーザネームとパスワードを入力するフォームを作成します。
(今までのようにphpのテンプレートで記述する事も可能です。)

$ vi src/Acme/SecurityBundle/Resources/views/Security/login.html.twig
{% if error %}
    <div>{{ error.message }}</div>
{% endif %}
<form action="{{ path('login_check') }}" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="_username" value="{{ last_username }}" />
    <label for="password">Password:</label>
    <input type="password" id="password" name="_password" />
    <input type="submit" name="login" />
</form>

※2 Twigとは速くてセキュアで柔軟性の高い(と公式で主張されている)テンプレートエンジンです。
phpのテンプレートだとモダンなテンプレートエンジンでサポートされている機能がサポートされていないため導入した
と記載がありました。
個人的には「echo 」や「()」などを記述しなくて良い事で、わりかし見た目が簡潔になるのが良いと思ってます。
Twig公式 http://www.twig-project.org/
構文/設定 http://docs.symfony.gr.jp/symfony2/book/templating.html


(5)認証設定

認証の肝となる設定を行ないます。(※3)
今回は「/user」を認証エリア、その他は非認証エリアという設定にします。

「providers」「encoders」は認証方法などの項目で平文で設定ファイルのID/PASSで認証するように設定してます。
「firewalls」は認証を適用するエリア(URL)と認証しないエリアを設定しています。
「access_control」はパスに対するアクセス権限の設定しています。他にもアクセス元IPアドレスや接続プロトコル(https or http)などの設定が可能です。

$ vi app/config/security.yml
security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    providers:
        in_memory:
            users:
                user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
    firewalls:
        login:
            pattern:  ^/user/login$
            security: false
        secured_area:
            pattern:    ^/user
            form_login:
                check_path: /user/login_check
                login_path: /user/login
            logout:
                path:   /user/logout
                target: /user
    access_control:
        - { path: ^/user, roles: ROLE_USER}
※3 セキュリティ機能の設定について: http://symfony.com/doc/current/book/security.html

これで「/user」以下にアクセスしようとすると「/user/login」のログイン画面が表示されて認証が求められるようになり、「user/userpass」でログインできるようになります。
(やり方さえ知ってれば)簡単な気がしてきましたね!

Amazon S3 を PHP から使ってみた

こんにちわ。ECナビ事業本部の小林です。
9月からECナビに入社し、先日リリースした「ナビバリュー」の開発に携わってきました。

ナビバリューはECナビのサイト上で実施している人気の商品をどのECサイトよりも安く購入できるサービスです。現在のところ、毎週10種類の商品を数量限定で販売していますので、是非毎週チェックしてみてくださいね!!

さて宣伝はここらへんにしておいて、今回ナビバリューを開発するにあたって twitter、 bitly、Amazon Web Service など様々な外部のサービスを利用しています。
そのなかで今回は Amazon S3 を PHP から利用する方法について簡単に紹介をしていきたいと思います。

Amazon S3 って?
今更説明の必要はないかもしれませんが、Amazon が提供する従量制課金のストレージサービスです。詳しくは公式ページへ→ http://aws.amazon.com/jp/s3/

目的
ナビバリューの開発に合わせてユーザーが商品レビューを投稿する際に画像をアップロードできるようになりました。S3はアップロードされた画像の保管場所兼コンンツ提供サーバとして使用しています。自前でストレージを用意するよりは以下のようなメリットが挙げられます。
  • ディスク容量などの物理的な制限を気にせずに済む
  • 専用サーバを用意するより早い、初期コストが安く済む
実際に使ってみる
まず本格的にアプリケーションに組み込む前にファイルをアップロードする簡単なプログラムで試してみました。
  1. SDK
    PHP から利用するには amazon から提供されている SDK を利用させてもらいます。この SDK には PHP から Amazon S3 や EC2 などを使うのに便利な API が含まれています。
  2. 設定ファイル
    ダウンロードした SDK の中に config-sample.inc.php という名前のファイルが含まれており、これが設定ファイルの雛形になります。これを config.inc.php という名前でコピーして、必要な項目を記述していきます。設定に必要な情報は http://aws.amazon.com/ からアカウント→セキュリティ証明書で確認できます。この config.inc.php と SDK に含まれる sdk.class.php を include します。
    include_once "./sdk-1.0.1/sdk-1.0.1/sdk.class.php";
    include_once "./config.inc.php";
  3. バケットをつくる
    Amazon S3 ではバケットというディレクトリのようなものを作成し、そこにファイルをアップロードしていきます。ただしバケットの中にバケットを作ることはできません。また、バケット名は Amazon S3 のサービス内でユニークでなければなりません。ここでつけたバケット名はアップロードしたファイルを公開したときのURLに使われます。たとえば ecnavi-tech-blog  というバケット内に test.jpg というファイルを作成した場合は
      http://ecnavi-tech-blog.s3.amazon.aws.com/test.jpg
    でアクセスすることができます。 バケットを作成するには AmazonS3 クラスの create_bucket を使います。ここではリージョンやアクセス権の指定もできますが、テストなのでとりあえずデフォルトのままにしておきます。
    $s3 = new AmazonS3();
    $s3->create_bucket("ecnavi-tech-blog");
  4. ファイルをアップロードする
    ファイルをアップロードするには AmazonS3 クラスの create_object で、バケット名、アップロード先のファイル名、第3引数の配列でアップロード元のファイル名やアクセス権などを指定します。ここでは/tmp/foo.jpgという名前のファイルをtest.jpgという名前でアップロードしています。またhttp経由で誰でも見られるようにするために、アクセス権はACL_PUBLICを指定します。
    $s3->create_object("ecnavi-tech-blog", "test.jpg", array('fileUpload' => "/tmp/foo.jpg", 'acl' => AmazonS3::ACL_PUBLIC);
    これで3で書いた URL で画像が表示されると思います。
    TIPS
      公開URLを階層構造にしたい場合、アップロード後のファイル名(2番目の引数)を "testdir/test.jpg" などとすると下記のようなURLで公開できます。
      http://ecnavi-tech-blog.s3.amazon.aws.com/testdir/test.jpg
まとめ
アップロードの触りだけでしたが、ドキュメントとSDKに含まれているサンプルコードを一通りながめればその他の操作も特に難なくできると思います。ソフトウェアの開発期間が短かくなっている昨今、このように便利なAPIが用意されているとRESTやSOAPを直接操作することをしなくて済むので非常に助かります。

重たい処理を華麗にスルーして、アプリケーションの体感速度をぐっと向上させる方法

こんにちは。adingoでプログラマをしている真幡です。

アプリケーションの評価指標の一つにレスポンス性能があります。どれほど素晴らしいアプリケーションでも、レスポンスを返すまでに時間がかかるシステム(=重いシステム)は敬遠されがちです。今回はGearmanというジョブキュー管理ソフトウェアを使い、ウェブアプリケーションのレスポンス性能を向上させる方法を紹介します。

ジョブキューとは何か

ジョブキューとはジョブをキューで管理するものです。これでは説明になっていませんね。キューとはFIFO(First In First Out)を実現するデータ構造です。キューに登録されたモノは、キューに登録した順に処理されます。ジョブキューにおいては、キューに登録するモノはジョブなので、キューに登録した順にジョブが処理されることになります。ジョブキューに登録するジョブの粒度は大小を問いません。したがって、ユーザから重たいジョブを処理するようにリクエストされたときに、ジョブを逐次的に処理せず、ジョブキューに登録することで「ジョブの受付をしました」とユーザにレスポンスを返すことができます。「リクエストを受け付ける=>リクエストを処理する=>レスポンスを返す」という従来の流れから「リクエストを受け付ける=>(とりあえず)レスポンスを返す=>リクエストを処理する」という流れを実現することができます。レスポンスの早さはユーザ体験の向上につながるので、これはうまいやり方であると言えます。

次の図はジョブキューの全体像です。

ジョブキュー

ジョブサーバーが持つキューに注目してください。ジョブサーバーから出ている矢印を見ると、ジョブをキューの上側から詰めていることが分かります。一方、ジョブキューから出ている矢印を見ると、ジョブをキューの下側から取っていることが分かります。先ほどの「キューに登録した順にジョブが処理される」という説明を図で表現すると、このようになります。

より具体的なケースとして、ブログサービスにおける「ブログ記事の投稿」について考えてみましょう。このブログサービスでは、記事が投稿されると次のような処理を行うとします。

  • 記事内容をデータベースに追加
  • 添付画像を(モバイル用の)サムネイル画像に変換
  • RSSファイルの更新
  • 記事投稿の完了通知をメールで送信
この一連の処理において、添付画像の処理が重たい処理であると仮定します(例えば5秒程度の処理時間がかかるとします)。この場合、とりあえず記事内容をデータベースに追加し、それから先の処理をジョブキューに登録し、先にユーザにレスポンスを返すという実装方法が考えられます。こうすることで、ユーザのブラウザを5秒程度フリーズさせる部分を解消することができます。この代償として、添付画像の反映、RSSファイルの更新、メールの送信が5秒程度遅れることになりますが、サービスの性質を考えると、それほど大きな問題ではないと思います。

ところで、同じようなことをcronを使って実現することも可能です。短い間隔でcronを実行し、その中で添付画像の処理、RSSファイルの更新、メールの送信をすれば同様の結果を得られます。では、cronではなくジョブキューを使うメリットは何でしょうか。後述しますが、ジョブキューではワーカーと呼ばれるプロセスが常駐することで、プロセス起動のコストを削減することができます。cronが起動するプロセスがライブラリなどを大量に読み込む必要がある場合、プロセス起動のコストは高くつきます。そのような場合は、cronで都度プロセスを立ち上げるより、ジョブキューの常駐プロセス(ワーカー)を使う方が適切でしょう。

以上のことをまとめると、次のような処理を行うケースではジョブキューを使うメリットがあると言えます。
  • 即時性を求められない処理
  • プロセス起動のコストが高い処理
ジョブキューを効果的に使うことによって、アプリケーションの体感速度を向上し、システムの負荷を下げることも可能であると納得できたでしょうか。以下では、Gearmanというジョブキュー管理システムの使い方について説明します。

Gearmanのインストール

Gearmanのインストール方法は次の通りです。インストール環境は「MacOSX Snow Leopard」を想定しています。

$ sudo port selfupdate
$ sudo port install gearmand
$ sudo pecl install gearman channel://pecl.php.net/gearman-0.7.0

最後に、php.iniに"extension=gearman.so"を追記してください。

Gearmanの構成要素

Gearmanの構成要素には次の3種類があります。

  • ワーカー(またはコンシューマー): 要求に応答する役割
  • クライアント(またはプロデューサー): 要求を生成する役割
  • ジョブサーバー(またはエージェント): クライアントとワーカーの仲介を行う役割

Gearmanデーモンの起動

gearmand(Gearmanデーモン)を起動します。次のようにコマンドを打ちます(前述のようにGearmanをインストールした場合)。

$ /opt/local/sbin/gearmand -d

"-d"オプションはバックグラウンドで起動するためのオプションです。次のようにgearmandプロセスが残れば問題ありません。

$ ps xu | grep gearmand | grep -v "grep"
y-mahata 1314 0.0 0.0 2435700 320 ?? Ss 1:35PM 0:00.00 /opt/local/sbin/gearmand -d

クライアントとワーカーを動かす

ワーカーとしてworker.phpを用意します。

<?php
// worker.php
// ワーカー(コンシューマー)
$worker = new GearmanWorker();
$worker->addServer();
$worker->addFunction('log', 'my_log');
while ($worker->work());

function my_log($job)
{
sleep(60);
file_put_contents('/tmp/foo/' . $job->workload(), $job->workload());
}

ワーカーをバックグラウンドで動作させるため、実行するときには"&"を付けます。

$ php worker.php &

worker.phpについて説明します。これは、GearmanWorkerオブジェクトを生成し、addServer関数でジョブサーバーを追加します。引数なしでaddServer関数を呼ぶと127.0.0.1がジョブサーバーとして追加されます。その後、ワーカーとして提供する関数をaddFunction関数で追加します。この例では、60秒のスリープの後に/tmp/foo以下にログを吐き出すmy_log関数を"log"という名前で(クライアントに)提供します。

次に、クライアントとしてclient.phpを用意します。

<?php
// client.php
// クライアント(プロデューサー)
$client = new GearmanClient();
$client->addServer();
for ($i = 0; $i < 10; $i++) {
// $client->do('log', $i);
$client->doBackground('log', $i);
}

こちらは、何も考えずに実行することができます。

$ php client.php

client.phpについて説明します。これは、GearmanClientオブジェクトを生成し、addServer関数でジョブサーバーを追加します。GearmanWorkerオブジェクトと同様に、引数なしでaddServer関数を呼ぶと127.0.0.1がジョブサーバーとして追加されます。GearmanClientオブジェクトのdoBackground関数は、ジョブキューにジョブを追加し、すぐに終了します。この関数は第一引数にワーカーの関数名を、第二引数にワークロードを取ります。ワークロードとは、ワーカー側のworkload関数で取り出すことのできる値です。do関数はdoBackground関数と同様の引数を取りますが、結果を受け取るまで待ちに入る点が異なります。この他にも、GearmanClientオブジェクトには、ジョブを追加し最後にまとめて実行する関数なども存在します。詳細についてはPHPマニュアルのGearmanの項を参照してください。

client.phpを実行した後、/tmp/fooディレクトリを見ると次のようになります。

$ ls -l /tmp/foo/
total 80
drwxr-xr-x 12 y-mahata wheel 408 6 7 17:05 ./
drwxrwxrwt 22 root wheel 748 6 7 17:05 ../
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:56 0
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:57 1
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:58 2
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:59 3
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:00 4
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:01 5
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:02 6
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:03 7
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:04 8
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:05 9

client.phpはすぐに終了したはずですが、ファイル生成はきっかり60秒間隔で行なわれたことが確認できます。

注意するべきポイント

doBackground関数やdo関数の第二引数であるワークロードは文字列を受け取ります。したがって、文字列以外のデータ(例えば配列など)をワーカーに渡すときにはシリアライズをする必要があります。また、doBackground関数やdo関数は第三引数としてID(となる文字列)を渡すことができます。この値を指定すると、同一のIDで登録したジョブは一度しか実行されないので、注意してください。

一歩進んだトピック

Gearmanはデフォルトでメモリ上でキューを管理します。もしキューの管理をMySQLやDrizzleで行いたければlibdrizzleを使うことができます。また、ジョブキュー管理ソフトウェアはGearmanの他にも存在します。TheSchwartzQ4Mなどと比較することで、Gearmanの長所と短所をより理解することができるかもしれません。

参考資料

記事検索
QRコード
QRコード