gihyo.jpさんにPHP Matsuri 2011のレポートが2本とも掲載され、やっと俺の祭りが終わった。結局締め切りギリギリになってしまいとても反省しています。

少し振り返ってみると、祭りが終わった直後からものすごい勢いでモチベーションが上がっていて、心の中で悶々としてたものが一気に整理がついたように思う。それは、yandoさんの行動力や日々進化していくhyoshiokさんの生き様を目の当たりにして、ハッカーとは何たるかが少しだけわかった気がするからでもある。ここ2年ぐらい、スーツとギークの狭間で葛藤していた気持ちがやっと晴れた感じがしてとても心地よい一方、仕事や社会生活において自分も「ハッカー」としてアウトプットしていかなきゃならないなと少しプレッシャーを感じてもいる今日この頃。

さて、PHP Matsuriでは一応cakephp-pjax-pluginという、CakePHPに簡単にpushstate+ajaxを実装できるプラグインを書いて発表した。pjaxはgithubなんかで使われてる素敵技術(このへんの説明がわかりやすい)なので興味があったら使ってやってください、デモはこちら。ただ、pjax_railsというrailsプラグインのパクリで、数行で出来てしまう簡単なものだったので欲求不満気味。しかも、CakePHP2.0を実際に動かした事が無かったのでCakePHP1.3対応というお粗末さ。

そこでこんなの作ってみた。一応のデザインを入れたり、Flickr APIとスライドショーライブラリのドキュメントと睨めっこしつつも7,8時間ぐらいで出来たのでハッカソンで作っても間に合うぐらいの規模感。もともとはひたすら猫の画像見続けるサイトが欲しいねって友達と話していてドメインだけ取っていたものなんだけど、PHP Matsuriを期にFlickrを使い始めたこともあり、Flickrのタグ検索に当たった写真をスライドショーにできるサービスにしてみた。それだけだと面白くも何ともないので、youtubeにアップされている動画のURLを一緒に登録することでBGMを流せるようにした。これでPHP Matsuri参加者には響くサービスになったんですよきっと!

というわけで、Flickrにアップした写真に”phpmatsuri2011″のタグを付けると上のサンプルにも追加されるはず。

まだ4点ほど問題があって、

  1. 最新のchromeとfirefoxでしか確認してない
  2. スライドショーのjavascriptがパフォーマンス的に厳しくて今は写真の枚数を30枚までに制限してる
  3. 肝心のスライドショーを作ったり管理したりの機能を作り込んでないのでまだ公開できるレベルじゃない
  4. IE爆発しろ

と取り急ぎ自己満足レベルにしかなってないんだけど、CakePHP2.0で何か作るっていう目的は達成できたし、とりあえず真新しいサービスでもないのでゆっくりやることにします。あと、これ作るついでにTwitterBootstrapをCake用にゴニョゴニョしたCakeBootstrapというプラグインも作りました。最初Themeとして作ってたのでREADMEが完全に間違ったままだったりしますが意外と便利なので良かったらどうぞ。特にCSS周りは荒削りなのでpull request貰えると泣いて喜びます。

 

CakePHPのACLを使って権限を管理していると、権限がないアクションへのリンクを隠したくなることがあるので私はこんな感じにしてます。
※注: ユーザーロールによるACLを対象に書いてますので個々のユーザーによるACLだとそのままでは動きません

HtmlHelper::link()を叩く度にACLで権限チェックをしていると大変よろしくないので、まずは以下のようなComponentのinit()を叩いてログインユーザーの権限情報を整理してセッションに格納します。

class PermissionComponent extends Object {
  public $components = array('Session', 'Auth', 'Acl');
  private $C = null;

  public function startup(&$controller) {
    $this->C = &$controller;
  }

  public function init() {
    $this->clear();

    $aro = $this->Acl->Aro->find('first', array(
      'conditions' => array(
        'Aro.model' => 'Role',
        'Aro.foreign_key' => $this->Auth->user('role_id'),
      ),
    ));

    $permissions = $this->Acl->Aro->Permission->find('all', array(
      'conditions' => array(
        'Permission.aro_id' => $aro['Aro']['id'],
      ),
    ));

    foreach ($permissions as $p) {
      if ($p['Permission']['_create'] == 1 && $p['Permission']['_read'] == 1 &&
        $p['Permission']['_update'] == 1 && $p['Permission']['_delete'] == 1) {
        $allow = true;
      } else {
        $allow = false;
      }

      if (!empty($p['Aco']['alias']) && $p['Aco']['alias'] == 'controllers') {
        $this->Session->write('Auth.Permissions.controllers', $allow);
      } elseif(!empty($p['Aco']['parent_id'])) {
        $parent = $this->Acl->Aco->findById($p['Aco']['parent_id']);
        $key = 'Auth.Permissions.' . $parent['Aco']['alias'] . '.' . $p['Aco']['alias'];
        $this->Session->write($key, $allow);
      }
    }
  }

