2013年7月3日水曜日

39.Google App Engine for PHPでCakePHPを動かしてみた




Ryo Yamasaki(@vierjp)です。

Google I/O 後に Limited Preview のアカウントをもらってから、
これまでGoogle App Engine for PHPに関して以下のブログを書いてきました。

28.Google App Engine for PHP (GAE/PHP) を早速試してみた
29.Google App Engine for PHPでWordPressを動かしてみた
30.Google App Engine for PHPにおけるポータビリティを考える
36.Google App Engine for PHPでWordPressを運用するためのプラグインが登場

私がPHP素人ということもありこれまでは主に「既存のPHPアプリをApp Engineで動かす」内容でしたが、
今回は「新規にアプリを作成する」ケースを想定して
GAE/PHP上でCakePHPというフレームワークを動かしてみようと思います



◯GAE/PHPで動かすフレームワークの条件

公式のIssue Trackerでのフレームワークについてのやりとりの中で、
Googleのエンジニアの以下の書き込みがありました。(本当はもっと細かいけど概要はこんな感じ)

1.GAEのファイルシステムはReadOnlyなので、一時ファイルはGoogle Cloud Storageに書き込む必要がある。
2.ロギングはsyslog関数を使っていること。(ファイルシステム内の指定ディレクトリへの出力はできない)

一つ目は#DevFestでも話にあった「ローカルに一時ファイルを書き出す既存のPHPフレームワークを動かすのは厳しいのではないか」と同じ内容ですね。

期待は薄かったのですが、今時フレームワークが使えない開発も厳しいと思うのでダメ元で試してみました。
PHPのフレームワークについて軽くググったところ、日本ではCakePHPが人気らしいのでCakePHPにしました。

結論から書くと、
「適切に設定をすることでApp Engine上でCakePHPを動作させることができた」
と言って良いのではないかと思います。 (全機能を試したわけではありませんが)



◯ローカル環境の構築

実はこれまでGAE/PHPに関してローカル環境で動かしたことがなかったのですが、
さすがに今回はエディタと本番環境でのテストだけではきつそうだったのでローカルに開発環境を構築しました。
以下はローカル開発環境のざっくりとした構築手順です。

・MySQLのインストール
Mac OS へのMySQLのインストール方法
こちらの手順に沿って行いました。

・MacPortsのインストール
MacPorts公式

冒頭の「“pkg” installers for Mountain Lion」のリンクからダウンロードできます。
Xcodeが入っている環境でインストール時にエラーが発生する場合はこちら

・PHPのインストール
App Engine公式ドキュメントより
sudo /opt/local/bin/port install php54-cgi php54-APC php54-calendar \
    php54-exif php54-gd php54-mysql php54-oauth php54-openssl php54-soap \
    php54-xdebug php54-xsl

・PHPStormのインストール (任意)
PHP IDE :: JetBrains PhpStorm (ダウンロードページ)
Getting Started with PhpStorm as Google App Engine PHP IDE (使い方)
最近人気のPHP用IDEだそうです。
最初から GAE/PHP に対応するプラグインが組み込まれていて、IDE上で GAE/PHP のデバッグ・デプロイが可能です。
有料ですが、最初の一ヶ月は無料で利用できます。



◯初期設定

・アプリのルートディレクトリを作成する
今回は「CakePHPTest3」という名前にしました。

・CakePHPのサイトから最新の安定版(現時点で2.3.6)のzipをダウンロードして展開する



・「CakePHPTest3」以下に全てコピーする

・app.yamlの設定

/app.yaml
application: [アプリケーションID]
version: [任意のバージョン名]
runtime: php
api_version: 1
threadsafe: true

handlers:
- url: /css
  static_dir: app/webroot/css

- url: /js
  static_dir: app/webroot/js

- url: /img
  static_dir: app/webroot/img

- url: /favicon.ico
  static_files: app/webroot/favicon.ico
  upload: app/webroot/favicon.ico

- url: /.*
  script: app/webroot/index.php

* 通常CakePHPでは apache の mod_rewrite でルーティングの設定を行うようですが、GAE/PHPではapp.yamlで行います。


・デフォルトタイムゾーンの設定
ルートディレクトリ直下に「php.ini」を作成して以下の1行を記述します。

/php.ini
date.timezone = Asia/Tokyo


