Ethna_DBでfetchArrayのようなことをする

2010.03.03    Ethna, PHP, 馬場   タグ: —    baba   

PHPでmysql_queryの結果オブジェクトは、mysql_fetch_row()しても良いですが、mysql_fetch_array()した方が便利ですよね。
※mysql_fetch_row()では、結果を通常の配列で取得しますが、mysql_fetch_array()では、カラム名をキーにした連想配列で取得できます

EthnaでAppObjectを使わずにEthna_DBを直接たたく場合、結果オブジェクトはPEAR::DB_Resultオブジェクトです。
これはfetchRow()のメソッドを持ちますが、fetchArray()は存在しません。
http://pear.plus-server.net/package.database.db.db-result.fetchrow.html

カラム名をキーにした連想配列が欲しい場合、queryを投げる前にsetFetchMode(DB_FETCHMODE_ASSOC)を実行しておく必要があります。
http://pear.plus-server.net/package.database.db.db-common.setfetchmode.html

EthnaのデフォルトDBアダプタ、DB_PEARは、内部にPEAR::DBを持っているので、以下のようなコードを書けばOKです。

$db = $this->backend->getDB();
$db->db->setFetchMode(DB_FETCHMODE_ASSOC);
$res = $db->query($sql);
print_r($res->fetchRow());

http://ethna.jp/ethna-document-dev_guide-db.html
Ethnaの デフォルトの DB接続クラス(Ethna_DB_PEAR) はPEAR::DBを継承しているので
と書いてありますが、継承ではなくて委譲なので、$db->db->setFetchMode()となります。

Ethnaコマンド

2010.02.15    Ethna, 馬場      baba   

WindowsのXAMPP環境でEthnaを使う際、Ethnaコマンドの設定でよく戸惑います。

プロジェクトの環境依存を減らすため、Ethna自体をプロジェクトのlibフォルダに入れるのが好きなのですが、そのような環境だと

  • add-project で Could not open input file: ~\Ethna\bin\\bin\ethna_handle.php となどのエラー
  • add-action で Fatal error: Could not redeclare class Ethna などのエラー

が出ることがあります。

私の使い方では、以下のような設定に落ち着きました。

共通の設定
phpにパスを通します。xampp/php のフォルダを環境変数のPATHで設定します。
ethna.bat があるフォルダにはパスを通さない方が良いです。

add-project をするとき
Ethnaをダウンロードして、workspaceに設置します。
workspace/Ethna/bin/ethna.bat を、workspaceにコピーします。

workspace/ethna.bat を開き、@PEAR-DIR@ を .\Ethna に一括置換します(2カ所)。

これで、コマンドプロンプトで、workspaceに入り、
C:\Users\baba\workspace > ethna add-project hoge
と実行できます。

add-action などをするとき
add-project 以外では、Ethnaのプロジェクトのディレクトリにいる必要があります。
hogeプロジェクトがある場合、workspace/hoge/lib フォルダに、Ethna(とSmarty)を設置しておきます。

workspace/hoge/lib/Ethna/bin/ethna.bat を、workspace/hoge にコピーします。
workspace/hoge/ethna.bat を開き、@PEAR-DIR@ を、 .\lib\Ethna に一括置換します(2カ所)。

これで、コマンドプロンプトでhogeディレクトリに入り、
C:\Users\baba\workspace\hoge > ethna add-action hoge_index
と実行できます。

以上で実行できますが、正直面倒なので、Eclipseプラグインを作ろうと思います。

Ethnaでデフォルトアクション名を変更する

久々のEthna小ネタです。

Ethnaでのデフォルトアクション名はAppID_Action_Hogeのようなスタイルのアクション名ですが、これを変更したい場合のTIPSです。

例えば、AppID_HogeActionのようにアクションのクラス名を変更したいと思います。

AppID_Action_Hoge => AppID_HogeAction

その場合は下記のようにAppID_Controller.php内にgetDefaultActionClassメソッドをオーバーライドすることで簡単に実現できます。
ちなみに、アクションをコマンドから作る場合にコントローラに下記のコードを書いておくだけで命名規則が変更されるため、アクションを作る前にコントローラに実装しておくことがお薦めです。
また、ビュー(View)やアクションフォーム(ActionForm)などの命名規則も同様にgetDefaultViewClass, getDefaultFormClassをオーバーライドするだけです。

