December 11, 2011

FuelPHPでFacebookアプリを作ってみよう。実装編。

2011/12/11 追記
当初、ソース内にValidation::factory()と記述をしていましたが、近々廃止予定とのことです。
Validation::forge()を使うべきとご指摘を頂きました。有難うございます。
当記事の該当箇所について、修正してあります。


FuelPHP Advent Calendar 2011 11日目です。
@madmamor です。

前日の @kenji_s さんの
FuelPHP での Migration の使い方
でもご紹介頂いたとおり、今回は、Facebookアプリの実装編です。

尚、先にこちらをお読み下さるよう、お願いします。
FuelPHPでFacebookアプリを作ってみよう。準備編。

今回実装する機能は
* Facebookでのログイン/ログアウト機能
* Facebook情報のDB保存(バリデーション付き)
* Facebookのウォール投稿機能(バリデーション付き)
です。
各項にポイントを書いています。

では、一気にいきましょう。


【DBとテーブルを作成】
MySQLを使います。DB名はfuelfb。テーブル名はusers。とします。
尚、今回、Facebook情報はusersテーブルに保存するのみです。
検索して使用する。という処理は組み込みませんが、DB周りの機能紹介を兼ねて、記載します。
create database fuelfb;

create table fuelfb.users
(
    id int not null auto_increment,
    created_at int not null,
    updated_at int not null,
    facebook_id varchar(255) not null unique,
    facebook_name varchar(255) not null,
    facebook_link varchar(255) not null,
    primary key (id)
);
【ポイント】
idはFuelPHPが定める主キーカラム名です。

created_atはFuelPHPが定めるレコード作成時刻カラム名です。
updated_atはFuelPHPが定めるレコード更新時刻カラム名です。
created_atとupdated_atはデフォルトだとUNIXタイムスタンプとなります。
datetime型にすることも可能ですが、今回はint(UNIXタイムスタンプ)で。

その他のカラムは、Facebook認証時に取得するデータ用のカラムです。
ユーザ入力ではありませんが、外部データなので、制約は緩めにしてあります。


【ORMモデルを作成】
前述のusersテーブルに対するモデルです。

fuel/app/classes/model/user.phpを作成。
<?php

namespace Model;

class User extends \Orm\Model
{

    protected static $_observers = array(
        'Orm\Observer_CreatedAt' => array('events'=>array('before_insert')),
        'Orm\Observer_UpdatedAt' => array('events'=>array('before_save')),
        'Orm\Observer_Validation'=> array('events'=>array('before_save')),
    );

    protected static $_properties = array(
        'id',
        'created_at',
        'updated_at',
        'facebook_id' => array(
            'validation' => array(
                'trim',
                'required',
                'max_length' => array(255),
                'valid_string' => array('integer'),
            ),
        ),
        'facebook_name' => array(
            'validation' => array(
                'trim',
                'required',
                'max_length' => array(255),
            ),
        ),
        'facebook_link' => array(
            'validation' => array(
                'trim',
                'required',
                'max_length' => array(255),
            ),
        ),
    );
}
【ポイント】
今回は、Orm\Modelを継承したモデルとします。

$_observersで、様々な振る舞いを指定可能です。
今回は、created_at値、updated_at値の自動登録と、
insert or update前のバリデーション自動実行。を設定しています。

$_propertiesでカラム毎にバリデーションを定義可能です。

バリデーションは、"単一のStringを受け、Stringを返却するPHP関数"を指定可能です。
今回は、trimを設定しています。

$_propertiesには全てのカラムを記述して下さい。エラーとなります。


【ORMパッケージの有効化】
fuel/app/config/config.phpを修正
'packages'  => array(
    //'orm',
),

'packages'  => array(
    'orm',
),
【ポイント】
前述のORMモデルが使用可能となります。


【DB接続情報を設定】
fuel/app/config/development/db.phpを修正
<?php
/**
 * The development database settings.
 */

return array(
    'default' => array(
        'connection'  => array(
            'dsn'        => 'mysql:host=localhost;dbname=fuel_dev',
            'username'   => 'root',
            'password'   => 'root',
        ),
    ),
);

<?php
/**
 * The development database settings.
 */