・Security.salt と Security.cipherSeed の変更

/app/Config/core.php
/**
 * A random string used in security hashing methods.
 */
 Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi');

/**
 * A random numeric string (digits only) used to encrypt/decrypt strings.
 */
 Configure::write('Security.cipherSeed', '76859309657453542496749683645');

上記はデフォルト値です。
この2つのセキュリティ的な値を適当に変更します。
「Security.salt」は文字列、「Security.cipherSeed」は数値です。


・App EngineのProduction環境とローカル環境で分岐するための処理を作成

データベースの接続先の切り替え等で使うのでまずはこれを作ります。

/app/Utility/AppEngine.php
<?php

class AppEngine {
    public static function isProduction() {
        if(isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'],'Google App Engine') !== false) {
            //syslog(LOG_WARNING, 'is Production.');
            return true;
        }else{
            //syslog(LOG_WARNING, 'is DevServer.');
            return false;
        }
    }
}

* 「$_SERVER['SERVER_SOFTWARE']」の値はProduction環境では「Google App Engine/1.8.1」、ローカル環境では「Development/2.0」になります。


・作成したUtilityを利用できるようにcore.phpの冒頭に下記を記述
/app/Config/core.php
App::uses('AppEngine', 'Utility');



・データベースの設定
最初から存在する「database.php.default」をコピーして「database.php」という名前で保存し、
下記の内容に書き換えます。

/app/Config/database.php
<?php
class DATABASE_CONFIG {

 public $default = NULL;

    public $prod = array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'unix_socket' => '/cloudsql/******:*****',
        'login' => 'cake_user',
        'password' => '********',
        'database' => 'cake_db',
        'encoding' => 'utf8',
    );

    public $dev = array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'cake_user',
        'password' => '********',
        'database' => 'cake_db',
        'encoding' => 'utf8',
    );

 public $test = array(
  'datasource' => 'Database/Mysql',
  'persistent' => false,
  'host' => 'localhost',
  'login' => 'user',
  'password' => 'password',
  'database' => 'test_database_name',
  'encoding' => 'utf8',
 );

    function __construct(){
        if(AppEngine::isProduction()) {
            $this->default = $this->prod;
        }else{
            $this->default = $this->dev;
        }
    }
}


・ローカルのMySQLにDBを作成する

CREATE DATABASE IF NOT EXISTS cake_db;
CREATE USER 'cake_user'@'localhost' IDENTIFIED BY '********';
GRANT ALL PRIVILEGES ON cake_db.* TO 'cake_user'@'localhost';


・dev_serverを起動してブラウザでアクセスします

PHPStormから起動する場合、
初回起動時の設定画面でPHPのPathを入力する必要があります。
上記手順でMacPortsからインストールした場合は、
「Path to php-cgi executable」に「/opt/local/bin/php-cgi54」と設定すればOKです。


デフォルトのトップページはCakePHPの設定を確認するためのページが開きます。
(ここまでの手順もこのページにアクセスしながら設定を変えていくと、変える度に警告が消えていきます)

dev_serverは制限が緩く「ファイルシステムへの書き込み」ができてしまうので、下記画像のように問題なくページが開きます。

*制限が緩いのでローカル環境では「Your tmp directory is writable.」と表示されます。


しかし、App Engine上では「ファイルシステムへの書き込み」ができないためエラーになってしまいます。

デフォルトのCakePHPは下記画像のように、「cache」や「Log」を「tmp」ディレクトリ以下に出力します。
(「sessions」「tests」はデフォルト設定ではファイルが書き込まれないので問題なし)



*App EngineでPHPの「$_SESSION」を使った場合にはMemcacheに書き込むようになっています。

初期設定のままのCakePHPの場合、予想どおり一時ディレクトリへのキャッシュの書き込みでエラーになります。
また、ログもファイルに出力する設定なので、App Engineのログには出力されません。

GAE/PHPでは「ファイルI/OはCloud Storageに対して行うべし」というのが基本のようなので、
当初一時ディレクトリをCloud Storageにするため、index.phpに下記を追加しました。
define('TMP', "gs://[bucket名]/");

しかし、これは期待通りに動作しませんでした。
GAE/PHP上で「gs://〜〜」のPathを指定してファイルI/O関連の操作をすると透過的にCloud Storageの読み書きをするWrapperに処理が移譲されるのですが、
ディレクトリの存在チェック処理で「ディレクトリが存在しない」という扱いでエラーになってしまいます。
(事前にCloud Storage上にFolderを作成しておいてもダメ)


