Tags: 技術日記

技術日記Lamblog
フルサーバレスCMS: Lamblog 開発中Edit

概要

  • 一番安いコースのさくらのレンタルサーバーでずっと動かしていたWordPressを撤収した
  • 安いサーバーで動かすWordPressは動作が重くて使いたくならないね
  • ブログ以外の自分のサイトを、静的なサイトとしてFirebase上に置いたまま放置していた
  • git管理してコマンド一発で更新できるようにしたんだけど、やっぱりオンラインで更新できないと面倒くさいね
  • 何が欲しいのか考えてみた
    • サーバー管理したくない→サーバレスがいい
    • オンラインで気軽に更新したい→CMSにする
    • 重いのはいや→軽快に動作する環境を選ぶ
    • どうせ大してアクセスなんてないんだから0円スタート無料枠ありの従量課金がいい
  • というものを作って自分のサイトを移行することにした

Lamblogの構成

構成イメージ

AWS Lambda with Ruby 2.5

実行環境はAWS Lambdaです。

ちょっと前までは、周辺のミドルウェアが一通りセットになっていて、無料スタートしやすいFirebase押しだったんですが、Firebase Functionsは(日本から使うと)微妙に重いし、ちょっと古いnode.jsが標準でそれ以外を使おうとするとさらに制約が上乗せされるし、深く使おうとするとFirebaseからはみ出てGCP側のコンソールでごにょごにょする感じが分かりにくいし、その辺にうんざりして一押しから脱落しました。

一方のAWS Lambdaはいろいろな言語(特にRuby)が使えるし、(ちゃんとメモリを割り当てれば)日本で使っても軽快な動作だし、Firebase Functionsみたいに中途半端な隠蔽がないし(その分、最初からフルセットで見えているから初めて扱うときの敷居は高そう)、いまいち使いにくいAPI GatewayのREST API以外に、ALB直接マウントやAPI GatewayのHTTP API(beta)とか使いやすいHTTPマウントの方法が揃ってきて、だいぶ重しが取れました。

Rubyはまだまだ初心者に毛が生えた程度な習熟度なんですが、普通に動くコードを書けるくらいには使えるようになったんで、まあ練習がてら。

API Gateway HTTP API

LambdaをHTTPで公開するのに一番便利なのは、ALBに直接マウントしちゃうやり方だと思うんですよね。

ただ、ALBを作っちゃうと時間制の利用料金が発生しちゃって、個人でお手軽に運用するにはちょっとなーというのが唯一の欠点でした。まあALB 1個動かしたところで、月2〜3000円程度しか発生しなかった気はするし、1個のALBで複数のサービスをマウントできるから、税金的に払ってしまうのも悪くはないんですが。

API Gateway REST APIはAPI向けなんで普通のサイトをあの上で無理矢理公開する気にはまったくなれなかったんですが、最近HTTP APIというのが(betaだけど)でているんですね。で、ちょっと触ってみたらかつてのREST APIの欠点がだいぶなくなっていて、これなら個人が気軽にHTTP APIをサーバレスで公開するためのミドルウェアとして使いやすそう。

ただ問題は、API Gateway HTTP APIはHTTPSのサイトしか運用できないっぽいんですよね。まあ普通はそれで全然問題ないんですけど、過去のサービスとかを移行する場合とか、HTTPでのアクセスがあることもある程度想定しておかなければならないんで、せめてHTTP to HTTPSのリダイレクタくらいは仕込んでおきたいじゃないですか。ALBなら一瞬なのに。

CloudFront

というところで思いついたのがCloudFront。

Lambda+API Gateway HTTP API構成ならば、個人レベルではまったく問題ないレベルのハイパフォーマンス&スケーリング能力を持っているから、CloudFrontなんていらないよねーと思っていたんですが、こいつってHTTP/HTTPS両対応な上にバックエンド側へHTTP/HTTPS振り分けする能力も持っている。しかもCDNとしても使える(ついでかよ)。

CloudFrontも無料枠があるんで基本無料+従量課金で使えるから、こいつを使えばいいんじゃね。

と思ったら、実はCloudFrontの無料枠は最初の1年だけだった。くそー、この最初の1年だけ無料枠があるやつ分かりにくいなー。まあ完全従量課金でも個人レベルではたいした額にならないからいいんだけど。

あと、Lambda→API Gateway HTTP API→CloudFrontは、最終的には思った通りに動いたんだけど、結構はまりどころが多かった。ポイントとしては、

  • API Gateway HTTP APIのちょっとバギーな挙動に気をつける
  • CloudFrontの裏側は全体的にHTTPSで正しくつながるようにセットアップする
  • セッションCookieとかを維持するためにはCloudFront、API Gateway HTTP API、Lambdaまで正しくドメイン名(HTTP_HOST)が伝わらないといけない

というあたり。

Amazon S3

コンピューティング能力の話ばかり語ってきましたが、CMSとか使う際にはもう一つ重要なリソースがありますよね。もう一つというか、場合によってはコンピューティング能力よりも重要かも知れない。そう、データベース。

が、今回はデータベースは使っていません。だって個人のサイトで必要なデータなんて、今どきのコンピューティングパワーを考えれば、専用のデータベースアプリケーションが必要なほど複雑で巨大なデータなんて使わないし。

というわけで、データストレージはAmazon S3を使い、そこにすべてのデータを置いておくことにしました。ただ、さすがに毎回記事ファイル一つ一つをS3 APIごしになめて検索したのでは、ネットワーク的なスピードに問題が出そうなので、普段は1ファイルにまとめたインデックスファイルを使って必要な記事を検索し、その後詳細な記事ファイルを取得する感じにしておきました。これなら個人のサイトが扱う程度のデータ量ならば余裕で捌けるでしょう。

