今日も適当ダイアリー

PHP や Javascript や Symfony、BEAR.Sunday などのWeb周りのことを中心に。それ以外のことも気まぐれに投稿します。

Symfony2 + MongoDB ODM を使ってみる

この記事は、Symfony アドベントカレンダー2010 に参加しています。



投稿時の設定を間違え、12/14付けになっちゃってますが、Adventカレンダー15日目の記事です!(投稿も15日早朝です)
日付変えちゃうとURL変っちゃうのでそのままにしてありますが、ご容赦くださいませ〜


と発言したので、今回は Doctrine MongoDB ODM を Symfony2 から使ってみる。という、割と初心者向けチュートリアル的な記事です。

今回使用した環境は下記の通り。

  • PHP 5.3.3
  • MongoDB 1.7.3
  • Symfony2 PR4
  • Doctrine MongoDB ODM 1.0.0 beta1
  • PHP Mongo Extension 1.1.0 (pecl)

MongoDB ODM ってなに?

ODM は Object Document Mapper の事で、Doctrine2 のORM(Object Relational Mapper)によく似ていて、PHP 5.3.0+ のオブジェクトをMongoDBのドキュメント(RDBのレコードに相当)として簡単に扱えるようになります。

Symfony2 から MongoDB ODM は標準でサポートされているので、早速ためしてみたいと思います。

作るもの

解説のみだとドキュメント見たほうが豊富だし、そもそも1回の記事では解説しきれないので、今回はサンプルプログラムとして“ささやきアプリケーション”を作る事にします。
まぁ、twitter的な一言掲示板アプリですね。

要件は下記の通りです。

  • ささやきアプリケーションの機能
    • ささやき投稿
    • ささやきリスト表示(ささやきを時間順(降順)で表示)
    • 特定の人のささやき表示(指定した人が発言したささやきを時間順(降順)で表示)
  • ささやきは以下の項目で構成されます
    • ID (今回は MongoDB で自動生成される MongoId を利用します)
    • 名前(String)《入力項目》
    • 本文(String)《入力項目》
    • ささやいた時間(DateTime)

下記のような感じになる予定です。

サンドボックスを用意

今回は、サンドボックスをベースに作りますので、サンドボックスを用意します。
GitがインストールされたLinux系OSであれば、下記のような感じでしょうか。

$ git clone git://github.com/symfony/symfony-sandbox.git
$ cd symfony-sandbox/src
$ ../bin/install_vendors.sh
$ chmod 777 ../app/cache ../app/logs

アプリケーション設定の変更

では、さっそく、アプリケーションの設定を変えていきます。

app/AppKernel.php

まずは、MongoDB ODM を使うためのバンドルと、ささやきアプリケーション用のバンドルをアプリケーションに登録するために、AppKernel.phpファイルを編集します。

<?php

/* 8< 8< 8< 8< 略 8< 8< 8< 8< */

class AppKernel extends Kernel
{

    /* 8< 8< 8< 8< 略 8< 8< 8< 8< */

    public function registerBundles()
    {
        $bundles = array(
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\TwigBundle\TwigBundle(),

            // enable third-party bundles
            new Symfony\Bundle\ZendBundle\ZendBundle(),
            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new Symfony\Bundle\DoctrineBundle\DoctrineBundle(),
            //new Symfony\Bundle\DoctrineMigrationsBundle\DoctrineMigrationsBundle(),
            new Symfony\Bundle\DoctrineMongoDBBundle\DoctrineMongoDBBundle(),
            // ↑コメントアウトを解除し、DoctrineMongoDBBundleを有効にします。

            // register your bundles
            new Application\SasayakiBundle\SasayakiBundle(),
            // ↑HelloBundleは使わないので、HelloBundleを削除し、
            // かわりにSasayakiBundleを登録します。
        );

        if ($this->isDebug()) {
            $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
        }

        return $bundles;
    }