* CakePHPに限らず、既存のアプリのファイル出力先をCloud Storageに変更しただけではこういったケースは他にもあるかもしれません。

Issue Trackerでのやりとりに書かれていた通り一時ファイルとログ出力が問題になりました。
また、単純に書き込み先のPathをCloud Storageにすれば良い、というわけにもいかないようです。
次項からこれらについて対処していきます。



◯cacheの設定を変更する

前述の通りデフォルト設定のCakePHPは一時データを「tmp」ディレクトリ以下にファイルとして保存しますが、
一時データの保存領域としてMemcacheやAPC(後述)を利用するためのコードも用意されています。
また、パフォーマンス的にもMemcacheやAPCの方がファイルキャッシュより高速とされているそうです。

・bootstrap.phpの冒頭でキャッシュ設定を変更する

/app/Config/bootstrap.php
// Setup a 'default' cache configuration for use in the application.
//Cache::config('default', array('engine' => 'File'));
if(AppEngine::isProduction()) {
    Cache::config('default', array('engine' => 'Apc'));
}else{
    Cache::config('default', array('engine' => 'File'));
}

・core.phpの最後の方でキャッシュ設定を変更する
/app/Config/core.php
//$engine = 'File';
// in Production Apc, in Devserver File
if(AppEngine::isProduction()) {
    $engine = 'Apc';
}else{
    $engine = 'File';
}


ここではAPCを使うように変更しました。
* ローカル環境でもAPCを使えますが、開発時にはtmpディレクトリ内にファイルそのものが見えた方がわかりやすいかも、ということで分岐してみました。

再度デフォルトのトップページを開くと以下のようになります。

上から3つ目の項目が「The ApcEngine is being used for core caching.」になっているのがポイント。
前述のローカル環境で実行した結果のキャプチャと比べるとわかりますが、
変更前は「The FileEngine is being used for core caching.」となっていました。

*「Your tmp directory is NOT writable.」と警告が出ていますが、tmpディレクトリを使用しないので問題ありません。


・APC (Alternative PHP Cache)について
基本的には「PHPの中間コードのキャッシュや最適化を行う拡張モジュール」ですが、
MemcacheのようなKeyValue型のメモリキャッシュとしても使えるそうです。

APCはMemcacheと比べると以下のような特性があります。
 ・サーバーのローカルメモリを利用
 ・Memcacheより読み書きが高速
 ・キャッシュはサーバー間で共有されない
 ・サーバーを再起動するとキャッシュは消える

App Engineはアプリのバージョン名毎にインスタンスが別なので、
デプロイ後に古いコードのキャッシュが残ってしまうことはありません。
(同じバージョン名に対して上書きする形でデプロイした場合には一度インスタンスが全て停止するのでやはりキャッシュはクリアされます)


・APCではなくMemcacheを使う場合についても考えてみる
 ・App Engineでは頻繁にサーバーの起動・停止をするのが半ば前提なので、Memcacheの方がキャッシュを共有できる点では効率は良さそう。
 ・この場合デプロイ時にキャッシュをクリアしないと最新のコードや設定と内容がずれるだろうからそのタイミングでMemcacheのクリアが必要と思われる。
 ・複数バージョンを同時に操作した場合に競合しそう。(キャッシュのKeyのPrefixにバージョン名を追加する等で対応はできそう)
 ・データ量の多いサイトでDBのデータをMemcacheにキャッシュしている場合にはMemcacheが枯渇してCakePHPのキャッシュが活きづらい状況はあるかも。


今回は無難そうなAPCを使いましたが、
APCとMemcacheのどちらが良いかは実際に様々な条件で比較してみなければなんとも言えません。
システムの性質によって最適なキャッシュ方法が変わる可能性もありそうです。



◯ログの設定を変更する

Issue Trackerでのやりとりにある通り、ログの出力をsyslog関数を使って行うことでApp  Engineのログに記録する事ができます。

CakePHPではログの出力先(出力方法)も設定で変更できます。

・Syslogに出力するためのコードを取得する
最新の安定版(2.3.6)にはログをSyslogに出力するコードは含まれていませんが、
開発中のバージョンには「lib/Cake/Log/Engine/SyslogLog.php」という、Syslogに出力するためのコードが含まれています。