ちなみに私はむかーしむかし、PHPでブログツールを作る書籍を出したんですが、そのときもデータベースを使わずに、ローカルファイルシステム上に記事ファイルとインデックスファイルを置いて捌く構成にしました。三つ子の魂百までってやつですね(?)。ちなみにローカルファイルシステム上だったら、インデックスファイルなんてなくてもかなりのスケールまで動くとは思いますが。

ドッグフード食べながら開発してみます

ひとまず大まかに動くようになったんで、

の3サイトをlamblogに入れ替えてみました。

特にここは過去のwordpressからエクスポートしたデータをインポートして、全部で3000記事くらい突っ込んであるんで、負荷テスト的にもちょうどいいんじゃないでしょうかね。

そういえばかつてブログツールと言えば、RSS、Trackbackとかついているのが普通でしたけど、今更あんなの作る必要あるんでしょうかね。なんか別になくてもいいかなー感。

ソースコードは、 https://github.com/heavymoons/lamblog のdevelopブランチに公開しています。ドキュメントは全然まとめていないし、周辺ツール類もまだ作りかけなんで、私以外がセットアップするのはかなり難易度が高いとは思いますが。

Published At2019-12-31 01:07Updated At2020-01-01 03:44

技術日記
Twe-td client for iPhoneを作ったEdit

というか、作ったのはTwe-td client for Androidよりも前なんだけど、ようやく公開されたんで。

機能としてはAndroid版とほぼ同じで、画面左右端あたりをタップするとアイテムの前後移動。画面下にある言語とタイプの左右の「<」「>」をタップすると、言語やタイプが切り替わる。画面右上にあるゴミ箱をタップするとそのアイテムが非表示になる。

Web版だと過去の日付を指定して、「○○語で○月×日に話題になった○○」とかを見ることができるけれども、アプリ版は「○○語で現在話題になっている○○」しか見ることができない。

単にユーザーインターフェースをシンプルにするために、それだけに機能を絞っているだけで、作れば簡単に作れるんだけど。

ところで、Android版にもiOS版にもGoogle Analyticsのアプリ版を仕込んでいるんだけど、プラットフォームごとにGoogle Analyticsのプロパティをそれぞれ別に設定してある。けど、同じアプリの別プラットフォームバージョンを作る場合って、別プラットフォームでも同じプロパティIDを使うのが普通なのかな?

せっかくGoogle Analyticsがその辺を識別して集計してくれるのに、わざわざ別IDに分けてしまうのはアホらしい気がする。と、公開されてから思った。

それにしても、Androidアプリの公開までのスピード感と比べると、iOSアプリのスピード感のなさは、自分で軽量の(作成に一週間もかからない)アプリを作るときには、致命的なくらいきついな。

Androidアプリは実験的なWebサービスを公開するのと大して変わらないスピード感でいけるけど、iOSアプリの10日近いタイムラグは、公開されるまで待っている間に、次のバージョンを開発するモチベーションがなくなるくらいだ。

こんなに間が空くと、既存のアプリをアップデートするよりも、別の新しいアプリを作ったほうがましな気分になってしまう。

Published At2014-03-18 10:53Updated At2014-03-18 10:53

技術日記
Twe-td client for Androidを作ったEdit