    /* 8< 8< 8< 8< 略 8< 8< 8< 8< */
app/config/config.yml

次に、設定ファイルにMongoDB ODM の設定を追記します。

## doctrine_odm.mongodbが有効になるように追記。
doctrine_odm.mongodb: ~

ちなみに、上記設定の場合、接続などにデフォルト値が使われますので、変更したい場合はパラメータを指定する必要があります。
今回は、下記のように設定しましたが、環境に合わせて変更してください。

## doctrine_odm.mongodbが有効になるように追記。
doctrine_odm.mongodb:
    mappings:
        SasayakiBundle: ~
    server: mongodb://localhost:27077
    default_database: sasayakiapp
    options:
        connect: true
        persist: foobar
app/config/routing.yml

ルーティングルールも変更します。
今回は、Sasayakiバンドルのルーティングルールに従うように、下記のように変更しました。

homepage:
    resource: SasayakiBundle/Resources/config/routing.yml
その他の設定

もし、デフォルトのHTMLを変えたければ、お好みで app/views/layout.twig も編集しましょう。

Sasayakiバンドルの作成

下準備が整ったら、Sasayakiバンドルを作るためにディレクトリを作ります。
ディレクトリの構成は下記の通りです。

  • src/Application/SasayakiBundle : Sasayakiバンドルのルート
    • /Controller : コントローラファイル
    • /Document : ODM用のクラス(ドキュメント)ファイル
    • /Resources
      • /config : 設定ファイル
      • /views : ビューファイル
src/Application/SasayakiBundle/SasayakiBundle.php

Sasayakiバンドルを有効にするために、Bundleファイルを書きます。

<?php

namespace Application\SasayakiBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class SasayakiBundle extends Bundle
{
}

Sasayakiドキュメントの作成

まずは、MongoDBに保存される「Sasayakiドキュメント」を作ります。

基本的には、上に書いたささやきの情報をそのままクラス化すればOKですが、MongoDB ODM を使うために「@mongodb:」から始まるコメントを記述する必要があります。

src/Application/SasayakiBundle/Document/Sasayaki.php

2010/12/15追記:
下記のサンプルでは要素をpublicプロパティで宣言していますが、Doctrine2では、private/protectedプロパティとして宣言した上で、getter/setterメソッドでアクセスする事が推奨されています。
Doctrine2マニュアル:27. Best Practices — Doctrine 2 ORM 2 documentation

<?php
namespace Application\SasayakiBundle\Document;

/**
 * -- 接続するコレクション名を指定
 * @mongodb:Document(collection="sasayaki")
 *
 * -- nameとsasayaki_atをインデックスとして登録
 * @mongodb:Indexes({
 *   @mongodb:Index(keys={"name"="asc"}),
 *   @mongodb:Index(keys={"sasayaki_at"="desc"})
 * })
 */
class Sasayaki
{
    /**
     * -- MongoID型
     * (MongoDB上では規定のプライマリキーである
     *   "_id" フィールドとして保存されます)
     * @mongodb:Id
     */
    protected $id;

    /**
     * @mongodb:String
     */
    public $name;

    /**
     * @mongodb:String
     */
    public $body;

    /**
     * -- MongoDate型 (Timestamp)
     * @mongodb:Date
     */
    protected $sasayaki_at;

    public function __construct()
    {
        // 生成時に時間が自動で設定されるようにします。
        // \DateTime を使うのがオススメ。
        $this->sasayaki_at = new \DateTime();
    }