function getDefaultActionClass($action_name, $gateway = null)  {
  $gateway_prefix = $this->_getGatewayPrefix($gateway);

  $postfix = preg_replace('/_(.)/e', "strtoupper('\$1')", ucfirst($action_name));
  $r = sprintf("%s_%s%sAction", $this->getAppId(), $gateway_prefix ? $gateway_prefix . "_" : "", $postfix);
  $this->logger->log(LOG_DEBUG, "default action class [%s]", $r);

  return $r;
}

Ethnaでユーザ権限ごとのアクセス許可を一括管理する

2009.07.02    Ethna, PHP, 馬場   タグ: —    baba   

Ethnaでアクセス制限をする場合、一般的にActionクラスのauthenticate()メソッドを使います。
アクション数が多くなってくると、個別に書くのは面倒なので、継承を駆使する場合が多いと思います。
しかしそれでも、ユーザ権限が何種類もあると、管理が煩雑になってしまいます。
今回は、ユーザ権限ごとのアクセス許可を一元管理する方法を紹介します。

まず、ユーザ種別はdefineしてあるものとします。

define(’USER_TYPE_ANONYMOUS’, 0);
define(’USER_TYPE_GUEST’,     1);
define(’USER_TYPE_MEMBER’,    2);
define(’USER_TYPE_ADMIN’,     3);

次に、ユーザ種別ごとのアクセス許可を、適当なファイルにグローバル変数で記述します。
グローバル変数にするメリットは、index.phpからControllerを生成する前に読み込めるため、BASIC認証と連動しやすい点です。
このアクセス許可一覧変数では、ユーザ種別定数をキーに、アクセス許可するアクション名一覧の配列を値にします。

require_once ‘Appid_Const.php’; //USER_TYPE_*をdefineしてあるファイルをrequire
/**
* 権限毎のアクセス可能アクション一覧
* アクション名の最後の1文字のみ、ワイルドカードとして*(アスタリスク)を使える
*/
$accept_action_list = array(
    USER_TYPE_ADMIN =< array(
        ’*',
    ),
    USER_TYPE_MEMBER =< array(
        ’entry_*’,
        ’comment_*’,
        ’inquiry_*’,
    ),
    USER_TYPE_GUEST =< array(
        ’comment_*’,
        ’inquiry_*’,
    ),
    USER_TYPE_ANONYMOUS =< array(
        ’index’,
        ’help’,
    ),
);

また、Appid_Sessionには、getUserType()というメソッドを作り、ユーザ種別を取得できるようにしておきます。

そして、以下のメソッドを、Appid_ActionClassに入れます。

/**
* このアクションにアクセスできるかどうか調べる
* @return アクセスできるときtrue, 出来ないときfalse
*/
function _authenticate()
{
    global $accept_action_list;

    $controller =& $this->backend->getController();
    if ($controller->_isAcceptableActionName($controller->getCurrentActionName(), $accept_action_list[USER_TYPE_ANONYMOUS])) {
        //ログインなしでアクセスできるアクションの場合
        return true;
    } else if ($this->session->isStart()) {
        $userType = $this->session->getUserType(); //現在の権限を取得
        if (is_null($userType)) {
         $this->ae->add(null, ‘ログインしていません’);
         return false;
        }

        $list = $accept_action_list[$userType]; //現在の権限でアクセスできるアクション
        if (!$controller->_isAcceptableActionName($controller->getCurrentActionName(), $list)) {
            $this->ae->add(null, ‘権限がありません’);
            return false;
        } else {
            return true; //認証された
        }
    } else {
        $this->ae->add(null, ‘ログインしていません’);
        return false;
    }
}

あとは、authenticate()メソッドで

function authenticate()
{
    if ($this->_authenticate() === true)
    {
        return null;
    }
    return ‘index’; //ログイン画面へ飛ばす
}

とすれば完了です。

全アクションの許可・拒否を一括管理できるので、漏れが無くて安心です。

getObjectListの罠

2009.06.05    Ethna, PHP, 馬場      baba   

昨日に引き続きEthnaネタです。今回は当たり前のことですが。。

AppManagerのgetObjectListを使う場合に、

$filter = array(’number’ => ‘0123′);

などと、省略記法を使う場合が多いと思います。
デフォルトではstring型はLIKE扱いになるので、

WHERE number LIKE ‘%0123%’

というSQLが生成されてしまいます。これでは、郵便番号や会員番号などで検索する場合などに困ります。

もちろん、