Twe-td(http://twe-td.net/)のAndroidクライアントアプリを作りました。Google Play Storeからダウンロードできます。

howto

機能としては単純で、起動すると前回表示した言語/種類の最初のアイテムが表示されます。画面左右の端にある「<」「>」で表示するアイテムを前後に切り替えることができます(「<」「>」が表示されている部分だけでなく、画面端のほぼ全体でタッチに反応します)。

画面下側にある「Engilish」「Tweets」の部分が、現在表示しているアイテムの「言語」「種類」の設定で、その左右にある「<」「>」で切り替えることができます。種類は「Tweets(ツイート)」「Movies(ムービー)」「Pictures(写真)」「Goods(商品)」「All Types(すべてのURL)」が選べます。

スマートフォンでWeb版を表示すると、一度に表示するアイテム的に厳しい端末もあるのですが、アプリ版だと一度に1アイテムずつを素早く切り替えることができるので、たいていの端末で軽快に動作するはず。

iPhone版も1週間くらい前にAppleStoreに申請に出しているんだけど、いまだにWaiting For Reviewのままだなー。iPhone版を作った後に作り始めたAndroid版の方が先に公開されるとは。

Published At2014-03-15 15:37Updated At2020-01-01 04:24

技術日記
Twe-td - what is tweeted in the world ? -を作りました。会社を辞めましたEdit

Twe-td(ツイーティド) [http://twe-td.net]というサービスを作りました。

これは何かというと、Twitter上でつぶやかれているツイートに含まれるURLを収集・解析し、世界中でどんなWebサイト、ツイート、ムービー、写真、グッズなどの話題が注目を集めているのかを、シンプルなインターフェースでみることができるサービスです。

「世界中で」というところがポイントで、Twe-tdでは収集した情報をつぶやかれた「言語」を軸に区分しています。

たとえば、トップページから「Let's go!」で飛んだ先は「表示する種類:ツイート(Tweets)」+「表示する言語:全言語(All languages)」となっており、この記事を執筆している時点では、以下のような内容になっています。

表示する内容は、画面上下にあるナビゲーションから変更することができます。

twe-td-selector

一番左が表示する種類の選択肢で、「All(全種類)」「Tweets(ツイート)」「Movies(ムービー)」「Pictures(写真)」「Goods(グッズ)」から選べます。

真ん中が、対象となる言語の選択肢で「All languages(全言語)」「English(英語)」「Japanese(日本語)」を始め、Twitter上でツイートされている様々な言語が選択できます。一応投稿されているツイートが多い順に並んでいます。

一番右は対象の日付(GMT)となります。データ収集プロセスを動かし始めたのは2014-02-20くらいなのですが、いろいろ試行錯誤があり、まともにデータが取れ始めているのは2014-03-02以降となっています。

たとえば「表示する種類:ツイート(Tweets)」+「表示する言語:日本語(Japanese)」にしてみると、以下のような内容になります。

これをスペイン語(Spanish)のムービー(movies)に変えると、以下のようになります。

「ツイート」などは知らない言語だと内容がさっぱりわかりませんが、「写真」や「ムービー」ならば違う言語でもなんとなくわかるもの。言語圏の違いによって、流行にどのような違いが出てくるのか、わからないなりに眺めていても楽しいものです。

また、もうちょっと情報がたまってきたら、同じ言語でも「何月何日頃にはどういうものが流行っていたっけ?」みたいな探索にも使えるかもしれません。

ちなみに、ツイーター上に流れる大量のツイートから無作為にデータを収集しているため、収集結果にはいわゆる「公序良俗に反するもの」が含まれている場合があります。その場合、各URLのそばに表示されているゴミ箱マークをクリックしてもらえると、そのコンテンツを画面から削除することができます。

この削除行為は運営側にも通知が来るようになっていて、ユーザー削除が多いコンテンツはサーバー側でも削除されますので、やばそうなコンテンツを見かけたらまめに削除していただけると助かります。

「公序良俗に反するもの」の規定は明確に定めていませんが、ひとまずは「日本の法律的にNGなもの」は削除していこうかと思っています。また、「自動生成アカウントによる大量操作」「釣りアプリによる権限奪取を使った大量操作」なども、スパムとして削除対象にするつもりです。

まだ開発し始めたばかりですが、自分で使ってみてなかなか面白そうな感じにできてきつつあるので、興味を持った方は是非使ってみてください。

あと私事ですが、長い間お世話になった株式会社ジョルスを3月一杯で退職することになりました。在職中にお世話になった方々、今までありがとうございました。

Published At2014-03-03 15:55Updated At2020-01-01 04:25

技術日記Laravel4PHP
LaravelをWindows環境で使うEdit

最近メインマシンをMacbookに移行したんだけど、Laravel4で作ったアプリケーションをWindows環境で動かさなければならない要件があったんで、Windows環境にセットアップして問題ないかどうか試してみた。手元にあるのがほぼまっさらなWindows8.1(on Parallels)だったので、それをベースに。

まずXAMPP for Windows をインストール。今だとXAMPP 1.8.3(Apache 2.4.7/MySQL 5.6.14/PHP 5.5.6)だった。

インストール後、XAMPPコントロールパネルからApacheを起動して、http://localhost/にアクセスできるかどうか確認。

続いてLaravelをcomposerを使ってインストールするために、まずはPHPにパスを通す。コントロールパネルのシステムの詳細設定-環境変数から、システム環境変数のPathにPHPのパス(C:\xampp\php)を追加する。

試しにコマンドプロンプトを起動して、php -iとか実行してちゃんとPHPにパスが通っているかどうかを確認。すでにコマンドプロンプトが起動済みだった場合、環境変数の変更を反映させるためにコマンドプロンプトを再起動。

続いて、composerをインストール。コマンドプロンプトで、

cd C:\xampp\php
php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));"

を実行してcomposer.pharをダウンロード。これだけだと使い勝手が悪いので、C:\xampp\php内にcomposer.batファイルを作成。内容は、

@echo off if "%PHPBIN%" == "" set PHPBIN=C:\xampp\php\php.exe "%PHPBIN%" "C:\xampp\php\composer.phar" %*

にしておく。これでコマンドプロンプトから、

composer update

みたいな感じでcomposerを実行できる。

でcomposer create-project laravel/laravelしようと思ったら、途中でgitがなくてこけた。gitも必要だったっけ。

Git for Windows(msysgit)をインストール。コマンドプロンプトから使いたいんで、インストールオプションで実行スタイルを選択するところで、「Run Git from the Windows Command Prompt」とか、gitにPathを通してくれるオプションを選択する。

もう一回コマンドプロンプトを再起動。Laravelアプリケーションプロジェクトを作りたいディレクトリを適当に作成する。

mkdir c:\laraveltest
cd c:\laraveltest

あとは、

composer create-project laravel/laravel .

今度はエラーも出ずにプロジェクトの作成に成功。Apacheからこのプロジェクトディレクトリを参照したい場合は、C:\xampp\apache\conf\httpd.confから、

DocumentRoot "C:/xampp/htdocs"
<Directory "C:/xampp/htdocs">

となっている部分を、

DocumentRoot "C:/laraveltest/public"
<Directory "C:/laraveltest/public">

なんて変えてから、Apacheを再起動すると、http://localhost/ で今作ったプロジェクトにアクセスできる。まあC:\xampp\apache\conf\extra\httpd-vhosts.confとC:\Windows\System32\drivers\etc\hostsを使って、バーチャルホストにしておいた方が便利だろうけど。