return array(
    'default' => array(
        'type'            => 'mysql',
        'connection'    => array(
            'hostname'        => 'localhost',
            'port'            => '3306',
            'database'        => 'fuelfb',
            'username'        => 'root',
            'password'        => '',
        ),
    ),
);
【ポイント】
必要に応じて、接続情報は書き換えて下さい。


【ビューの修正】
fuel/app/views/index/index.phpを修正
<p>Index</p>

<?php if ($is_login): ?>
<a href="http://127.0.0.1/fuelfb/public/index/logout">Log out.</a>
<?php else: ?>
<a href="http://127.0.0.1/fuelfb/public/index/login">Login with Facebook.</a>
<?php endif; ?>

<?php if ($is_login): ?>
<?php echo(Form::open('index/index/'))?>
<?php echo(Form::textarea('message'))?>
<?php echo(Form::submit('submit','Post to Facebook'))?>
<?php echo(Form::close())?>
<a href="http://127.0.0.1/fuelfb/public/index/logout">Log out.</a>
<?php else: ?>
<a href="http://127.0.0.1/fuelfb/public/index/login">Login with Facebook.</a>
<?php endif; ?>
【ポイント】
ログインしていれば入力フォームとログアウトリンクを表示します。

ログインしていなければログインリンクを表示します。

判断基準の$is_loginは後述のコントローラでセットします。


【コントローラに機能を実装】
fuel/app/classes/controller/index.phpを修正
<?php
require_once APPPATH.'vendor/facebook-php-sdk/src/facebook.php';

class Controller_Index extends Controller_Template {

    private $fb;

    public function before()
    {
        parent::before();
        $this->fb = new Facebook(Config::get('facebook.init'));
    }

    public function action_index()
    {
        $this->template->title = 'Index » Index';

        $data = array(
            'is_login' => $this->fb->getUser()?true:false,
        );

        $this->template->content = View::forge('index/index',$data);
    }

    public function action_login()
    {
        exit('TODO : login');
    }

    public function action_callback()
    {
        exit('TODO : callback');
    }

    public function action_logout()
    {
        exit('TODO : logout');
    }

}

<?php
require_once APPPATH.'vendor/facebook-php-sdk/src/facebook.php';

use Model\User;

class Controller_Index extends Controller_Template {

    private $fb;

    public function before()
    {
        parent::before();
        $this->fb = new Facebook(Config::get('facebook.init'));
    }

    public function action_index()
    {
        $this->template->title = 'Index » Index';

        $is_login = $this->fb->getUser()?true:false;
        $data = array(
            'is_login' => $is_login,
        );

        if($is_login and Input::method() == 'POST')
        {
            $v = Validation::forge();
            $v->add('message', 'message')->add_rule('required');
            if(!$v->run())
            {
                Session::set_flash('notice', $v->errors('message')->get_message());
            }
            else
            {
                $message = $v->validated('message');
                try
                {
                    $res = $this->fb->api(array(
                        'method' => 'stream.publish',
                        'message' => $message,
                    ));
                    Session::set_flash('notice', 'complete!!');
                }
                catch (FacebookApiException $e)
                {
                    Session::set_flash('notice', $e->getMessage());
                }
                Response::redirect('index/index/');
            }
        }

        $this->template->content = View::forge('index/index',$data);
    }

    public function action_login()
    {
        $url = $this->fb->getLoginUrl(Config::get('facebook.login'));
        Response::redirect($url);
    }

    public function action_callback()
    {
        try
        {
            $me = $this->fb->api('/me');

            $user = User::find_by_facebook_id($me['id']);
            if(!$user) $user = new User;

            $user->facebook_id = $me['id'];
            $user->facebook_name = $me['name'];
            $user->facebook_link = $me['link'];

            $user->save();
            Response::redirect('/index/index/');
        }
        catch (Orm\ValidationFailed $e)
        {
            throw new Exception($e->getMessage());
        }
        catch (FacebookApiException $e)
        {
            throw new Exception($e->getMessage());
        }
    }