    /**
     * return integer $id
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * return DateTime $sasayaki_at
     */
    public function getSasayakiAt()
    {
        return $this->sasayaki_at;
    }
}

id と sasayaki_at については、自動的に生成され、変更不可なので protected メンバ変数として定義し、getXXXというメソッドを使用してアクセスする事とします。

なお、通常のタイムスタンプの保存には「@mongodb:Date」を指定します。
「@mongodb:Timestamp」ではありませんので、注意してください。「@mongodb:Timestamp」で定義した場合、MongoTimestampクラスが利用され、このクラスはシャーディングで使用される特別な型となります。
MongoTimestampクラスについては、PHP: MongoTimestamp - Manualを参照してください。

注意:

ODMへのIndex関連の設定は、上記設定で通っているようなのですが、MongoDB側にちゃんと伝わっていない模様です。
なぜかは良く分かっていない(きちんと調べていない)のですが、情報をお持ちの方がいれば教えてください。。。

ODM側でのインデックス情報のチェック

<?php
    // MongoDB ODM DocumentManager を取得</span>
    $dm = $this->get('doctrine.odm.mongodb.document_manager');

    // インデックス情報を取得
    var_export($dm->getClassMetadata('Application\SasayakiBundle\Document\Sasayaki')->getIndexes());

返ってきた値(nameとsasayaki_atがそれぞれキー指定されている)

<?php
    array(
        array(
            'keys' => array('name' => 1),
            'options' => array('unique' => false),
        ),
        array(
            'keys' => array('sasayaki_at' => -1),
            'options' => array('unique' => false),
        ),
    )

MongoDB側でのインデックス情報のチェック(Mongoシェル)

> db.sasayaki.getIndexes();

返ってきた値(_idしかキーになっていない)

[{
    "name" : "_id_",
    "ns" : "sasayakiapp.sasayaki",
    "key" : { "_id" : 1 },
    "v" : 0
}]

なぜゆえ!?

2011-01-04追記
自動的にやってくれるものと勘違いしていたのですが、
下記のように明示的にインデックスを作らないといけないようです。

<?php
   $dm->getSchemaManager()->ensureIndexes();

タイミング的には、データ投入時のflush()の前あたりかと思われます。

コントローラの作成

MongoDB ODMの利用方法

さっき作ったドキュメントを使うのであらかじめ、

<?php
use Application\SasayakiBundle\Document\Sasayaki;

などとしておきます。

まず、コントローラ内で下記のようにMongoDB ODM の DocumentManager を取得しておきます。

<?php
    // MongoDB ODM DocumentManager を取得
    $dm = $this->get('doctrine.odm.mongodb.document_manager');

MongoDB へデータを保存するには、DocumentManager を取得した後に下記のようにします。

<?php
    // Sasayakiドキュメントを生成し、データを入れる
    $sasayaki = new Sasayaki();
    $sasayaki->name = '名前です';
    $sasayaki->body = 'ここにボディテキストを指定します!';

    $dm->persist($sasayaki); // データを入れて
    $dm->flush(); // 保存

データの取得は方法が何通りもありますが

<?php
    // 1. 全件取得
    $sasayaki = $dm->find('Application\SasayakiBundle\Document\Sasayaki')
        ->getResults();

    // 2. _idを指定し、1件だけ取得
    $sasayaki = $dm->findOne('Application\SasayakiBundle\Document\Sasayaki',
        array("_id" => new \MongoId("XXXXXXXXXXXXXXXXXXXXXXXX"))
    );

    // 3. リポジトリを取得し、nameがJhonのものを取得
    $sasayaki = $dm->getRepository('Application\SasayakiBundle\Document\Sasayaki')
        ->findByName("Jhon")
        ->getResults();

    // 4. createQuery でクエリを生成し、sasayaki_atを降順で取得
    $sasayaki = $dm->createQuery('Application\SasayakiBundle\Document\Sasayaki')
        ->sort('sasayaki_at', 'desc')
        ->getResult();

などの方法で取得出来ます。

ちなみに、今回は使いませんが、findAndModify を使用する事で効率的に編集・削除を行うことも可能です。

<?php
    $sasayaki = $dm->createQuery('Application\SasayakiBundle\Document\Sasayaki')
        // ささやきを探す
        ->findAndModify()
        ->field('name')->equals('Jhon')

        // アップデートします
        ->update()
        ->field('name')->set('ジョン')
        ->execute();