前にWindows環境でLaravelを動かしたときは、mcryptとかのPHPモジュールを有効にしたり、timezone設定を追加したりとかしないといけなかったような気がするんだけど、今はそういう問題はないんだな。

このくらい手軽に動くなら、ピュアPHPで作れるようなアプリならば、Windows環境でも作りやすそうだ。

Published At2013-12-13 18:16Updated At2019-12-30 15:03

技術日記Laravel4PHP
Laravel 4.0.10からLaravel 4.1.5にアップグレードEdit

PHPフレームワークLaravelの4.1がリリースされた。手元に作りかけのLaravel 4アプリケーションがあったんで、今後のことを考えてとっととLaravel 4.1にアップグレードすることにした。手元のLaravel 4フレームワークのバージョンは4.0.10。

アプリケーションのスペックとしては、

  • 使用DBはsqliteのみ。ちょっとしたデートストア程度にしか使っていない。
  • JeffreyWay / Laravel-4-Generatorsで生成したマスタメンテ用Scaffoldページがいくつか。
  • JavaScriptアプリケーションのコンテナWebページが5ページほど。サーバーロジックはほとんどないのでroutes.phpでView::make()しているだけ。
  • メインのJavaScriptアプリケーションから呼ばれるJSON APIが10個ほど。DBとのやりとりがほとんどなので、routes.php内で直接Eloquent ORMを使って処理をして、JSONで結果を返すだけ。
といった感じ。小規模だしLaravel 4の機能は最低限しか使っていないから、移行のハードルは低い。

まずは、4.0から4.1へのアップグレードガイド日本語訳にざっと目を通す。私はLeanpubで日本語ドキュメントを買ってるんで、PDFで読んでるけど。

変更点としては、いくつかのファイルの差し替え、設定ファイル関連の変更がいくつか、コントローラの変更がちょっと。パスワードリマインダーも変更されたらしいけど、使ってないから内容は未チェック。作業ステップは一桁程度か。

まず、composer.jsonのrequireセクションにあるlaravel/frameworkのバージョンを4.0.から4.1.に変更。アップグレードガイドの手順とは違うけど、まずはそのままcomposer updateしてみる。

大量のremove&installの後で、Fatal Errorで終了。エラーは、

Error Output: PHP Fatal error: Call to undefined method Illuminate\Foundat
 ion\Application::redirectIfTrailingSlash() in /Users/ishinao/NetBeansProjec
 ts/webgldance/laravel/bootstrap/start.php on line 16

なんで、アップグレードガイドにある「Trailing Slashリダイレクトの削除」に引っかかったらしい。bootstrap/start.phpから,

$app->redirectIfTrailingSlash();

を削除。もう一度composer updateしたら、今度はエラーなしで正常終了した。

この段階で、試しにWebアプリケーションルートをブラウザで開いてみたところ、200 OKは返るけどコンテンツは何も返らない状態だった。

ということで、アップグレードガイドに書いてある作業を一つずつつぶしていくことにする。

まずはWebアプリケーション起動ファイルを新バージョンの内容に置き換え。public/index.phpを https://raw.github.com/laravel/laravel/master/public/index.phpからダウンロードして差し替え。

同様に、artisanコマンドの内容もhttps://raw.github.com/laravel/laravel/master/artisanからダウンロードした新バージョンの内容に置き換え。

続いて、設定ファイルを新バージョンの内容に追随させていく。

app/config/app.phpの内容をhttps://raw.github.com/laravel/laravel/master/app/config/app.phpの内容を元に書き換える。変わっているのはprovidersとaliasesのところなので、自分で追加/編集した内容がなければ、丸ごと新しい方で置き換える。自分で追加した内容があればそれを残しつつ。

新しく追加されたhttps://raw.github.com/laravel/laravel/master/app/config/remote.phpをapp/config/remote.phpとして保存。

セッション設定ファイル app/config/session.phpに

'expire_on_close' => false,

を追加。あとで、app/storage/logsを見てみたら、

'Undefined index: expire_on_close' in vendor/laravel/framework/src/Illuminate/Session/Middleware.php:171

が残っていたんで、これが起動時に画面が表示されない原因だったっぽい。

新機能の失敗したジョブキューからの復帰用設定を追加。app/config/queue.phpに

  'failed' => array(
    'database' => 'mysql', 'table' => 'failed_jobs',
  ),

を追加。

これでWebアプリケーションルートにブラウザからアクセスしてみたところ、無事画面が表示された。

他にもコントローラ関連の修正項目が書かれていたけれども、Generatorsで自動生成したコントローラクラスは、特に修正する必要がなかった。

アップグレードしたバージョンは4.1.0かと思っていたら、php artisanしてみたら、

Laravel Framework version 4.1.5

になっていた。

追記@2013/12/14

アップグレードガイドがアップデートされ、アップグレード手順で一つ重要な内容が欠けていたので、その部分を追記。

$app->redirectIfTrailingSlash() 相当の処理を.htaccessで行うようになったため、bootstrap/start.phpからそのコードを削除したにもかかわらず、上記手順では.htaccessをLaravel 4のままで使っている。public/.htaccessをhttps://raw.github.com/laravel/laravel/master/public/.htaccessに差し替える必要がある。

ちなみに修正内容として、以下の内容が追加されている。

# Redirect Trailing Slashes...
RewriteRule ^(.*)/$ /$1 [L,R=301]

また、Laravel Framework本体もアップデートされており、composer updateすると、

Laravel Framework version 4.1.8

になっていた。アップデートペース、はやっ。

Published At2013-12-13 15:19Updated At2020-01-01 15:39