  public function clear() {
    $this->Session->delete('Auth.Permissions');
  }
}

このPermissionComponent::init()は、ユーザーがログインした時点で実行するようにしています。AuthComponentのデフォルト設定であれば以下のようになるはずです。

class UsersController extends AppController {
  public $components = array('Auth', 'Permission');

  public function login() {
    $user = $this->Auth->user();
    if ($user) {
      $this->Permission->init();
      $this->redirect($this->Auth->loginRedirect);
    }
  }

  public function logout() {
    $this->redirect($this->Auth->logout());
  }
}

次に、セッションに格納した権限情報をViewからチェックする為のHelperを作ります。
このPermissionHelper::check()は、ログインユーザーが引数に取ったURLへアクセスする権限を持つ場合にtrueを返します。

class PermissionHelper extends AppHelper {
  public $helpers = array('Session');

  public function check($path){
    if(!is_array($path)) {
      $path = Router::parse($path);
    }
    if(empty($path['controller'])) {
      $path['controller'] = $this->params['controller'];
    }
    if(empty($path['action'])) {
      $path['action'] = 'index';
    }

    if(!empty($path['prefix']) && $path['prefix'] == 'admin') {
      return true;
    }

    $allkey = 'Auth.Permissions.controllers';
    if($this->Session->check($allkey) && $this->Session->read($allkey) === true){
      return true;
    }

    $key = 'Auth.Permissions.' . Inflector::camelize($path['controller']) . '.' . $path['action'];
    if($this->Session->check($key) && $this->Session->read($key) === true){
      return true;
    }

    return false;
  }
}

しかし、これでいちいちチェックしてからHtmlHelper::link()も呼ぶのは非常にめんどくさいので、このPermissionHelperを内部で使うようにHtmlHelperを継承したMyHtmlHelperを作ります。

class MyHtmlHelper extends HtmlHelper {
  public $helpers = array('Permission');

  public function link($title, $url = null, $options = array(), $confirmMessage = false) {
    $options = array_merge(array(
      'escape' => false,
    ), $options);
    $url = ($url !== null) ? $url : $title;

    if($this->Permission->check($url)) {
      return parent::link($title, $url, $options, $confirmMessage);;
    }
    return null;
  }
}

このMyHtmlHelper::link()はリンクを出力する前にPermissionHelperを用いて権限チェックを行い、アクセス権限がある場合のみリンクを出力します。

使い方は通常のHtmlHelperと何ら変わりありません。
しかし、わざわざViewで$this->MyHtml->link();などと打つのは大変煩わしいので、@hiromi2424のHackPluginを貰ってきてAppControllerで以下のようにするとviewを変更する必要がなくなるのでとても幸せになれます。

class AppController extends Controller {
  public $helpers = array('Hack.alias' => array('Html' => 'MyHtml'));
}

全然関係ない宣伝ですが、今年もPHPMatsuriの準備が本格化してきてます。
PHP Matsuri 2011
今年はPHP5.4, Symfony2, CakePHP2.0, CodeIgniter2.0とPHP界隈が盛り上がってますし、PHP以外にもTitanium mobileやTDD関連のセッション、ワークショップも開催予定です。
協賛企業様も絶賛募集中ですのでぜひ一度お気軽にご連絡ください。

Cake2.0ももうすぐRCですし私もアクティビティAGEAGEで参ります。

 

AuthComponent関係ないけど。
CakePHPだとcore.phpの内容に応じてsessionの設定などをいろいろやってるので、
php.iniなんかでcookieの有効期限を0にするだけだとうまく動かない。

独自の設定を追加するには、core.phpに書いあるとおり以下の方法を使うといいらしい。

To define a custom session handler, save it at /app/config/<name>.php.
Set the value of ‘Session.save’ to to utilize it in CakePHP.