    $sasayaki = $dm->createQuery('Application\SasayakiBundle\Document\Sasayaki')
        // ささやきを探す
        ->findAndModify()
        ->field('name')->equals('Jhon')

        // 削除する
        ->remove()
        ->execute();
src/Application/SasayakiBundle/Controller/SasayakiController.php

では、結果として出来たコントローラは下記の通りです。

<?php

namespace Application\SasayakiBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Symfony\Component\Form\Form;
use Symfony\Component\Form\TextField;
use Symfony\Component\Form\TextareaField;

// さっき作ったSasayakiドキュメントを使う
use Application\SasayakiBundle\Document\Sasayaki;

class SasayakiController extends Controller
{
    /**
     * ささやきの取得
     */
    public function listAction()
    {
        // MongoDB ODM DocumentManager を取得
        $dm = $this->get('doctrine.odm.mongodb.document_manager');

        // Sasayakiドキュメントのクエリを作成
        $sasayaki = $dm->createQuery('Application\SasayakiBundle\Document\Sasayaki')
            // 発言順(降順)にソート
            ->sort('sasayaki_at', 'desc')
            // 最初の10件だけ
            ->limit(10)
            // 取得
            ->getResult();

        return $this->render('SasayakiBundle:Sasayaki:index.twig', array(
            'sasayaki' => $sasayaki,
            'sasayakiname' => 'みんな',
        ));
    }

    /**
     * $nameのささやきを取得
     */
    public function findByNameAction($name)
    {
        // MongoDB ODM DocumentManager を取得
        $dm = $this->get('doctrine.odm.mongodb.document_manager');

        // Sasayakiドキュメントのクエリを作成
        $sasayaki = $dm->createQuery('Application\SasayakiBundle\Document\Sasayaki')
            // フィールド name が $name のドキュメントを
            ->field('name')->equals($name)
            // 発言順(降順)にソート
            ->sort('sasayaki_at', 'desc')
            // 最初の10件だけ
            ->limit(10)
            // 取得
            ->getResult();

        return $this->render('SasayakiBundle:Sasayaki:index.twig', array(
            'sasayaki' => $sasayaki,
            'sasayakiname' => $name,
        ));
    }