技術日記Laravel4PHP
Laravel 4のエラーメッセージとフォームエラーEdit

Laravel 4では、

Redirect::to()->withErrors()

をすると、自動的にリダイレクト後のビュー内で$errorsに展開されるなど、エラーメッセージは特別な扱いになっている。このエラーメッセージは共通レイアウトなんかで、

@if ($errors->count() > 0)
<ul class="error">
@foreach ($errors as $error)
<li>{{{ $error }}}</li>
@endforeach
@endif

なんて感じに表示するのが普通だろうけど、フォームエラーなんかで、入力欄のそばにエラーメッセージを表示したい場合は、どういうふうに使い分けるといいんだろう。

(エラー表示コードを含む)共通レイアウトは使わず、フォーム表示ビューの方で、

{{ Form::open() }}
{{ Form::label('name') }}
{{ Form::text('name') }}
{{ $errors->first('name', '<p class="error">:message</p>') }}
{{ Form::close() }}

みたいな感じに書くのがいいのか。でもそうなると、(エラーメッセージ表示をしたい)フォームを含むかどうかで、レイアウトファイルを(エラー表示コードを含む物と含まない物で)切り替えることになるのが、あんまり気持ちよくない。

あるいは

[source language="php"] Redirect::to()->with('formErrors', $validator->messages) [/source]

なんて感じで、標準エラーメッセージを使わずに、独自のフラッシュセッションに保存して、

[source language="php"] if (Session::has('formErrors')) { View::share('formErrors', Session::get('formErrors'); } [/source]

とかやる方がいいんだろうか。これはこれでありだろうけど、せっかく標準エラーメッセージ機能があるのに、別でハンドリングするのも今ひとつのような気もする。

標準エラーメッセージを使いつつ、フォームビューの方でエラーメッセージを表示した場合は、何らかの方法でフラグをセットして、レイアウトの方のエラーメッセージを抑止するという方法も考えたんだけど、(コンテンツ)ビューから(レイアウト)ビューにデータを渡す、ほどよい方法が見当たらないんで、ペンディング中。

$errorsの実装クラスであるMessageBagにMessageBag::clear()とかが実装されていれば話が早かったんだけど、MessageBagは値を追加したりマージしたりはできても、削除とかリセットとかはできない模様。

今のところ、独自フラッシュセッションを使う方法が一番ましかなーとは思っているんだけど、もっときれいな方法があるという方、情報をお待ちしております。

追記@2013/07/18)

って話は、Laravel 4のwithErrors()は、レイアウトファイルなどを使って、サイト全体で共通表示するフラッシュメッセージ的なものと捉えた上で書いていたんだけど、なんか違うような気がしてきた。

withErrors()で渡すエラーは、共通フラッシュメッセージ的なものではなく、あくまでも特定機能(ページ)でのエラーを手軽にハンドリングするための入れ物なのか? 共通フラッシュメッセージは、with()とかを使った独自処理として書くべき?

たとえば、Laravel-4-Bootstrap-Starter-Site/app/views/notifications.blade.php at master · andrew13/Laravel-4-Bootstrap-Starter-Siteなんかを見ると、共通フラッシュメッセージ的な物としては、success、warning、error、infoを直接Session::flash()(=with())経由でやりとりして表示し、withErrors()(=$errors)はフォームエラー表示専用として扱っているようだ。

withErrors()はあくまでフォームエラー専用の入れ物であって、フラッシュメッセージとは別物、というのが妥当なアプローチなのかな。なんかフォームエラー専用の代物を、フレームワーク標準で自動的にビュー変数にまで代入しちゃうのは、やり過ぎ感が否めないんだけどなー。

Published At2013-07-17 19:07Updated At2020-01-01 15:40

技術日記Laravel4PHP
Laravel 4でfacebookログイン機能も作ってみるEdit

Twitterログイン機能を作ってみたついでに、facebookログイン機能も作ってみる。ただfacebookはあんまり好きじゃないんで、APIのドキュメントを眺めてみたことがある程度しか知識がないから、Laravel 4以前に手探りで実装することになるんだけど。

まず、facebookのPHP SDKをインストールする。composer.jsonのrequireに

    "facebook/php-sdk": "dev-master"

を追加して、composer updateするとfacebook PHP SDKがインストールされる。

続いて、facebookアプリケーションの登録。facebook developersで新しいアプリを作成し、アプリのIDとシークレットキーを取得しておく。その設定は、app/config/facebook.phpとして、

<?php

return array(
    'appId' => '[アプリID]',
    'secret' => '[シークレットキー]',
);

って感じで保存しておくと、Laravel 4アプリ内ではConfig::get('facebook')で取得できる。このあたりの設定ファイルの扱いもLaravel 4はシンプルでいいね。環境(本番/開発など)ごとにオーバーライドさせたければ、環境名のディレクトリを掘ってその中に入れればいいだけだし。

続いてDBの準備。Twitterログインと同じように、usersテーブルに直接facebookのユーザー情報を保持する設計にする。

        Schema::create('users', function($table) {
            $table->biginteger('id')->unsigned();
            $table->primary('id');
            $table->string('name');
            $table->string('access_token');
            $table->timestamps();
        });

facebookのユーザーIDは桁数が多いっぽいんでbigintegerにしておいた。あとは名前とアクセストークンを保存するようにしておく。

ログインルーティングは以下のような感じ。

Route::get('login', function() {
    $facebook = new Facebook(Config::get('facebook'));
    $config = array(
        'redirect_uri' => url('/login/callback'),
    );
    return Redirect::to($facebook->getLoginUrl($config));
});