$filter = array(’number’ => new Ethna_AppSearchObject(’0123′, OBJECT_CONDITION_EQ);

とすれば良いのですが、たぶんEQの方がよく使うので、めんどくさいです。

なので、getObjectListをオーバーライドして、省略時はEQになるようにしました。

function getObjectList($class, $filter=null, $order=null, $offset=null, $count=null)
{
if (is_array($filter)) {
foreach ($filter as $key => $value) {
if (is_string($value)) {
$filter[$key] = new Ethna_AppSearchObject($value, OBJECT_CONDITION_EQ);
}
}
}
return parent::getObjectList($class, $filter, $order, $offset, $count);
}

AppObjectのupdateを改良

2009.06.04    Ethna, PHP, 馬場      baba   

EthnaのAppObjectネタです。

add()したあと、auto_incrementで決定されたIDに応じて会員番号を生成する、といったときに、AppObjectのadd()をオーバーライドして、

class Appid_UserAppObject extends Appid_AppObject
{
//オーバーライド
function add()
{
parent::add();

//会員番号numberは、t+6桁の連番とする
$this->set(’number’, sprintf(’t%06d’, $this->get(’id’)));
$this->update();
}
}

としたいものですが、これだとupdateの時にSQLエラーが出ることがあります。
このエラーは、プロパティにNULLが含まれているときに、中途半端なSQLが生成されているのが原因です。

これに対処するには、Appid_AppObjectで、以下の関数をオーバーライドします。

/**
* オブジェクトプロパティを更新するSQL文を構築する
*
* @access private
* @return オブジェクトプロパティを更新するためのUPDATE文
*/
function _getSQL_Update()
{
$tables = implode(’,',
$this->my_db_rw->quoteIdentifier(array_keys($this->table_def)));

// SET句構築
$set_list = “”;
$prop_arg_list = $this->prop;
Ethna_AppSQL::escapeSQL($prop_arg_list, $this->my_db_type);
foreach ($this->prop_def as $k => $v) {
//この下のif文を追加
if (isset($prop_arg_list[$k]) && $prop_arg_list[$k] !== null && $prop_arg_list[$k] !== ”) {
if ($set_list != “”) {
$set_list .= “,”;
}
$set_list .= sprintf(”%s=%s”,
$this->my_db_rw->quoteIdentifier($k),
$prop_arg_list[$k]);
}
}

// 検索条件(primary key)
$condition = null;
foreach (to_array($this->id_def) as $k) {
if (is_null($condition)) {
$condition = “WHERE “;
} else {
$condition .= ” AND “;
}
$v = $this->prop_backup[$k]; // equals to $this->id
Ethna_AppSQL::escapeSQL($v, $this->my_db_type);
$condition .= Ethna_AppSQL::getCondition(
$this->my_db_rw->quoteIdentifier($k), $v);
}

$sql = “UPDATE $tables SET $set_list $condition”;

return $sql;
}

UPDATE文を生成するときに、値がセットされていないものをSETしないようにif文を追加しただけです。

EthnaのimportFormの強化版

2009.04.15    Ethna, PHP, Web, 馬場      baba   

EthnaのAppObject::importForm()は便利ですが、DBの全カラムと同じ名前のform値を取得してしまうため、たとえばzipcodeはハイフン前後で2つのFormを用意して結合した値をセットする、等の処理が少々やりにくいです。
また、どの値がインポートされるのか見えにくいのが何となく気持ち悪い、という人もいると思います。

そこで、AppObjectに下記のようなメソッドを作ってみました。
実際には全AppObjectの基底クラスにすると良いと思います。

class AppId_Hoge extends Ethna_AppObject
{
  function setAll($list, $option = null)
  {
    foreach ($list as $key => $data) {
      $name = ”;
      $value = ”;
      if (is_int($key)) {
        //連想配列のキーが数値の場合は、連想配列の値と同名のform値をセットする
        $name = $data;
        $value = $this->af->get($data);

        //値がNULLの場合、オプション指定によって、無視するか空文字列に変換するかを決定する
        if (is_null($value)) {
          if ($option == OBJECT_IMPORT_IGNORE_NULL) {
            //NULLを無視
            continue;
          } else {
            $value = ”;
          }
        }
      } else {
        //それ以外の場合、連想配列のキーがそのまま名前に、連想配列の値がそのまま設定すべき値になる
        $name = $key;
        $value = $data;
      }

      if (isset($this->prop_def[$name]) == false) {
        Ethna::raiseError(”Unknown property $name”);
        return null;
      }
      $this->set($name, $value);
    }
  }
}

使うときは

$list = array(
  ’user_name’,
  ’password’,
  ’zipcode’ => $this->af->get(’zipcode1′).$this->af->get(’zipcode2′)
);
$obj =& $this->backend->getObject(’Hoge’);
$obj->setAll($list);

のようにすると、単純に配列指定したuser_nameとpasswordはForm値が直接セットされ、連想配列になっているzipcodeは、$list['zipcode']の値がセットされます。
これによって、ほとんどの値はActionFormからそのままセットするが、任意のフィールドだけを特別な値にする、といった処理が簡単になります。

Ethnaでレイアウト

    Ethna, PHP, Web, 馬場   タグ: —    baba   

割と既出ネタですが。。。

Ethnaでも、CakePHPなどのようなレイアウトテンプレートを使いたくなります。
ViewClassのforwardを変更すると、意外と簡単に実現できました。
Controllerの$directoryに’layout’のパスをセットして、ViewClassのforwardを下記のようにオーバーライドします。

class AppId_ViewClass extends Ethna_ViewClass
{
function forward()
{
//Rendererを取得
$renderer =& $this->_getRenderer();
$this->_setDefault($renderer);

//各Viewのコンテンツを取得
$contents = $renderer->perform($this->forward_path, true);


//レイアウトを取得
$layout = $renderer->getProp(’layout_file’);
if ($layout) {
$controller =& $this->backend->getController();
$dir = $controller->getDirectory(’layout’);
$layout_file = $controller->getDirectory(’layout’) . ‘/’ . $layout . ‘.’ . $controller->getExt(’tpl’);
$renderer->setProp(’contents’, $contents);
$renderer->perform($layout_file);
} else {
echo $contents;
}
}

これで、各tplで
{assign var=”layout_file” value=”default”}
と指定すれば、default.tplが呼ばれて、default.tpl内の {$contents} のところに各Viewのコンテンツが挿入されます。

レイアウトファイルに各Viewのテンプレート内から値を渡したいときは、{assign}を使えばOKです。
tplの見通しが良くなって、便利ですよ。

Ethna2.5以降でTemplateのディレクトリ・パスを変えたい場合

2009.03.15    Ethna, PHP, フレームワーク, 中井      hiko   

ethna2.3.6までは、デフォルトのTemplateディレクトリ(template/ja)から変更したい時に{App_Id}_ControllerでgetTemplatedirをオーバーライドするのが定石でした。ethnaコマンドもgetTemplatedirを見てくれるのでethna add-templateなども問題ありませんでした。

ただ、Ethna-2.5以降では、国際化対応のためLocaleが追加され、以下のジェネレータプラグインのコードのコメント通り(39,40行目)、Localeが入っていないと勝手に補完してくれるようです。このため、ethna add-templateコマンドでtemplateを生成する際にLocaleをディレクトリパスに強制的に入れられてしまうため、国際化対応したくない場合(笑)はここをスキップする必要があります。

今回は綺麗に拡張するのが面倒だったためコメントアウトしましたが、今後は何らかの切り替えができるといいかもしれませんね。

Ethna/class/Plugin/Generator/Ethna_Plugin_Generator_Template.php

13 /**
14  *  スケルトン生成クラス
15  *
16  *  @author     Masaki Fujimoto <fujimoto@php.net>
17  *  @access     public
18  *  @package    Ethna
19  */
20 class Ethna_Plugin_Generator_Template extends Ethna_Plugin_Generator
21 {
22     /**
23      *  テンプレートのスケルトンを生成する
24      *
25      *  @access public
26      *  @param  string  $forward_name   テンプレート名
27      *  @param  string  $skelton        スケルトンファイル名
28      *  @param  string  $locale         ロケール名
29      *  @param  string  $encoding       エンコーディング
30      *  @return true|Ethna_Error        true:成功 Ethna_Error:失敗
31      */
32     function &generate($forward_name, $skelton = null, $locale, $encoding)
33     {
34         //  ロケールが指定された場合は、それを優先する
35         if (!empty($locale)) {
36             $this->ctl->setLocale($locale);
37         }
38
39         //  ロケール名がディレクトリに含まれていない場合は、
40         //  ディレクトリがないためなのでそれを補正
41         $tpl_dir = $this->ctl->getTemplatedir();
42         if (!empty($locale) && strpos($tpl_dir, $locale) === false) {
43             $tpl_dir = $this->ctl->getDirectory(’template’);
44             $tpl_dir .= “/$locale”;
45         }
46         if ($tpl_dir{strlen($tpl_dir)-1} != ‘/’) {
47             $tpl_dir .= ‘/’;
48         }

COPYRIGHT [C] 2009 BEYOND PERSPECTIVE SOLUTIONS LTD. ALL RIGHTS RESERVED.