app/config/[Session.saveの値].phpを作ったらいいよだって。

というわけで実際にやってみる。
my_sessionの部分は適宜置き換えてください。

app/config/core.php line 127:

// ここを
Configure::write('Session.save', 'php');
// こう
Configure::write('Session.save', 'my_session');
?>

app/config/my_session.php

if (empty($_SESSION)) {
  session_set_cookie_params(0);
}

ちなみに、この my_session.php には他にもini_setなどでphpのセッション設定などいろいろできる。

 

# これめっちゃ間違ってるので訂正!
# s/$this->Security = false/$this->Security->enabled = false/g

SecurityComponentのAPI見ても全然書いてないけど。
あるコントローラとかアクションのみ、SecurityComponentを無効にしたい。
Controllerにて$this->Security->enabled = falseすればOK。

例えば、ExampleControllerのtestアクションのみ無効化したいとすると、

<?php
class ExampleController extends AppController {
  public $components = array('Security');
  public function beforeFilter() {
    parent::beforeFilter();
    if($this->params['action'] == 'test') {
      $this->Security->enabled = false;
    }
  } 

  public function test() {
    // のっぴきならない事情でSecurityComponentが使えない処理
  }
}

フォームで入力する値が動的に変わっちゃう上に項目数の多い画面だとdisableFieldsだけでえらいことになってしまうので。
もっと良い方法ありそうだけど力尽きた。
知ってる方いらっしゃったら教えて下さい。

 

現状、Apache+mod_phpをnginxで置き換えるとすると、nginx+spawn-fcgi+php-cgiを使ってる人が多いと思うんだけど、Ubuntuの公式リポジトリだとnginxのバージョンが低く(0.7系)、spawn-fcgiはinit.dスクリプトを書くかdaemontools使わなきゃいけないし、Apache+mod_phpみたいにインストール一発で動かせないので割とメンドクサイ。

同じこと思ってる人はnginxチームが提供しているaptリポジトリを使うと、nginx1.0.2+php5.3.5+php-fpmをaptでインストールできるので楽になりますよ。

既にnginxがインストールされている場合は念のためremoveしておく、spawn-cgiなどはkillしておきましょう。
以下、手順。

# nginxリポジトリを登録
$ sudo echo "deb http://ppa.launchpad.net/nginx/stable/ubuntu lucid main" > /etc/apt/sources.list.d/nginx-stable-lucid.list
$ sudo echo "deb http://ppa.launchpad.net/nginx/php5/ubuntu lucid main" > /etc/apt/sources.list.d/nginx-stable-lucid.list
$ sudo echo "deb-src http://ppa.launchpad.net/nginx/php5/ubuntu lucid main" > /etc/apt/sources.list.d/nginx-stable-lucid.list
$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C300EE8C
# aptitudeのリストを更新
$ sudo aptitude update
# nginxをインストール
$ sudo aptitude install nginx
# PHP関連をインストール
$ sudo aptitude install php5 php5-cgi php5-cli php5-mysql php5-memcached php5-gd php5-curl php-pear php-apc php-fpm

インストールできたら、プロセスを確認してみる。

$ ps aux | grep nginx
root     14051  0.0  0.1  71092  1568 ?        Ss   May28   0:00 nginx: master process /usr/sbin/nginx
www-data 14052  0.1  0.2  72496  3568 ?        S    May28   3:47 nginx: worker process

$ ps aux | grep php
root     14238  0.0  0.3 182432  4944 ?        Ss   May28   0:25 php-fpm: master process (/etc/php5/fpm/main.conf)
www-data 14255  0.1  3.1 192428 48292 ?        S    May28   4:54 php-fpm: pool www
www-data 14256  0.1  4.0 207328 62988 ?        S    May28   4:43 php-fpm: pool www
www-data 14258  0.1  3.8 202720 59212 ?        S    May28   4:36 php-fpm: pool www
www-data 14291  0.1  3.6 199648 55496 ?        S    May28   4:39 php-fpm: pool www
www-data 14293  0.1  3.3 196576 52260 ?        S    May28   4:51 php-fpm: pool www
www-data 14294  0.1  3.4 196576 52952 ?        S    May28   4:47 php-fpm: pool www
www-data 14297  0.1  4.5 214756 70368 ?        S    May28   4:34 php-fpm: pool www
www-data 14298  0.1  3.1 192336 48900 ?        S    May28   4:46 php-fpm: pool www
www-data 14299  0.1  3.1 192332 48856 ?        S    May28   5:05 php-fpm: pool www
www-data 14300  0.1  2.9 191044 46132 ?        S    May28   4:58 php-fpm: pool www