このファイルをGithubにあるCakePHP 2.4のbranchから取得して「app/Log/Engine/SyslogLog.php」にコピーします。(「app/Log/Engine/」ディレクトリも作成)

・bootstrap.phpを修正する

/app/Config/bootstrap.php
/**
 * Configures default file logging options
 */
//App::uses('CakeLog', 'Log');
//CakeLog::config('debug', array(
// 'engine' => 'FileLog',
// 'types' => array('notice', 'info', 'debug'),
// 'file' => 'debug',
//));
//CakeLog::config('error', array(
// 'engine' => 'FileLog',
// 'types' => array('warning', 'error', 'critical', 'alert', 'emergency'),
// 'file' => 'error',
//));
if(AppEngine::isProduction()) {
    $engine = 'SyslogLog';
}else{
    $engine = 'FileLog';
}

App::uses('CakeLog', 'Log');
CakeLog::config('debug', array(
 'engine' => $engine,
 'types' => array('notice', 'info', 'debug'),
 'file' => 'debug',
));
CakeLog::config('error', array(
 'engine' => $engine,
 'types' => array('warning', 'error', 'critical', 'alert', 'emergency'),
 'file' => 'error',
));

・php.iniの修正
この状態では以下の警告が出力されるので、php.iniに追記します
PHP Warning:  php_sapi_name() has been disabled for security reasons. It can be re-enabled by adding it to the google_app_engine.enable_functions ini variable in your applications php.ini

/php.ini
google_app_engine.enable_functions = "php_sapi_name, gc_enabled"
date.timezone = Asia/Tokyo

* 一行目を追加しています。

これでApp Engineのログにシステムログが出力されるようになります。



◯DebugKit pluginの設定をする

後回しにした「DebugKit plugin」も少し修正することで利用可能です。

・DebugiKitをGitリポジトリから取得する

・/app/Plugin/DebugKit ディレクトリを作成して、取得したファイルをコピー

・bootstrap.php に以下の記述を追加
/app/Config/bootstrap.php
CakePlugin::load('DebugKit');


・AppControlleを修正する
/app/Controller/AppController.php
class AppController extends Controller {
    public $components = array('DebugKit.Toolbar'); // ←この行を追加する
}

・ToolbarComponentの修正

このコードの391行目でCacheにFile Engineを使ってしまっているのでこちらもAPCに変えます。
/app/Plugin/DebugKit/Controller/Component/ToolbarComponent.php
 protected function _createCacheConfig() {
        if (Configure::read('Cache.disable') !== true) {
            $cache = array(
                'duration' => $this->cacheDuration,
                'engine' => 'Apc',
                'path' => CACHE
            );
            if (isset($this->settings['cache'])) {
                $cache = array_merge($cache, $this->settings['cache']);
            }
            Cache::config('debug_kit', $cache);
        }
    }


・app.yaml に以下を追記 (最後の「- url: /.*」より先に定義する)

/app.yaml
- url: /debug_kit/css
  static_dir: app/Plugin/DebugKit/webroot/css

- url: /debug_kit/js
  static_dir: app/Plugin/DebugKit/webroot/js

- url: /debug_kit/img
  static_dir: app/Plugin/DebugKit/webroot/img





◯環境変数の問題を解決する

一部の環境変数の値を期待どおりに取得できずに画面遷移において発生する問題を解決するため、以下の記述を追加する。

Issue Trackerにも似たような内容が挙がっています。
関連: Issue 9369: PHP $_SERVER['SCRIPT_NAME'] variable does not match the expected implementation

・index.php の最後の方に2行追加する
/app/webroot/index.php
App::uses('Dispatcher', 'Routing');

// ここから
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
$_SERVER['PHP_SELF'] = 'webroot/index.php';
// ここまで

$Dispatcher = new Dispatcher();
$Dispatcher->dispatch(
 new CakeRequest(),
 new CakeResponse()
);

* そのうちIssueが修正されてこの部分の記述は不要になるかもしれません。




◯Sessionの保存場所を変える