    /**
     * ささやきの投稿
     * モード切替のために $standalone を定義しておく(viewで使用)
     */
    public function createAction($standalone = true)
    {
        // Sasayakiドキュメントを生成
        $sasayaki = new Sasayaki();

        // フォームと結びつけて
        $form = new Form('sasayaki', $sasayaki, $this->get('validator'));

        // 入力項目を追加
        $form->add(new TextField('name'));
        $form->add(new TextareaField('body'));

        // メソッドが POST の場合
        if ('POST' === $this->get('request')->getMethod()) {
            // 送信された sasayaki を取得して
            $form->bind($this->get('request')->request->get('sasayaki'));

            // バリデーションOKなら
            if ($form->isValid()) {
                // MongoDB ODM DocumentManager を取得
                $dm = $this->get('doctrine.odm.mongodb.document_manager');
                // DocumentManager にデータを入れて
                $dm->persist($form->getData());
                // 保存
                $dm->flush();

                // リストページにリダイレクト
                return $this->redirect($this->generateUrl('sasayaki_list'));
            }
        }

        return $this->render('SasayakiBundle:Sasayaki:sasayakiform.twig', array(
            'form' => $form,
            'standalone' => $standalone,
        ));
    }
}
src/Application/SasayakiBundle/Resources/config/routing.yml

では、このコントローラにアクセス出来るようにルーティングを書きます。

sasayaki_list:
    pattern:  /
    defaults: { _controller: SasayakiBundle:Sasayaki:list }

sasayaki_create:
    pattern:  /sasayaku
    defaults: { _controller: SasayakiBundle:Sasayaki:create }

sasayaki_byname:
    pattern: /name/:name
    defaults: { _controller: SasayakiBundle:Sasayaki:findByName }

これで、http://example.com/ で一覧、http://example.com/sasayaku で投稿画面、http://example.com/name/[検索した名前] で特定の人の投稿画面が見られますね。

src/Application/SasayakiBundle/Resources/config/config.yml

Validationが有効になるようにconfig.ymlも書きます。

app.config:
    validation:
        enabled: true
src/Application/SasayakiBundle/Resources/config/validation.yml

最低限のValidationの設定を書きました。

Application\SasayakiBundle\Document\Sasayaki:
    properties:
        name:
            - NotBlank: ~
        body:
            - NotBlank: ~

ビューの作成

ここまでくれば、後はビューの作成のみ。
サンプルなので、最低限の内容を書いていきます。

src/Application/SasayakiBundle/Resources/views/layout.twig
{% extends "::layout.twig" %}

{% block body %}
{% block content %}{% endblock %}
{% endblock %}
src/Application/SasayakiBundle/Resources/views/Sasayaki/index.twig

リストページのテンプレートです。

{% extends "SasayakiBundle::layout.twig" %}

{% block content %}
{# <!--
  一覧ページにも投稿画面が欲しいので、createActionの内容を描画します。
  このフォームは、リストページに組み込まれるため、
  standaloneフラグをfalseにしておきます。
--> #}
{% render "SasayakiBundle:Sasayaki:create" with ['standalone': false] %}

{# <!-- 特定の人のささやきの表示の場合、sasayakinameに人の名前が入ります --> #}
<h2>{{ sasayakiname|default('みんな') }} のささやき</h2>

<div id="list">
{% for item in sasayaki %}
{# <!-- それぞれのささやきを表示 --> #}
  <div class="sasayaki">
    <div class="name">name: <a href="{% path 'sasayaki_byname' with ['name': item.name] %}">{{ item.name }}</a></div>
    <div class="body">{{ item.body }}</div>
    <div class="date">sasayaki at: {{ item.sasayakiAt|date("Y-m-d H:i:s") }}</div>
  </div>
{% endfor %}
</div>
{% endblock %}
src/Application/SasayakiBundle/Resources/views/Sasayaki/sasayakiform.twig
{# <!-- 
  standalone が true(SasayakiBundle:Sasayaki:create に直接アクセス)の場合、
  通常のレイアウトを継承し、
  それ以外の(リストページから呼び出された)場合、form.twig を 継承します。
--> #}
{% extends standalone ? "SasayakiBundle::layout.twig" : "SasayakiBundle:Sasayaki:form.twig" %}

{% block content %}
<div id="sasayakiForm">
<h2>ささやく</h2>

<form action="{% path 'sasayaki_create' %}" method="post">
    {{ form|render }}

    <input type="submit" value="Send!" />
</form>
</div>
{% endblock %}
src/Application/SasayakiBundle/Resources/views/Sasayaki/form.twig
{# フォーム以外には描画しない #}
{% block content %}
{% endblock %}

完成!

というわけで、完成したささやきアプリケーションは下記のようなものになりました。

■一覧画面
  

■投稿画面
  

■特定の人の一覧画面
  

Symfony2 の下準備から始めたため、若干長くなってしまいましたが、MongoDB ODMへのアクセス自体は非常に簡単で、柔軟なアクセスが可能になっていることが分かるかと思います。
MongoDB ODM の機能は、ここでは到底書きつくせないほど多くありますので、ぜひ MongoDB ODM を触っていただけたらと思います。

MongoDBもSymfony2も楽しいよ!

こうやったらいいよ!とか、こうじゃない?とか、そういったツッコミがありましたら、是非、 @madapaja もしくは、コメントで教えてください!



Symfony Advent 2010

Symfony Advent 2010では12月1日から12月24日までを使って日替わりでsymfonyでイイなと思った小さなtipsから内部構造まで迫った解説などをブログ記事にして公開していくイベントです。

日本Symfonyユーザー会
Symfony アドベントカレンダー2010

※Syfony Advent 2010はsymfony好きな有志で集まったチームです。