facebook側の認証ページにリダイレクトさせ、その結果をコールバックURLに返してもらう。続いてコールバックのルーティング。

Route::get('login/callback', function() {
    $code = Input::get('code');
    if (strlen($code) == 0) {
        return Redirect::to('/')->with('message', 'ログインできませんでした。');
    }

    $facebook = new Facebook(Config::get('facebook'));
    $user_id = $facebook->getUser();

    if ($user_id == 0) {
        return Redirect::to('/')->with('message', 'ログインできませんでした。');
    }

    $user = User::find($user_id);
    if (empty($user)) {
        $user = new User;
        $user->id = $user_id;
    }

    $me = $facebook->api('/me');
    $user->name = $me['name'];
    $user->access_token = $facebook->getAccessToken();
    $user->save();

    Auth::login($user);

    return Redirect::to('/')->with('message', 'ログインしました。');
});

facebookログインの仕組みの全貌が把握できていないので、本当にこれでいいのかいまいち確信が持てていないが、少なくとも認証自体はできているはず。

Twitterの場合と同様に、最終的にはAuth::login()でLaravel側での認証情報をセットしている。

ログアウトは、Laravel側のログアウト処理だけ。

Route::get('logout', function() {
    Auth::logout();
    return Redirect::to('/');
});

ホーム画面にfacebookに登録されている名前くらいは表示するようにしておく。

Route::get('/', function()
{
    $data = array();

    if (Auth::check()) {
        $facebook = new Facebook(Config::get('facebook'));
        $me = $facebook->api('/me');
        $data['me'] = $me;
    }

    return View::make('home', $data);
});
// app/views/home.blade.php
@if (!empty($me))
    Hello, {{{ $me['name'] }}}
    <a href="/logout">ログアウト</a>
@else
    <a href="/login">ログイン</a>
@endif

今回はLaravel 4側の問題ではなくfacebook APIの仕組みを調べるのに手間取ってしまった。ちゃんと使うんだったら、もうちょっとfacebook APIのドキュメントを読み込まないとだめだなー。

Published At2013-07-12 15:45Updated At2020-01-01 15:42

技術日記Laravel4PHP
Laravel 4でTwitterログイン機能を作ってみるEdit

Laravel 4で、標準のAuth関連機能とTwitterのOAuth認証パッケージとを連動させて、Twitterでログイン&ユーザー登録機能を作ってみる。

Twitterのユーザー情報をそのまま自前のユーザーテーブルに保存して利用するんで、

php artisan migrate:make create_users_table