以前に「30.Google App Engine for PHPにおけるポータビリティを考える」に書いたとおり、
現状GAE/PHPでは「$_SESSION」を使った場合のSessionデータの格納先は「Memcache」となっています。
そのためドキュメントには
・Memcacheのデータはクリアされるかもしれないのでそれによってセッション情報が失われる可能性がある。  
・長い期間Sessionを維持したければ、Cloud SQL等にセッション情報を保存した方が良い。
と書かれています。
この手順をしなくても一応CakePHPは動作しますが、ついでにこの対策もしてみます。


◯セッションをCloudSQLに保存する場合

CakePHPには「Sessionをデータベースに書き込むためのプログラム」が付属しているので、
DBにテーブルを用意した上でこの仕組みを使うようにCakePHPの設定を変更するだけで、
Sessionデータの格納場所を変更することができます。

・DBにSession格納用テーブルを作成する
USE cake_db;
CREATE TABLE IF NOT EXISTS cake_sessions (
  id varchar(255) NOT NULL,
  data text NOT NULL,
  expires int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

・core.phpを修正する
/app/Config/core.php
// Configure::write('Session', array(
//  'defaults' => 'php'
// ));

Configure::write('Session', array(
    'defaults' => 'database'
));


AppController.php を修正して'Session'を追加する
/app/Controller/AppController.php
public $components = array('Session','DebugKit.Toolbar');


◯MemcacheとCloudSQLを併用する場合

・前述の「Cloud SQLに保存する場合」と同様にテーブルを作成する

・「キャッシュとDBを併用するためのクラス」を作成する

/app/Model/Datasource/Session/ComboSession.php
<?php
App::uses('DatabaseSession', 'Model/Datasource/Session');

class ComboSession extends DatabaseSession implements CakeSessionHandlerInterface {
    public $cacheKey;

    public function __construct() {
        CakeLog::write('debug',"ComboSession#__construct");
        $this->cacheKey = Configure::read('Session.handler.cache');
        parent::__construct();
    }

    // セッションからデータ読み込み
    public function read($id) {
        CakeLog::write('debug',"ComboSession#read id:".$id);
        $result = Cache::read($id, $this->cacheKey);
        if ($result) {
            CakeLog::write('debug',"ComboSession#read result from cache result:".$result);
            return $result;
        }
        CakeLog::write('debug',"ComboSession#read result from db result:".$result);
        return parent::read($id);
    }

    // セッションへデータ書き込み
    public function write($id, $data) {
        CakeLog::write('debug',"ComboSession#write id:".$id." data:".$data);
        $result = Cache::write($id, $data, $this->cacheKey);
        CakeLog::write('debug',"ComboSession#write cache id:".$id);
        if ($result) {
            CakeLog::write('debug',"ComboSession#write db id:".$id);
            return parent::write($id, $data);
        }
        CakeLog::write('error',"ComboSession#write failed to write session:".$id);
        return false;
    }

    // セッションの破棄
    public function destroy($id) {
        CakeLog::write('debug',"ComboSession#destroy id:".$id);
        $result = Cache::delete($id, $this->cacheKey);
        if ($result) {
            return parent::destroy($id);
        }
        return false;
    }

    // 期限切れセッションの削除
    public function gc($expires = null) {
        return Cache::gc($this->cacheKey) && parent::gc($expires);
    }
}

・core.phpを修正する
/app/Config/core.php
// Configure::write('Session', array(
//  'defaults' => 'php'
// ));
    Configure::write('Session', array(
        'defaults' => 'database',
        'handler' => array(
            'engine' => 'ComboSession',
            'model' => 'Session',
            'cache' => 'sessionCache'
        )
    ));

    // セッション用Memcache設定の定義
    Cache::config('sessionCache',array(
        "engine" => "Memcache",
        "duration"=> 3600,
        "probability"=> 100,
        "compress" => false,
    ));

・前述の「Cloud SQLに保存する場合」と同様にAppController を修正して'Session'を追加する

コードは公式のサンプルに検証用のログを追加しただけです。
参照時にMemcacheにキャッシュがあればそれを利用し無ければDBを参照する、という処理になっているので、
パフォーマンスの向上と課金額の低下を期待できます。

公式のサンプルではAPCとDBの組み合わせにしていますが、
複数のサーバーが起動するのが(ほぼ)前提のApp EngineではWebサーバーのローカルメモリに書き込むAPCでセッション情報をキャッシュするのはまずいでしょうから、
MemcacheとDBの組み合わせにしました。
(スティッキーセッションではないのでどこのサーバーにリクエストが振り分けられるかわからない、よって全てのWebサーバーが共有する領域にキャッシュする必要がある)

キャッシュポリシーは名前をつけて複数設定できるので、セッションハンドラ専用のMemcacheの設定を追加してそれを利用しています。



◯ドットインストールのPHP入門に沿ってアプリを作ってみる

「一覧・登録・編集・削除」機能を持つシンプルなCRUD処理を行うWebアプリです。


ドットインストールの動画に沿って21回目の機能まで作って動作しました。
(その後のAjaxやコメント機能の追加はフレームワークの動作確認としては必要ないかな、という判断でここまで。ていうか力尽きました)

* このサンプルでは登録・更新・削除後に表示するメッセージの管理にSessionを使っています
 「setFlash」という関数を使った際にCakePHPが内部的に
 1.登録・更新・削除処理時にSessionにメッセージをセット
 2.一覧ページへリダイレクト
 3.Sessionからメッセージを取得して表示、Sessionから削除
 という処理をしているようです。


ドットインストールのPHP入門に沿って作ったサンプルアプリを以下に配置しました。
自由に読み書きしてもらって構いません。(投稿内容は常識の範囲でお願いします^-^;)
* 2013/11/10 追記 Cloud SQLの課金が地味に痛いので停止しました。

今回のサンプルアプリのコードはまるごとGithubに公開してあります。
https://github.com/vierjp/CakePHPTest3

* core.phpの「Security.salt と Security.cipherSeed」、database.phpの「接続先とパスワード」は伏せているので動作させる場合には書き換えてください。



◯まとめ

ドットインストールの動画の解説はおそらくCakePHPでのオーソドックスなCRUD処理のコーディング方法に沿っていると思いますので、
このサンプルの動作確認をもって「CakePHPが動いた」と言ってもいいんじゃないでしょうか。
もちろん全機能試したわけではないので「完全に動く」と断言まではできないのですが。

当初はフレームワークの選択肢が少ないか最悪利用が難しいのではと考えていただけに、
有名なフレームワークが動作したことでGAE/PHP上で動作させるアプリを新規に開発するにあたっての懸念が一つ減ったように感じています。

ついでに#Devfestの時には「mbstring が使えない」件も懸念されていましたが、
Google App Engine Ver.1.8.1のリリース時にmcrypt、iconv と一緒にこれも追加されています。

GAE/PHPでは拡張モジュールを自分で追加することはできませんが、
mbstringmcrypticonv は Issue Tracker に挙げられた要望が取り入れられた結果としてGAE/PHPに標準で導入されました。
「多くの人にとって価値があるような汎用的なモジュール」であれば、
要望を挙げることによって追加されるかもしれません。


今回試してみた感想として、
・JavaでDIをするように簡単にI/Oの対象を切り替える仕組みを最初から持っているCakePHPは良い
・ちょっとググれば日本語の情報が大量に見つかるという点でPHPという言語は良い(情報の得やすさはJava以上かも?)
・ローカル環境は制限が緩いのでフレームワークの基本動作や既存アプリは実際にデプロイして動作確認した方が良い
と思いました。


余談ですが、公式の Issue Trackerでのフレームワークや拡張モジュール関連の話題としては、
Phalcon Framework というCの拡張モジュールで書かれたPHP用の超高速フレームワークを入れてくれ」
というスレッドが盛り上がっているようです。
(現時点でスターの数はダントツ。盛り上がりすぎたのかコメント禁止ステータスになっていますがw)



◯参考リンク

Installing the PHP SDK on Mac OS X - Google App Engine — Google Developers
CakePHPインストール - CakePHPの使い方
Ayman R. Bedair: Google App Engine
Re: Anyone tried installing CakePHP on Google App Engine (since PHP now support)
CakePHP DebugKit の導入手順
APCとmemcachedの比較 : アシアルブログ
「PHP」の「PECL::APC」をキーバリュー型のメモリキャッシュとして使う。 – FlatLabs
セッション — CakePHP Cookbook v2.x documentation
[ステップアップ! CakePHP] キャッシュに memcached を使う | バシャログ。
CakePHP入門 (全32回) - プログラミングならドットインストール
Issue 9336:Install the Phalcon Framework PHP extension


0 件のコメント:

コメントを投稿