出勤記録モジュールを作ってみる
出典: SugarForum.jp
このドキュメントの目的は、最小限のモジュールの作成を通じて、モジュール開発の概要を理解することです。モジュールの例として、出退勤を管理するものを作成します。
(なお、PHPやSQLの理解を前提として、Module Builderを使わない方法の説明となります)
目次 |
はじめに
最近は、カードをかざすだけのところも多いようですが、中小ではまだまだ「タイムカード」が現役だと思います。ここでは、Sugar上でその機能を実現したいと思います。さすがに「カードをかざせば...」というわけにはいきませんが、
- ユーザがSugarにログインして
- ボタンをひとつ押せば、出勤が記録され
- もう一度ボタンを押せば、退勤が記録される
ことを目指します。
作業の流れとしては、
の順で進めて行きます。一通りのファイルがそろったところで、パッケージにまとめて「モジュールローダ」でインストール可能なものに仕上げます。
開発環境の確認
実際にコードを書く前に、開発環境の確認をしておきましょう。
- テキストエディタ (UTF-8が扱えるもの)
- ZIP圧縮ソフト (パッケージ作成時)
があれば、OSはWindowsでもMacでもLinuxでも構いません。テキストエディタは各種ありますが、Windowsの「メモ帳」などはUTF-8という文字コードに対応しないので避けます。統合環境(Eclipseなど)を使う、あるいは秀丸(Windows)やEmEditor(Windows)などのテキスト中心のエディタでもOKです。(筆者はMacでskEditを利用しています)
- 文字コード : UTF-8 (BOMなし)
- 改行コード : LF
- インデント : 特に決まりなし (筆者はタブを推奨)
改行コードについて標準ではLFのようですが、PHPが解釈可能なら構わないので、CR+LF(Windows標準)でもOKです。インデントについては半角スペースあるはタブの両方が使われています。(このあたり、Sugarに関してコーディング規約の整備が望まれるところです)
モジュールに必要なファイル
作成するファイルを説明順にリストアップしておきます。
- verdefs.php
- ClockCard.php
- metadata/
- detailviewdefs.php
- editviewdefs.php
- listviewdefs.php
- searchdefs.php
- SearchFields.php
- sidecreateviewdefs.php
- language/
- ja.lang.php
- views/
- view.edit.php
- Forms.php
- Menu.php
上記、最低限の要素にとどめていますが、だいたい10個強のファイルがあれば、モジュールを構成できることになります。(初めて作成するのに「10ファイル」というのは、ハードルが高く感じられるかもしれません。ただ、ほとんどは定型な内容ですので、一度理解してしまえばそれほどの手間ではありません)
Vardefs
最初に作成するのは、Vardefsファイルです。Sugarでは、このVardefsを元にデータベース上にテーブルが作成されます。 さて、タイムカードとして記録するべき情報ですが、
- 何月何日
- 誰が
- 何時に出勤したか
- 何時に退勤したか
- 備考
があればよいでしょうか。(※備考は、電車が遅延したとかの言い訳を記入する欄です)
それぞれフィールドのタイプを考えてみましょう。
- 何月何日 → 日付
- 誰が → リレーション
- 何時に出勤したか → 時刻
- 何時に退勤したか → 時刻
- 備考 → テキスト
Sugarを利用しているユーザ(Usersモジュール)の「誰が」出退勤したかを記録すればいいので、Usersとの「リレーション(関連)」ということになります。
フィールド名も決めてしまいましょう。
- 何月何日 → date (date型)
- 誰が → assigned_user_id (id型)
- 何時に出勤したか → punch_in (time型)
- 何時に退勤したか → punch_out (time型)
- 備考 → description (text型)
date, time, text は見慣れたSQLのカラム型ですが、id型はSugarでの特殊な型です。ここではどのユーザかを示します。(なお、id型が実際にデータベースに格納される時には、varchar型となります)
フィールドはこれだけあれば良さそうですが、実際には
- date_entered : 入力日時
- date_modified : 更新日時
- created_by : 作成ユーザ
- modified_user_id : 更新ユーザ
- deleted : ゴミ箱にはいっているか (削除済みか)
なども必要になってきます。ただ、幸いなことに、これらの基本的なフィールドは予め「テンプレート」として用意されていますので、一々定義する必要はありません。さらに、この例では description と assigned_user_id もテンプレートに含まれているので、必要な定義は以下の3つだけです。
- date (date型)
- punch_in (time型)
- punch_out (time型)
それでは、これらをVardefsファイルに落とし込んでいきましょう。以下は、実際のvardefsコードの断片です。
'date'=>array( 'name'=>'date', 'vname'=>'LBL_DATE', 'type'=>'date', 'audited'=>true, 'comment'=>'日付', ),
配列の中身が示すものは次の通り。
- 'name' : フィールド名
- 'vname' : フィールドのラベル
- 'type' : カラム型
- 'audited' : 監査の有無
- 'comment' : コメント
フィールドのラベル'LBL_DATE'の部分は、後述の言語ファイルによって「日付」といった各言語ごとの名称に置き換えられます。監査を「有り(true)」にしておくと、フィールドの改変履歴を保持するようになります。(タイムカードは賃金換算にかかわりますから、ここではtrueにして改竄などに備えます)
残り二つのフィールドも、次のようになります。
'punch_in'=>array( 'name'=>'punch_in', 'vname'=>'LBL_PUNCH_IN', 'type'=>'time', 'audited'=>true, 'comment'=>'出勤時刻', ), 'punch_out'=>array( 'name'=>'punch_out', 'vname'=>'LBL_PUNCH_OUT', 'type'=>'time', 'audited'=>true, 'comment'=>'退勤時刻', ),
以上が、フィールド定義の抜粋になります。今度は、テーブル全体について設定を考えましょう。
- 'table' : テーブル名
- 'audited' : このテーブルで監査を有効にするかどうか
- 'comment' : コメント
- 'fields' : 前述のフィールド定義
- 'indices' : データベースのインデックス
- 'relationships' : 他モジュールとの関係の定義
基本的に、テーブル名はモジュール名を小文字にしたものを用います。ここでは、
- テーブル名 : clockcards
- モジュール名 : ClockCards
- クラス名 : ClockCard
として、説明します。複数形と単数形を用いている点に注意してください。
以上をすべてまとめると、以下のvardefsが出来上がります。
<?php
#ファイル名: vardefs.php
$dictionary['ClockCard'] = array(
'table'=>'clockcards',
'audited'=>true,
'unified_search'=>false,
'duplicate_merge'=>false,
'comment'=>'出勤記録',
'fields'=>array (
'date'=>array(
'name'=>'date',
'vname'=>'LBL_DATE',
'type'=>'date',
'audited'=>true,
'comment'=>'日付',
),
'punch_in'=>array(
'name'=>'punch_in',
'vname'=>'LBL_PUNCH_IN',
'type'=>'time',
'audited'=>true,
'comment'=>'出勤時刻',
),
'punch_out'=>array(
'name'=>'punch_out',
'vname'=>'LBL_PUNCH_OUT',
'type'=>'time',
'audited'=>true,
'comment'=>'退勤時刻',
),
),
'indices'=>array(
array(
'name'=>'idx_clockcard_assigned_user_del',
'type'=>'index',
'fields'=>array('assigned_user_id', 'deleted')
),
),
'relationships' => array (),
'optimistic_locking'=>true,
);
VardefManager::createVardef(
'ClockCards',
'ClockCard',
array('basic', 'assignable'),
'clockcard'
);
?>
上記、「$dictionary['ClockCard'] = ~」で始まっていますが、単数形のクラス名「ClockCard」が指定されていることに注意してください。インデックス(indeices)の項について、詳細は省きますが、検索速度を上げるためにいくつかインデックス設定の必要があるかもしれません。扱う件数が少ないうちはそれほど気にする必要はありませんが、数千件以上のレコードを扱うようなケースでは、インデックスを正しく設けることが大切になってきます。
また、後述のテンプレートで既に設定されているので、relationshipsには空配列を指定して設定を省略しています。最後に関数「VardefManager::createVardef()」を呼び出していますが、ここで重要なのはbasicとassignableなどのテンプレートを継承するという指定です。basicには、作成日時や作成者などの基本情報が、assignableにはアサイン先についての情報が定義されています。詳しくは、それぞれのソースコードを見ると良いでしょう。
- SugarRoot/include/SugarObjects/templates/basic/vardefs.php
- SugarRoot/include/SugarObjects/implements/assignable/vardefs.php
SugarBean
モジュールには通常、1つあるいは複数のBeanファイルが含まれます。BeanファイルにはSugarBeanを継承したクラスが記述され、ファイル名は「クラス名.php」の形式をとります。ここでは、クラス名をClockCardとしますので、ファイル名は「ClockCard.php」となります。クラスの内容は、メソッドの定義内容を省くと次のようになります。
<?php
require_once('data/SugarBean.php');//SugarBean
require_once('include/utils.php');//各種のユーティリティ関数などが定義されている
class ClockCard extends SugarBean {
var $table_name = "clockcards";//テーブル名
var $module_dir = 'ClockCards';//モジュールディレクトリ
var $object_name = "ClockCard";//Beanの名前(=クラス名)
var $new_schema = true;//基本的にtrue
function ClockCard(){}//コンストラクタ
function get_summary_text(){}//履歴に表示される
function fill_in_additional_list_fields(){}//リストビューで、関係フィールドなどを補完
function fill_in_additional_detail_fields(){}//詳細ビューで、関係フィールドなどを補完
function get_list_view_data(){}//リストビューの表示内容の調整
function bean_implements($interface){}//ACL設定など
function save($check_notify = FALSE){}//保存時のデータ補正など
}
?>
コンストラクタ
親クラス(SugarBean)のコンストラクタを呼び出せばOKです。クラス変数が必要な場合は、PHPの作法に従ってここで代入しておくと良いでしょう。
function ClockCard() {
parent::SugarBean();
}
get_summary_text()
オブジェクトの概略を返すメソッドです。Contactsモジュールなら氏名、Accountsモジュールなら会社名といった具合に、そのオブジェクトの代表的なフィールドを返すのが普通です。ここでは、出勤簿に記名した人の名前として、単にassigned_user_nameを返していますが、「山田-10/25」といった具合に追加情報を付加することも可能です。
function get_summary_text() {
return $this->assigned_user_name;
}
fill_in_additional_list_fields()
ここでは、親クラスのメソッドを呼び出すだけにします。
function fill_in_additional_list_fields() {
parent::fill_in_additional_list_fields();
}
fill_in_additional_detail_fields()
関係フィールドなどの内容を補完します。出勤者としてアサイン先ユーザ名(assigned_user_name)、作成者名(created_by_name)、更新者名(modified_by_name)などの情報は、clockcards以外のテーブルにあるため、それらの情報を取得して代入しています。
また、新規にオブジェクトを作成した際に、デフォルトで日付(date)と出勤時刻(punch_in)が設定されます。
function fill_in_additional_detail_fields() {
global $timedate;
parent::fill_in_additional_detail_fields();
$this->assigned_user_name = get_assigned_user_name($this->assigned_user_id);
$this->created_by_name = get_assigned_user_name($this->created_by);
$this->modified_by_name = get_assigned_user_name($this->modified_user_id);
if (is_null($this->date))
$this->date = $timedate->to_display_date(gmdate($timedate->get_db_date_time_format()));
if (is_null($this->punch_in))
$this->punch_in = $timedate->to_display_time(gmdate($timedate->get_db_date_time_format()));
}
get_list_view_data()
退勤時間(punch_out)が未設定の場合、つまりまだ退勤していない場合だけ、退勤時間の代わりに「退勤」ボタンを表示します。これは、リストビューからワンクリックで退勤の処理ができるようにする工夫です。
function get_list_view_data() {
global $app_list_strings, $action, $currentModule, $image_path;
$temp_array = $this->get_list_view_array();
if (empty($temp_array['PUNCH_OUT'])){
$temp_array['PUNCH_OUT'] = "<button class=\"button\" onclick=\"
var dd=new Date();
location.href='index.php
?return_module=$currentModule&return_action=$action
&action=EditView&module=$currentModule&record={$this->id}
&punch_out='+dd.getHours()+':'+dd.getMinutes();
return false;\">退勤</button>";
}
return $temp_array;
}
bean_implements()
ここでは、ACL(アクセスコントロールリスト)の設定をします。
function bean_implements($interface){
switch($interface){
case 'ACL': return true;
}
return false;
}
save()
データベースにオブジェクトの内容を保存するメソッドです。ここでは、とりあえず親クラスのメソッドを呼び出しています。フィールドの内容の調整が必要な場合は、後でコードを追加します。
function save($check_notify = FALSE){
return parent::save($check_notify);
}
メタファイル
Sugarでは、設定の多くをメタファイルという形で提供し、後のカスタマイズがしやすいフレームワークをとっています。詳細ビューや編集ビューなどの画面レイアウト、検索クエリの設定などが含まれます。
(これらは、SugarStudioでユーザレベルでもカスタマイズが可能ですが、SugarStudioで加えた変更はモジュールディレクトリ内のファイルには影響せず、customディレクトリ内にメタファイルの(変更済みの)コピーが作成されます)
detailviewdefs.php
詳細ビューのレイアウトを設定するファイルです。フィールドの配置順は、'panels' 配列に指定します。
'panels' => array(
'default' => array( //パネル名
array( //行
'field1', //1列目のフィールド
'field2', //2列目のフィールド
),
),
),
配列は、パネル > 行 > 列 > フィールド という階層になっています。また、フィールドは文字列だけを指定するとデフォルトの表示方法になり、
'field1',
配列で指定すると、表示方法をコントロールすることが可能です。以下の例だと、'customCode' を使って表示文字列が赤くなるよう設定しています。(なお、customCode内はSmartyの文法が有効です)
array (
'name' => 'field1',
'label' => 'LBL_FIELD1',
'customCode' => '<span style="color:red">{$fields. field1.value}</span>',
),
上記をふまえて、出勤記録モジュールのレイアウト(詳細ビュー)は、以下のようになります。
$viewdefs['ClockCards']['DetailView'] = array(
'templateMeta' => array(
'form' => array(
'buttons' => array( //上部に表示されるボタン
'EDIT', //編集ボタン
'DELETE', //削除ボタン
),
),
'maxColumns' => '2', //詳細ビューを2列構成に
'widths' => array(
array('label' => '10', 'field' => '30'),
array('label' => '10', 'field' => '30'),
),
),
'panels' =>array (
'default'=>array(
array (
'assigned_user_name', //出勤者
null,
),
array (
'date', //出勤日
null,
),
array (
'punch_in', //出勤時刻
array (
'name' => 'modified_by_name', //更新情報
'label' => 'LBL_DATE_MODIFIED',
'group'=>'modified_by_name',
'customCode' => '{$fields.date_modified.value}
{$APP.LBL_BY} {$fields.modified_by_name.value} ',
),
),
array (
'punch_out', //退勤時刻
array (
'name' => 'created_by_name', //作成情報
'label' => 'LBL_DATE_ENTERED',
'group'=>'created_by_name',
'customCode' => '{$fields.date_entered.value}
{$APP.LBL_BY} {$fields.created_by_name.value} ',
),
),
array (
'description', //備考
),
),
)
);
editviewdefs.php
$viewdefs['ClockCards']['EditView'] = array (
'templateMeta' => array (
'maxColumns' => '2',
'widths' => array(
array('label' => '10', 'field' => '30'),
array('label' => '10', 'field' => '30')
),
),
'panels' => array (
'default' => array(
array (
'assigned_user_name',
),
array (
'date',
),
array (
array(
'name' => 'punch_in',
'displayParams' => array (
'size' => 8,
'maxlength' => 5,//時刻なので最大5桁
),
),
),
array (
array(
'name' => 'punch_out',
'displayParams' => array (
'size' => 8,
'maxlength' => 5,//時刻なので最大5桁
),
),
),
array (
array (
'name' => 'description',
'displayParams' => array (
'rows' => '8',
'cols' => '80',
),
'nl2br' => true,
),
),
),
)
);
listviewdefs.php
$listViewDefs['ClockCards'] = array(
'DATE' => array(
'width' => '10',
'label' => 'LBL_DATE',
'default' => true,
),
'ASSIGNED_USER_NAME' => array(
'width' => '20',
'label' => 'LBL_ASSIGNED_USER_NAME',
'link' => true,
'default' => true,
),
'PUNCH_IN' => array(
'width' => '20',
'label' => 'LBL_PUNCH_IN',
'default' => true,
),
'PUNCH_OUT' => array(
'width' => '20',
'label' => 'LBL_PUNCH_OUT',
'default' => true,
),
'DESCRIPTION' => array(
'width' => '40',
'label' => 'LBL_DESCRIPTION',
'default' => true,
),
);
searchdefs.php
$searchdefs['ClockCards'] = array(
'templateMeta' => array(
'maxColumns' => '3',
'widths' => array('label' => '10', 'field' => '30'),
),
'layout' => array(
'basic_search' => array(
'date',
array(
'name' => 'punch_in',
'label' => 'LBL_PUNCH_IN',
'displayParams' => array('size'=>8, 'maxlength'=>5),//時刻なので最大5桁
),
array(
'name'=>'current_user_only',
'label'=>'LBL_CURRENT_USER_FILTER',
'type'=>'bool'
),
),
'advanced_search' => array(
'date',
array(
'name' => 'punch_in',
'label' => 'LBL_PUNCH_IN',
'displayParams' => array('size'=>8, 'maxlength'=>5),//時刻なので最大5桁
),
array(
'name' => 'punch_out',
'label' => 'LBL_PUNCH_OUT',
'displayParams' => array('size'=>8, 'maxlength'=>5),//時刻なので最大5桁
),
array(
'name' => 'assigned_user_id',
'type' => 'enum',
'label' => 'LBL_ASSIGNED_TO',
'function' => array('name' => 'get_user_array', 'params' => array(false)),
'displayParams' => array('size'=>1),//この設定はSugar5.1から有効
),
array(
'name' => 'description',
'label' => 'LBL_DESCRIPTION',
'type' => 'name',
),
),
),
);
SearchFields.php
$searchFields['ClockCards'] = array (
'description' => array('query_type'=>'default'),
'punch_in' => array('query_type'=>'default'),
'punch_out' => array('query_type'=>'default'),
'current_user_only'=> array(
'query_type'=>'default',
'db_field'=>array('assigned_user_id'),
'my_items'=>true,
),
'assigned_user_id'=> array('query_type'=>'default'),
);
sidecreateviewdefs.php
global $mod_strings;
$viewdefs['ClockCards']['SideQuickCreate'] = array(
'templateMeta' => array(
'form'=>array(
'hidden'=>array('<input type="hidden" name="return_action" value="index">'),
'buttons'=>array('SAVE'),
'button_location'=>'bottom',
'headerTpl'=>'include/EditView/header.tpl',
'footerTpl'=>'include/EditView/footer.tpl',
),
'maxColumns' => '1',
'panelClass'=>'none',
'labelsOnTop'=>true,
'widths' => array(
array('label' => '10', 'field' => '30'),
),
),
'panels' =>array (
'DEFAULT' => array (
array(
array(
'name'=>'date',
'label'=>$mod_strings['LBL_LIST_DATE'],
'displayParams' => array (
'required'=>true,
'size' => 16,
'maxlength' => 10,
),
),
),
array(
array(
'name'=>'punch_in',
'label'=>$mod_strings['LBL_LIST_PUNCH_IN'],
'displayParams' => array (
'required'=>true,
'size' => 8,
'maxlength' => 5,
),
),
),
array (
array(
'name'=>'assigned_user_name',
'label'=>$mod_strings['LBL_LIST_ASSIGNED_USER_NAME'],
'displayParams'=>array('required'=>true, 'size'=>11, 'selectOnly'=>true),
),
),
array (
array (
'name' => 'description',
'displayParams' => array (
'rows' => '4',
'cols' => '24',
),
'nl2br' => true,
),
),
),
)
);
言語ファイル
ja.lang.php
$mod_strings = array ( 'LBL_MODULE_NAME' => '出勤記録', 'LBL_MODULE_TITLE' => '出勤記録: ホーム', 'LBL_NEW_FORM_TITLE' => '新規出勤記録', 'LBL_SEARCH_FORM_TITLE' => '出勤記録検索', //フィールド名 'LBL_DATE' => '日付', 'LBL_ASSIGNED_USER_NAME' => '従業員名', 'LBL_PUNCH_IN' => '出社時刻', 'LBL_PUNCH_OUT' => '退社時刻', 'LBL_DESCRIPTION' => '備考', //フィールド名(リストのヘッダ表示用) 'LBL_LIST_FORM_TITLE' => '出勤記録一覧', 'LBL_LIST_ASSIGNED_USER_NAME' => '従業員名', 'LBL_LIST_DATE' => '日付', 'LBL_LIST_PUNCH_IN' => '出社時刻', 'LBL_LIST_PUNCH_OUT' => '退社時刻', 'LBL_LIST_DESCRIPTION' => '備考', //メニュー項目 'LNK_CLOCKCARD_LIST' => '出勤記録一覧', 'LNK_NEW_CLOCKCARD' => '出勤記録の作成', );
en_us.lang.php
ここでは、日本語のみで使用するものとして、英語版の言語ファイル作成は省略します。
その他のファイル
コントローラ(controller.php)
この例では不要です。カスタムビューを作成した場合などは必要になります。
メニュー(Menu.php)
global $mod_strings, $app_strings;
if(ACLController::checkAccess('ClockCards', 'view', true))
$module_menu[] = Array(
"index.php?module=ClockCards&action=EditView&return_module=ClockCards&return_action=index",
$mod_strings['LNK_NEW_CLOCKCARD'],
"CreateClockCards"
);
if(ACLController::checkAccess('ClockCards', 'list', true))
$module_menu[] = Array(
"index.php?module=ClockCards&action=index&return_module=ClockCards&return_action=DetailView",
$mod_strings['LNK_CLOCKCARD_LIST'],
"ClockCards"
);
フォーム(Forms.php)
サイドクイック作成を有効にする場合は、下記を記述。無効にする場合は、コメントアウト。
require_once('include/EditView/SideQuickCreate.php');
ビュー(views/)
ファイル名をview.edit.phpとして、下記のように記述。preDisplayメソッドで、GET配列から退勤時刻を取得しています。
require_once('include/MVC/View/views/view.edit.php');
class ClockCardsViewEdit extends ViewEdit {
function ClockCardsViewEdit(){
parent::ViewEdit();
}
function preDisplay() {
if (!empty($_REQUEST['punch_out'])){
list($h, $m) = explode(':', $_REQUEST['punch_out']);
$this->bean->punch_out = sprintf('%02d:%02d', $h, $m);
}
parent::preDisplay();
}
}