    public function action_logout()
    {
        $url = $this->fb->getLogoutUrl(Config::get('facebook.logout'));
        $this->fb->destroySession();
        Response::redirect($url);
    }

}
【ポイント】
以下の順を想定して、各メソッドのポイントを記載します。
(1) http://127.0.0.1/fuelfb/public/index/index/ にアクセス。
(2) "Login with Facebook."をクリック。
(3) Facebook認証をして http://127.0.0.1/fuelfb/public/index/index/ に戻ってくる。
(4) Facebookのウォールへ投稿。
(5) ログアウト。

■before()
Facebookオブジェクト($fb)の初期化。全アクションに対しての、事前処理みたいな感じです。

■action_login()
Facebook認証画面にリダイレクトします。
fuel/app/config/custom.phpのfacebook.loginには、
認証後のリダイレクト先とパーミッション設定が設定してあるはずです。

■action_callback()
Facebook認証後のリダイレクト先になります。

$me = $this->fb->api('/me');
自身のFacebook情報を取得しています。

$user = User::find_by_facebook_id($me['id']);
usersテーブルに対してFacebookのIDで検索を行なっています。
find_by_[カラム名]はFuelPHPのORMが標準に提供する検索機能です。

$user->save();
前述のfind_by_facebook_idでデータがヒットしなければinsert。
ヒットすればupdateを行います。

Response::redirect('/index/index/');
action_index()へリダイレクトします。

catch (Orm\ValidationFailed $e)
ORMモデルにバリデーションルールを書きましたが、それに反する場合、ここに入ります。
今回はユーザ入力データでは無いので、実質発生しないはずです。
なので、画面表示はしていません。

catch (FacebookApiException $e)
$this->fb->api('/me'); でエラーが発生した場合、ここに入ります。
その他、FacebookのAPIでエラーが発生した場合も、FacebookApiExceptionが発生します。

■action_logout()
Facebookのセッションを破棄してリダイレクトします。
リダイレクト先はfuel/app/config/custom.phpのfacebook.logout.nextに書いてあるはずです。

■action_index()
if($is_login and Input::method() == 'POST')
フォームに入力してsubmitボタンを押すと、この中に入ります。

$v = Validation::forge();
$v->add('message', 'message')->add_rule('required');
if(!$v->run())
{
    Session::set_flash('notice', $v->errors('message')->get_message());
}
必須チェックを行なっています。
入力が空だった場合、エラーメッセージを表示します。
具体的には、fuel/app/views/template.phpの
Session::get_flash('notice')
の箇所で表示されます。

$message = $v->validated('message');
try
{
    $res = $this->fb->api(array(
        'method' => 'stream.publish',
        'message' => $message,
    ));
    Session::set_flash('notice', 'complete!!');
}
catch (FacebookApiException $e)
{
    Session::set_flash('notice', $e->getMessage());
}
Response::redirect('index/index/');
バリデーション完了後のデータを取得して、Facebookのウォールに投稿(stream.publish)します。
FacebookApiExceptionについてはaction_callback()に記載した通りとなります。
最後にリダイレクトしているのは、リロード対策です。

■その他
再現性が乏しいのですが、Facebookの証明書エラーが発生するケースがありました。
その場合、ブラウザの関連するキャッシュをクリアして下さい。
また、XAMPPのApacheを再起動してみて下さい。
これで、私の場合、エラーが発生しなくなりました。


以下、画面キャプチャを掲載しておきます。

まずは、この画面が表示されるはずです。
Login with Facebook.をクリックして、Facebook側に飛びます。

Facebook側の認証画面です。
認証が済むと、今回作成したプログラム側に戻ります。

正しく実装されていると、この画面に戻るはずです。
textareaとpostボタンが表示されます。
postが正常に行われると、"complete!!"と表示されます。

この後、ログアウトをして、最初の画面に戻れば、一通りOKと思います。


以上、実装編でした。

fuel/app/classes/welcome.php等のゴミファイルが残っていたり
fuel/app/config/config.phpのbase_urlが空だったり
その他、色々とやり残したことはありますが、ミニマムな手順は、こんな感じと思います。

有難うございました。


明日の12日目は @mataga さんです。
FuelPHP動作実験 - 実験くんソースをModulesに閉じ込めてモジュール分割してみる。


P.S.
次回は、軽めのネタでいきます。。。

No comments:

Post a Comment