をしてmigrationファイルを生成し、以下のようなテーブルスキーマを定義する。

    public function up()
    {
        Schema::create('users', function($table) {
            $table->integer('id')->unsigned();
            $table->primary('id');
            $table->string('screen_name');
            $table->string('oauth_token');
            $table->string('oauth_token_secret');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::drop('users');
    }

プライマリーキーのusers.idに、Twitterのuser_idを直接保存して利用するので、$table->increments('id')にはせず(auto incrementにはせず)、unsigned intなプライマリーキーとして定義する。あと、Twitterの認証情報であるoauth_tokenとoauth_token_secretを保存するカラムも用意しておく。

Userモデルは以下のような感じ。

use Illuminate\Auth\UserInterface;

class User extends Eloquent implements UserInterface {

    public function getAuthIdentifier()
    {
        return $this->getKey();
    }

    public function getAuthPassword()
    {
        return null;
    }

}

Authで認証に利用するユーザークラスにはimplements UserInterfaceが必要なので、getAuthIdentifier()とgetAuthPassword()の二つを追加しておく。Twitter認証ではシステム独自のパスワードは必要ないのでgetAuthPassword()ではNULLを返している。

続いて、Twitter認証パッケージのTwitterOAuth Service Provider for Laravel 4をインストールする。インストール手順はドキュメント通り。composer.jsonに依存設定を追加して、composer updateしたり、config:publishしたり。

あと、dev.twitter.comでアプリケーション登録して認証キーの発行をおこない、app/config/packages/philo/twitter/config.phpにその値を登録しておく。

さてここからが実際のコーディング。Twitterログイン用のルーティング定義は以下のような感じになる。ほぼドキュメントに書かれている通り。

Route::get('login', function() {
    if (Auth::check()) {
        return Redirect::to('/')->with('message', 'ログイン済みです。');
    }
    $tokens = Twitter::oAuthRequestToken();
    Twitter::oAuthAuthorize(array_get($tokens, 'oauth_token'));
    die;
});

これで/loginにアクセスすると、Twitterの認証ページにリダイレクトする。Twitter側で認証されると呼ばれるコールバックURLは、dev.twitter.comで設定できるので、サンプルアプリの「/login/callback」に設定しておく。コールバック時の処理は以下のようになる。

Route::get('login/callback', function() {
    $token = Input::get('oauth_token');
    $verifier = Input::get('oauth_verifier');
    $accessToken = Twitter::oAuthAccessToken($token, $verifier);

    if (isset($accessToken['user_id'])) {
        $user_id = $accessToken['user_id'];
        $user = User::find($user_id);
        if (empty($user)) {
            $user = new User;
            $user->id = $user_id;
        }
        $user->screen_name = $accessToken['screen_name'];
        $user->oauth_token = $accessToken['oauth_token'];
        $user->oauth_token_secret = $accessToken['oauth_token_secret'];
        $user->save();

        Auth::login($user);

        return Redirect::to('/');
    } else {
        return Redirect::to('login')->with('message', 'Twitter認証できませんでした。');
    }
});

コールバックされたパラメータからアクセストークンを取得し、TwitterユーザーID($accessToken['user_id'])が取れていればログイン成功。そのTwitterユーザーIDがusersテーブルに登録されていない場合は、新しいレコードとして登録する。

screen_name、oauth_token、oauth_token_secretに関しては、ログインするたびに新しい情報に更新する。

で、Auth::login($user)がポイントで、これによってそのユーザーでログインしている状態をシステム(セッション)に記憶させる。

今回はusersテーブルに直接Twitterのログイン情報を持たせたんで、Twitter認証情報から直接Userクラスのインスタンスを取得できた。

もしも、Twitter認証情報をusersテーブル以外に持たせたい場合(通常は自前のID、パスワードでログインしつつ、必要ならばTwitterでもログインできるようにする、など)でも、最終的にUserクラス(というかUserIntafaceが実装されたクラス)のインスタンスを取得してしまえば同様にログイン処理が行える。

擬似コードとしては、以下のような感じ。

// Twitter認証テーブルから該当行を取得
$twitterUser = TwitterUser::find($accessToken['user_id']);

// Twitter認証テーブルとリンクしているUserテーブルの該当行を取得
$user = User::find($twitterUser['user_id']); 

Auth::login($user); // 該当ユーザーをログインさせる

一応ログアウト処理のルーティングも書いておく。これはTwitter認証とは関係なく、Laravel 4のログアウト処理のまま。

Route::get('logout', function() {
   Auth::logout();
   return Redirect::to('/')->with('message', 'ログアウトしました。');
});

あと、ホーム画面でログイン状況がわかるように書いておく。

Route::get('/', function()
{
    return View::make('home');
});
// app/views/home.blade.php
@if (Auth::check())
    {{{ Auth::user()->screen_name }}}ログイン中
    <a href="/logout">ログアウト</a>
@else
        未ログイン
    <a href="/login">ログイン</a>
@endif

未ログイン状態の場合は、ログインへのリンクを表示。ログイン中の場合は、ログインユーザーのscreen_nameを表示し、ログアウトリンクを表示している。

ちなみにこのTwitter認証パッケージは、Twitter APIパッケージを利用しているので、このままTwitter APIの機能を利用できる。たとえば、

$user = Auth::user();
Twitter::setOAuthToken($user->oauth_token);
Twitter::setOAuthTokenSecret($user->oauth_token_secret);
$timeline = Twitter::statusesUserTimeline($user->id);

なんて感じで、ユーザーのタイムラインを取得できる。

Laravel 4の認証処理周りはシンプルで、自前でいろいろ処理したい場合でも、なかなか使いやすそうだね。

Published At2013-07-11 17:34Updated At2020-01-01 15:44

技術日記Laravel4
Laravel 4でキューを使ってみるEdit

キューエンジンとして、beanstalkdを使うんで、

yum install beanstalkd
service beanstalkd start

とかやってbeanstalkdをデフォルト設定で動かしておく。

Laravel 4のapp/config/queue.phpで、

'default' => 'beanstalkd',

に変更する。beanstalkdを使うには追加でライブラリをインストールしておく必要があるんで、composer.jsonのrequireに、

"pda/pheanstalk": "2.0.*"

を追加して、composer updateしてインストールしておく。

これでbeanstalkdコネクタを使ってキューを使う準備OK。

キューを使うサンプルとしては、単純なDBカウンタを作ってみる。

事前準備として、app/config/database.phpを書き換えて、mysqlデータベースが動くように適当にしておく。続いて、

php artisan migrate:make create_counter_table

で、カウンター用のテーブルを作る。app/database/migrations/*_create_counter_table.phpに、

  public function up()
  {
    Schema::create('counters', function ($table)
    {
        $table->increments('id');
        $table->integer('value');
        $table->integer('total');
        $table->timestamps();
    });
  }

としておく。要は、

create table counters (
id integer primary key,
value integer,
total integer,
created_at datetime,
updated_at datetime
);

で、valueに加算する整数値、totalにそこまでのvalueの合計が入るイメージ。

これを操作するモデルも作っておく。app/models/Counter.phpとして、

<?php
class Counter extends Eloquent
{
}

を作っておく。

で、まずはcounterにキューを使わずに(同期処理で)カウントするAPIを用意する。app/routes.phpに以下のルートを追加する。

Route::get('count/{value?}', function ($value = null) {
    $value = is_null($value) ? rand(1, 5) : $value;
    $total = Counter::sum('value') + $value;
    sleep(rand(0, 2));
    $counter = Counter::create(array('value' => $value, 'total' => $total));
    $counter->save();
    return $total;
})->where(array('id' => '\d+'));

http://[sample_app_host]/countにアクセスすると、現在DBに登録されているvalueの合計に、1~5までのランダムな整数が加算され、その結果の数値が表示される。

ちなみに、4行目に0~2秒のランダムなsleepが入っているのは、DBから合計値を取得するタイミングと、新しい計算結果をDBに登録するまでの間に強制的にタイムラグを発生させる(処理に時間がかかった場合に、不整合が起こるようなシチュエーションを再現させる)ためだ。

カウントするAPIだけだと状況がわかりにくいんで、現在DBに登録されている内容を表示するAPIも追加しておこう。

Route::get('list', function () {
   $list = Counter::orderBy('id')->get();
   $result = '';
   $total = 0;
   foreach ($list as $row) {
       $result .= $total . '+' . $row->value . '=' . $row->total . "<br />\n";
       $total = $row->total;
   }
   return $result;
});

これでhttp://[sample_app_host]/listにアクセスすると、現在DBに登録されている内容が以下のように表示される。

0+4=4
4+2=6
6+1=7
7+5=12
12+2=14
14+5=19

ちなみに上記の内容は、カウントAPIをブラウザごしに何回かリロードした結果となっている。しかし、実際にはWeb APIは一つのブラウザから順序よくアクセスされるとは限らない。そこで、いったんcountersテーブルをリセットし、apache benchを使って、以下のような複数同時アクセスを発生させてみよう。

ab.exe -c 5 -n 20 http://[sample_app_host]/count

その結果は以下のようになった。

0+1=1
1+1=2
2+3=4
4+2=3
3+5=6
6+1=13
13+5=18
18+3=4
4+2=20
20+3=26
26+5=26
26+4=25
25+3=38
38+3=24
24+2=23
23+3=46
46+2=28
28+3=38
38+2=43
43+2=43

集計処理とその結果をDBに登録する処理の間にランダムなタイムラグ(sleep)があるため、正しい結果になっていない。

そこで、キューを使って、呼び出し&登録処理が非同期でも、実際の計算処理は必ず順番通りに行われるようにしてみる。

Route::get('qcount/{value?}', function ($value = null) {
    Queue::push(function ($job) use ($value) {
        $value = is_null($value) ? rand(1, 5) : $value;
        $total = Counter::sum('value') + $value;
        sleep(rand(0, 2));
        $counter = Counter::create(array('value' => $value, 'total' => $total));
        $counter->save();
        $job->delete();
    });
    return 'Queued.';
})->where(array('id' => '\d+'));

http://[sample_app_host]/qcountにアクセスすると、先ほどcountで実行した内容と同じ処理をクロージャとしてキューに登録するように書き直した。キューに登録した時点では処理は行われないので、この時点ではDBに登録処理は行われない。

php artisan queue:listen

を実行すると、キューに登録された処理を順次実行していくことになる。

はずだったのだが、実際には上記コードはうまく動かなかった。listenでキューの内容を順次実行するのではなく、

php artisan queue:work

を実行して、最初の一件のジョブだけを実行してみると、以下のようなエラーが発生していた。

{"error":{"type":"Symfony\\Component\\Debug\\Exception\\FatalErrorException","message":"syntax error, unexpected ';'","file":"\/path\/to\/app\/vendor\/laravel\/framework\/src\/Illuminate\/Support\/SerializableClosure.php(173) : eval()'d code","line":12}}

クロージャをevalする際に構文エラーが発生しているようだが、深追いする気になれなかったので、クロージャを使う方法はあきらめた。

クロージャを使わない方法としては、キュージョブを実行するクラスを用意し、そのクラス名を登録する方法がある。app/libs/QueueTest.phpとして、

<?php

class QueueTest
{
    public function fire($job, $data)
    {
        $value = is_null($data['value']) ? rand(1, 5) : $data['value'];
        $total = Counter::sum('value') + $value;
        sleep(rand(0, 2));
        $counter = Counter::create(array('value' => $value, 'total' => $total));
        $counter->save();
        $job->delete();
    }
}

を用意し、QueueTestクラスをオートロードするために、app/start/global.phpのClassLoader::addDirectoriesに、「app_path().'/libs'」ディレクトリを追加しておく。

さらにキューを使ったルートの内容を以下のように変更する。

Route::get('qcount/{value?}', function ($value = null) {
    Queue::push('QueueTest', array('value' => $value));
    return 'Queued.';
})->where(array('id' => '\d+'));

これで、キューに登録されたジョブを処理する際には、QueueTest::fire()メソッドが呼ばれるようになる。apache benchを使ってこちらの実行結果も試してみると、

ab.exe -c 5 -n 20 http://[sample_app_host]/qcount
0+1=1
1+4=5
5+5=10
10+2=12
12+5=17
17+3=20
20+2=22
22+2=24
24+3=27
27+2=29
29+2=31
31+1=32
32+4=36
36+2=38
38+4=42
42+5=47
47+3=50
50+4=54
54+2=56
56+2=58

こちらはAPIは同時アクセスで呼び出されても、処理自体は順次(シリアルに)実行されているのがわかる。

というわけで、Laravel 4のキューを実際に使ってみたら、なんかうまく動かない部分もあったけれども、一応回避して使えることはちゃんと使えました、よかったよかった、という話でした。

※すでにクロージャを使ったキュージョブが登録されている場合は、クラスを使ったキュー登録処理を実行する前に、beanstalkdを再起動するなどして、クロージャを使った(=実行できない)ジョブをクリアしておく必要がある。

※キューをリッスンしておくために、あらかじめsupervisordなどを使って、php artisan queue:listenを自動起動&自動再起動するように設定しておくと便利。

追記)クロージャでジョブを登録するとSyntax Errorが出る件は、

    Queue::push(function ($job) use ($value) {

    Queue::push(function($job) use ($value) {

のように、クロージャの「function」と「(」の間の半角スペースを除去すれば動くようになった。see: SerializableClosure::getCodeFromFile()。

現状のLaravel 4の仕様を厳密に捉えると、バグといえばバグなんだろうけど、修正するよりもPSR-2に準拠して書こうってことで、使う側が対処する方が真っ当な気もする。strposの代わりにpreg_matchを使ってクロージャの開始位置を探すようにすれば修正は可能だけど。

Published At2013-07-05 20:01Updated At2020-01-01 12:58