ちゃんとユーザーwww-dataでphp-fpmのプロセスが起動されてますね。一番上のrootで実行されているのでphp-fpmの子プロセスを管理してるマスタープロセスです。起動していないければsudo service nginx startとsudo service php-fpm startで起動させて再度確認します。
起動するプロセス数やfastcgiのlisten URLなどは/etc/php5/php-fpm/pool.d/www.confにて設定します。

次に、nginxのバーチャルホストの設定。
下の設定だとwordpressとCakePHPは動くはず。

# /etc/nginx/sites-avilable/example.com

# www.example.comへのアクセスをexample.comへリダイレクト
server {
  listen 80;
  server_name www.example.com;
  rewrite ^(.*) http://example.com$1 permanent;
}
server {
  listen   80;
  server_name  example.com;

  access_log  /var/log/nginx/example.com.access.log;
  error_log  /var/log/nginx/example.com.error.log;

  root   /var/www/example.com;
  index  index.php index.html;

  try_files $uri $uri/ /index.php?q=$1;

  # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
  location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
  }

  # deny access to .htaccess files, if Apache's document root
  # concurs with nginx's one
  location ~ /\.ht {
    deny  all;
  }
}

この内容を/etc/nginx/sites-available/example.comに保存し、/etc/nginx/sites-enabled/example.comからシンボリックリンクを張ります。

$ sudo vi /etc/nginx/sites-available/example.com
$ cd
$ sudo ln -s /etc/nginx/sites-availabe/example.com /etc/nginx/sites-enabled/example.com

ここまでやったらnginxとphp-fpmを再起動して、http://example.comをブラウザで開き、正常に動けばOKです。ダメだったら何とかしましょう。

 

CakePHPにもRailsでいうscaffold generatorのような、DBスキーマからCRUD画面を生成するbakeという機能があります。このbakeで生成するコードのテンプレートを予めカスタマイズしておくと、簡単なシステムなら非常に効率よく作れます。

しかしこの方法を説明している日本語版Cookbookが古いままである為、そのまま実践しても出力結果がデフォルトのままになってしまい大変残念なので、英語版Cookbookの該当ページを見ましょうっていうお話。

f you wish to modify the default HTML output produced by the “bake” command, follow these simple steps:
For baking custom views:
Go into: cake/console/templates/default/views
Notice the 4 files there
Copy them to your: app/vendors/shells/templates/[themename]/views
Make changes to the HTML output to control the way “bake” builds your views
The [themename] path segment should be the name of the bake theme that you are creating. Bake theme names need to be unique, so don’t use ‘default’.

Modify default HTML produced by “baked” templates

– 以下適当な意訳で解説 –

bakeコマンドで出力されるデフォルトHTMLを変更する場合、以下の手順を実行して下さい。
viewをカスタマイズする場合:

  1. cake/console/templates/default/viewsに移動する
  2. 4つのファイルがあるはず(index.ctp, form.ctp, view.ctp, home.ctp)
  3. これら4つのファイルをあなたのアプリケーションのapp/vendors/shells/templates/[themename]/viewsにコピーする
  4. コピーしたファイルを好きに編集したらbakeがそれを使ってコードを作ってくれる

ちなみに、[themename]の部分のディレクトリ名はあなた独自のものを指定する。特に’default’という名前は使わないこと。

– 解説以上 –

というわけで、上記の手順を実行すると、cake bakeしたときにthemeを選ぶ作業が追加されます。他のテーマが無ければ[themename]と’default’の2つから選ぶことになるので遠慮無く前者を選ぶとよいです。

デフォルトのテンプレートがあるcake/console/templates/default以下にはbakeで出力できる全てのテンプレートが置いてあるので、controllerやmodelなんかも修正できて大変便利です。ちなみにプロジェクトのテンプレートを編集する方法は上記の参考ページに書いてあるので是非見ることをお勧めします。

蛇足ですがググっても古い方の情報しか出てこないのでこの記事が上がるといいと思うし、
これを見た@hiromi2424が書き直してくれる気がする。

© 2011 sanojimaru.com Suffusion theme by Sayontan Sinha