Github認証を使う

dietcake advent calendar 2014 の22日目です。
Google認証を使ってみるの仕組みを少し応用すると、githubアカウントを認証に利用する事もできます。

https://github.com/dietcake/dietcake-showcase/tree/github_auth

基本的には、google認証と同じ手順で設定するだけでOKです。

$ cp controllers/github_auth_controller.php YOURPROJECT/app/controllers/
$ cp models/github_auth.php YOURPROJECT/app/models/
$ cp config/github.php YOURPROJECT/app/config/
$ echo "require_once __DIR__ . '/github.php';" >> YOURPROJECT/config/core.php

config/github.php 内で設定しなければいけない値は以下の4つです。

  • GITHUB_CLIENT_ID APIのクライアントIDを入力します
  • GITHUB_SECRET APIのsecretを入れます
  • GITHUB_ALLOW_DOMAINS ログインしたユーザのメールアドレスに含まれるドメインを利用してアクセス制御を行います。
  • GITHUB_ALLOW_EMAILS ログインしたユーザーのメールアドレスを利用してアクセス制御を行います。

google認証と比較するとgithub認証が使えるポイントが限られてきそうな気がしますが、エンジニア向けサービスの開発を検討している場合は使ってみてください。

シンプルなbatchを書く

dietcake advent calendar 2014 の21日目です。
そろそろ何を書いて何を書いてないのか曖昧になってきた気がします…。

Webアプリケーションを開発していく上で、batch処理の実装が必要になるケースは非常に多いと思います。
簡単なbatch処理であれば、以下の手順で実現できます。

まず最初に、エントリポイントを用意します。 ユニットテストを書く で作った bootstrap.php と
同じ感じで、 webroot/index.php を編集して用意します。appディレクトリの下に、 console ディレクトリをほって、その下に以下のファイルを配置してください。

app/console/console.php

<?php
/**
 * DietCake のコンソールアプリケーション用の初期化処理
 *
 */
define('ROOT_DIR', dirname(dirname(__DIR__)).'/');
define('APP_DIR', ROOT_DIR.'app/');

require_once ROOT_DIR . 'vendor/autoload.php';
require_once ROOT_DIR . 'vendor/dietcake/dietcake/dietcake.php';
require_once CONFIG_DIR . 'bootstrap.php';
require_once CONFIG_DIR . 'core.php';

次に、batchファイルを作ります。例えばデータを全部消すバッチならこんな感じ。

app/console/truncate_all.php

require_once dirname(__FILE__) . '/console.php';
ini_set('display_errors', 'On');

$db = DB::conn();
$db->query(’TRUNCATE TABLE user;');
$db->query(’TRUNCATE TABLE log;');

SimpleDBIは、DB::conn()でどこからでもコネクションを拾ってこれるので、こういう雑な処理を書くときには力を発揮します。
上記の例では、DBクラスを直接よんでいますが、console.php 経由で bootstrap もロードしているので、モデルクラスも呼び出す事ができます。
アプリケーション側で実装した機能をバッチ処理でそのまま使いたい場合はモデルクラスのメソッドを実行するとよいでしょう。

エラーハンドリングや、ログ処理まで含めて真面目にやるのであれば、もう少し全体的に処理を加える必要がありますが、
とりあえず簡単にコマンドラインから実行したい場合であれば、このようなシンプルな形で呼び出す事ができます。

現時点では spongecake にはバッチ処理の骨組みが含まれていませんが、近いうちに追加しておきたいですね。

Google認証を使ってみる

dietcake advent calendar 2014 の20日目です。
簡単なアプリケーションを作るときは、Google認証を使って認証すると、面倒なアカウント作成画面を省く事ができるので便利です。
社内で Google Apps を使っている場合は、Google Apps側でアカウント管理がしっかりできていれば、退職時に
すみやかにサービスへのアクセスを遮断する事ができて良いです。

そこで、Google認証を実装するために必要な素材を dietcake-showcase にまとめました。
これを使えば既存の dietcake で書かれたシステムにGoogle認証を差し込む事ができます。

https://github.com/dietcake/dietcake-showcase/tree/google_auth/google_auth

リポジトリ内にあるファイルを、アプリケーションにコピーしたあと、composerで、google/apiclinet パッケージを入れれば準備完了です。
手順は以下のような感じです。

$ cp controllers/google_auth_controller.php YOURPROJECT/app/controllers/
$ cp models/google_auth.php YOURPROJECT/app/models/
$ cp config/google.php YOURPROJECT/app/config/
$ echo "require_once __DIR__ . '/google.php';" >> YOURPROJECT/config/core.php
$ cd YOURPROJECT
$ composer require google/apiclient

インストールが終わったら、callbackメソッド内で、ユーザー登録を行なう仕組みを追加してください。
devtoolでの処理内容をコメントアウトして残してあるので、これを参考にすると良いでしょう。

https://github.com/dietcake/dietcake-showcase/blob/google_auth/google_auth/controllers/google_auth_controller.php#L17

config 設定を見るとわかるのですが、GOOGLE_ALLOW_DOMAINS と、 GOOGLE_ALLOW_EMAILS を活用することで、特定のユーザーや組織だけを対象にする事ができます。
Google Appsで運用している場合は、そのドメイン名を GOOGLE_ALLOW_DOMAINS に設定する事で、社内のユーザーだけが利用できるようになります。

Sessionの使い方

dietcake advent calendar 2014 の19日目です。
このアドベントカレンダーをやって良かったなと思うのは、外に公開されてる情報だけを拾っていくと、
当然入れたと思った機能が入ってない事に気づいたりして、結果的に spongecake のブラッシュアップができる事ですね。

そんなわけで、今日は追加するのを忘れていたシリーズのSessionについて紹介します。
つい昨日アップデートした spongecake 1.0.3 から Sessionクラスが追加されています。
Sessionを利用する場合、core.phpの中にある、

ini_set('session.auto_start', 0);

ini_set('session.auto_start', 1);

にするだけです。具体的なSessionデータのやりとりは、Sessionクラスを利用します。Sessionクラスの実装はこれだけ。
ほんとに関数をラップしただけです。

<?php
class Session
{
    public static function set($key, $value)
    {
        $_SESSION[$key] = $value;
    }

    public static function get($key, $default = null)
    {
        return isset($_SESSION[$key]) ? $_SESSION[$key] : $default;
    }

    public static function delete($key)
    {
        unset($_SESSION[$key]);
    }

    public static function destroy()
    {
        session_destroy();
    }
}

FuelPHPなんかでは、自前でmemcache session storage driverを自作したりしていますが、
dietcakeは、標準のPHP側でサポートされているsession_handlerを極力使うべきだと考えており、
そのためフレームワーク側ではSessionStorage系の機能を一切実装していません。
session_start()を呼ばず、session.auto_startを利用しているのも、
できる限りセッション制御を自分達でやらない事というメッセージを伝えるためです。

もしロードバランサー経由で複数台のサーバを利用するような自体になり、サーバをまたいだセッション共有が必要ということであれば、
php.iniを使ってsession_handlerを memcache などに変更してください。
そうする事で、アプリケーションがどのような仕組みで Session が動いているのか考えなくてすむようになります。

例外を使ってエラー判定処理を減らす

dietcake advent calendar 2014 の18日目です。
dietcake に限った話ではありませんが、PHPはバージョン5から例外を使えるようになっています。
老害ネタですが、PHP4の時代はエラーハンドリングといえば PEAR::isError で、何か実行するたびに、
if (PEAR::isError()) と打ち込んで結果の判定を行っていました。
もちろん処理によっては、実行直後に成功可否の判定を行なう事も必要なのですが、最初から復旧不可能である事がわかっている
処理については、例外を投げてあげる事で、判定をまとめる事ができます。

devtool における例を見てみましょう。

app/controllers/code_controller.php の show() メソッドを見てもらうと、以下の様な実装になっています。

    public function show()
    {
        $user = $this->start();

        $path = Param::get('p');

        $code_packs = CodePack::getAll($user);
        $code_pack = CodePack::get($user, $path);
        $codes = $code_pack->getCodes();

        $this->set(get_defined_vars());
    }

$user を作り、外部パラメータ p を元にCodePack::get() からデータを取り出し view に渡しています。
このコントローラーには、先ほど説明した if (PEAR::isError()) や、try-catch は存在しません。
ここで、外部パラメータpに存在しない値を与えるとどうなるか見てみましょう。

CodePack::get() は、 app/models/code_pack.php に定義されています。

    public static function get(User $user, $path)
    {
        $db = DB::conn();

        $row = $db->row('SELECT * FROM code_pack WHERE path = ?', array($path));
        if (!$row) {
            throw new RecordNotFoundException();
        }

        if ($user->id == $row['user_id']) {
            $row['writable'] = true;
        }

        $row['user'] = $user;

        return new self($row);
    }

pが存在しない場合は、DBからレコードを取り出せないため、 RecordNotFoundException が発生します。
この例外は、code_controller.phpではスルーされ、その上位の app/app_controller.php が受け取り、汎用エラー画面に飛ばします。

    public function dispatchAction()
    {
        try{
            try {
                parent::dispatchAction();
            } catch(Exception $e) {
                $log = sprintf('Exception:(%s)%s@%s %s', Session::getId(), $e->getFile(), $e->getLine(), $e->getMessage());
                $this->set('user', $this->start());
                error_log($log);
                throw $e;
            }
        } catch (PDOException $e) {
            $this->set('exception', $e);
            $this->render('error/database');
        } catch (RecordNotFoundException $e) {
            $this->render('error/not_found');
        } catch (PermissionDeniedException $e) {
            $this->render('error/permission');
        } catch (Exception $e) {
            $this->set('exception', $e);
            $this->render('error/unexpected');
        }
    }

この仕組みによって、コントローラー内でのエラー判定処理を減らし、全体の見通しをよくしています。

例外の仕組み自体はなかなか奥が深く、私自身もまだ完璧に使いこなせている感覚はありませんが、
1つのメソッドの中で、「 実行 -> 結果確認とエラー遷移 -> 実行 -> 結果確認とエラー遷移」 みたいになっている時は、
コードの内容に応じて例外を活用してうまく共通化できないか確認してみると良いですね。