FuelPHP 1.5 Logパッケージ修正

FuelPHP Exception thrown without a stack frame in Unknown on line 0 という記事の続きになりますが、FuelPHP 1.5で、cron用のtaskを作成してコマンドラインから実行したところ
FuelPHP Exception thrown without a stack frame in Unknown on line 0
が発生しました。

処理を追ってみたところ、ログを書き込んでいるところで発生しています。ログディレクトリ(APPPATH.’logs/’)を確認したところ、ファイルは存在していて
-rw-r--r-- 1 www-data www-data 607 Feb 4 22:21 fuel/app/logs/2013/02/04.php
となっています。コマンドラインで実行したユーザーがwww-dataではないため、書き込みが出来なかったようです。
1.4までは、’file.chmod.files’の設定に従ってパーミッションが変更されていたので、FuelPHP 1.5 での変更が原因でパーミッションが変更されなくなりました。

Log 機能は、FuelPHP 1.5でパッケージに置き換えられています。
Changelog v1.5

A new Log package has been introduced in preparation for the transition to 2.0, which replaces the Log class. As it is a required by the core, you MUST add the Log class to your always_load manually if you are upgrading from a previous version!

The Log class has been removed and replaced by the log package. If you have extended the Log class in your application, you will have to extend \Log\Log instead, and check the compatibility of your changes. If they are about logging to other locations, you might want to look into the Monolog stream handlers instead.


ソースを確認すると、パーミッション変更のコードがなかったので、Logパッケージのソースを修正します。

		$filename = $filepath.date('d').'.php';

		if ( ! file_exists($filename))
		{
			file_put_contents($filename, "<"."?php defined('COREPATH') or exit('No direct script access allowed'); ?".">".PHP_EOL.PHP_EOL);
			@chmod($filename, \Config::get('file.chmod.files', 0666));
		}
修正したバージョンは正確には、1.5.1 です。


FuelPHP Model::find() の変更

FuelPHPChangelog v1.5 を見ていたところ

Removed code (because it was deprecated in v1.4 or earlier)

ORM Model::find() can no longer be used to construct queries using method chaining. Use Model::query() instead.

という記述を見つけました。

Model::find()は多用していたので、ドキュメントにあった記述かと思い調べたところ、日本語ドキュメントのメソッドチェーンを使用して見つけるに以下のコードがありました。

$query = Model_Article::find()->where('category_id', 1)->order_by('date', 'desc');
最新の英語版の方は
$query = Model_Article::query()->where('category_id', 1)->order_by('date', 'desc');
となっていて、その上に

When you use the find() method without properties, it will be considered to be an error situation. Currently it will return an Orm\Query object which you can use, and possibly reuse to find entries. This behaviour might change in the future, so using this is discouraged, use the query() method instead.

という注意書きがあります。

githubで変更箇所を確認すると、以下が該当するようです。
https://github.com/fuel/orm/commit/bec06a582e332e426487b424dc1d970f3ab46d47
No longer accept Model::find(null) as a valid method call ·  bec06a5 · fuel/orm · GitHub
Model::find()の第1引数がnull(あるいは省略)で、引数が一つのときに例外を投げるように変更されています。
つまり、

$query = Model_Article::find(null)->where('category_id', 1)->order_by('date', 'desc');
は例外が発生して、
$query = Model_Article::find()->where('category_id', 1)->order_by('date', 'desc');
だと、これまで通り機能するようです。実際にコードを書いて確認してみても、この様になります。
Changelogと修正内容があっていないので、修正ミスなのかもしれません。

Model::find() が完全に使えなくなる可能性もあるため、今後作成するコードは Model::query() を使う事にして、パッケージ等の既存コードで Model::find() → Model::query() の修正が大変なものは、以下の様なコードを追加して対応する必要があります。
	public static function find($id = null, array $options = array())
	{
			if (is_null($id))
			{
				return parent::query($options);
			}
			return parent::find($id, $options);
	}


既存PDFをテンプレートにPDFを作成

PDFの埋め込み印刷機能を、既存PDFをテンプレートとして読み込むライブラリ fpdi を使って、tcpdf でPDF出力する事により実現します。

インストール

PHPフレームワークは、FuelPHP を使用、”app/vendor/”にライブラリを入れます。

fpdiのダウンロードページ から、FPDI-x.x.x.zip をダウンロード、展開して、fpdi ディレクトリに入れます。 FPDF_TPL-x.x.x.zipfpdf_tpl.php もダウンロード、展開して、”fpdf_tpl.php”を fpdi ディレクトリに入れます。

tcpdfのダウンロードページ から、tcpdf_x_x_xxx.zip をダウンロード、展開して、tcpdf ディレクトリに入れます。

FuelPHP からの利用

プログラムから利用するには、FPDIオブジェクトを作成、既存PDFを読み込んで、TCPDFのメソッドで入力データを埋め込んでいきます。サンプルプログラムは以下の様になります。
require_once(APPPATH.'vendor/tcpdf/config/lang/jpn.php');
require_once(APPPATH.'vendor/tcpdf/tcpdf.php');
require_once(APPPATH.'vendor/fpdi/fpdi.php');

	public function action_pdfview()
	{
		$objPdf = new FPDI();
		$objPdf->setPrintHeader( false );	// 余計な横線を消す
		$objPdf->setPrintFooter( false );	// 余計な横線を消す
		$objPdf->AddPage();
		$objPdf->setSourceFile(DOCROOT.'sample.pdf');
		$iIndex = $objPdf->importPage(1);
		$objPdf->useTemplate($iIndex);
		
		$objPdf->SetFont(self::$font_name, '', 12);
		$objPdf->Text(42, 28.5, Input::post('ruby'));
		$objPdf->SetFont(self::$font_name, '', 15);
		$objPdf->Text(42, 40, Input::post('name'));

		$margins = $this->getMargins();
		//A4 (210x297 mm ; 8.27x11.69 in)
		$x_max = 210 - $margins['left'] - $margins['right'];
		$y_max = 297 - $margins['top'] - $margins['bottom'];
		$x_step = 5;
		$y_step = 5;
		$style = array('width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => '10,20,5,10', 'phase' => 10, 'color' => array(255, 0, 0));
		
		for ($x = 50; $x < $x_max; $x += $x_step)
		{
			$objPdf->Text($x, 0, $x);
			$objPdf->Line($x, 0, $x, $y_max);
		}
		for ($y = 0; $y < $y_max; $y += $y_step)
		{
			$objPdf->Text(0, $y, $y);
			$objPdf->Line(0, $y, $x_max, $y);
		}
		
		$objPdf->Output('newpdf.pdf', 'I');
	}
15-18行目が、入力された文字を埋め込んでいる処理です。self::$font_name は、tcpdfに標準で入っている「小塚ゴシックPro M」を指定しています。
	public static $font_name = 'kozgopromedium';
20行目からは、埋め込む位置を決める際のガイド用に5mm刻みでグリッドを表示しています。

このアクションを実行すると、以下のエラーが表示されます。
ErrorException [ Runtime Notice ]:
 Declaration of FPDF_TPL::SetFont() should be compatible with
 that of TCPDF::SetFont()
[ Runtime Notice ] なので、E_STRICT を外しておけば問題ありませんが、一応確認しておきます。

fpdiの修正

fpdi(FPDF_TPL) は、TCPDFを継承して実装されています。
class FPDI extends FPDF_TPL {
class FPDF_TPL extends FPDF {
class FPDF extends TCPDF {
fpdiでオーバーライドしているメソッドと、tcpdfのメソッドで、引数が異なっているのが原因で上のエラーが発生しています。
    public function SetFont($family, $style = '', $size = 0, $fontfile='', $subset='default', $out=true) {
	public function SetFont($family, $style='', $size=null, $fontfile='', $subset='default', $out=true) {
tcpdfの方で省略可能な引数が追加されていて、fpdiの方にはありません。fpdiでは当然使用もされていないので、引数だけ追加しておきます。
SetFont()以外にも省略可能な引数が追加されたメソッドがあるので、同じように修正します。
FPDI-1.4.3、FPDF_TPL-1.2.1、tcpdf_5_9_202、の組み合わせで、以下の修正が必要になります。
Index: fpdf_tpl.php
===================================================================
--- fpdf_tpl.php	(org)
+++ fpdf_tpl.php	(new)
@@ -268,7 +268,7 @@
     /**
      * See FPDF/TCPDF-Documentation ;-)
      */
-    public function SetFont($family, $style = '', $size = 0) {
+    public function SetFont($family, $style = '', $size = 0, $fontfile='', $subset='default', $out=true) {
         if (is_subclass_of($this, 'TCPDF')) {
         	$args = func_get_args();
         	return call_user_func_array(array($this, 'TCPDF::SetFont'), $args);
@@ -288,7 +288,7 @@
     /**
      * See FPDF/TCPDF-Documentation ;-)
      */
-    function Image($file, $x = null, $y = null, $w = 0, $h = 0, $type = '', $link = '') {
+    function Image($file, $x = null, $y = null, $w = 0, $h = 0, $type = '', $link = '', $align='', $resize=false, $dpi=300, $palign='', $ismask=false, $imgmask=false, $border=0, $fitbox=false, $hidden=false, $fitonpage=false, $alt=false, $altimgs=array()) {
         if (is_subclass_of($this, 'TCPDF')) {
         	$args = func_get_args();
 			return call_user_func_array(array($this, 'TCPDF::Image'), $args);
@@ -309,7 +309,7 @@
      *
      * AddPage is not available when you're "in" a template.
      */
-    function AddPage($orientation = '', $format = '') {
+    function AddPage($orientation = '', $format = '', $keepmargins=false, $tocpage=false) {
     	if (is_subclass_of($this, 'TCPDF')) {
         	$args = func_get_args();
         	return call_user_func_array(array($this, 'TCPDF::AddPage'), $args);
@@ -324,7 +324,7 @@
     /**
      * Preserve adding Links in Templates ...won't work
      */
-    function Link($x, $y, $w, $h, $link) {
+    function Link($x, $y, $w, $h, $link, $spaces=0) {
         if (is_subclass_of($this, 'TCPDF')) {
         	$args = func_get_args();
 			return call_user_func_array(array($this, 'TCPDF::Link'), $args);
Index: fpdi2tcpdf_bridge.php
===================================================================
--- fpdi2tcpdf_bridge.php	(org)
+++ fpdi2tcpdf_bridge.php	(new)
@@ -28,7 +28,7 @@
  */
 class FPDF extends TCPDF {
     
-	function _putstream($s) {
+	function _putstream($s, $n = 0) {
 		$this->_out($this->_getstream($s));
 	}
 

複数行テキスト

tcpdfで複数行テキストを出力するには、MultiCell を使うと出力領域の幅に合わせて適宜改行されて出力できます。
まっさらな領域に出力する場合は良いですが、既存PDFをテンプレートとする場合、テンプレートとするPDFに罫線が引かれていて、それに合わせて出力する必要が出てきます。
MultiCell には改行幅を指定するパラメータがないので、GetStringWidth で出力幅を調べながら、1行ずつ出力していきます。
FPDIを拡張して、グリッド線を引くメソッドも入れたクラスを作成します。
<?php
require_once(APPPATH.'vendor/tcpdf/config/lang/jpn.php');
require_once(APPPATH.'vendor/tcpdf/tcpdf.php');
require_once(APPPATH.'vendor/fpdi/fpdi.php');

class Model_Mergepdf extends FPDI
{
	public function __construct()
	{
		parent::__construct();
		
		// 余計な横線を消す
		$this->setPrintHeader(false);
		$this->setPrintFooter(false);
	}
	
	/**
	 * A4用紙にグリッド線を描画する
	 */
	public function draw_grid($x_step = 5, $y_step = 5)
	{
		$margins = $this->getMargins();
		//A4 (210x297 mm ; 8.27x11.69 in)
		$x_max = 210 - $margins['left'] - $margins['right'];
		$y_max = 297 - $margins['top'] - $margins['bottom'];
		$this->SetFont("kozgopromedium", "", 6);
		for ($x = 0; $x < $x_max; $x += $x_step)
		{
			$this->Text($x, 0, $x);
			$this->Line($x, 0, $x, $y_max);
		}
		for ($y = 0; $y < $y_max; $y += $y_step)
		{
			$this->Text(0, $y, $y);
			$this->Line(0, $y, $x_max, $y);
		}
	}
	
	/**
	 * 罫線ありのテンプレートに複数行テキストを出力
	 *
	 * @param string $text 出力テキスト
	 * @param int $x X座標
	 * @param int $y Y座標
	 * @param int $w 領域の幅
	 * @param int $h 罫線の高さ
	 * @param int $n 行数
	 * @param string $n フォント名
	 * @param int $n フォントサイズ
	 * @param int $maxchar 1行の最大文字数
	 * @return string 出力されなかったテキスト
	 */
	public function RuledCell($text, $x, $y, $w, $h, $n, $font_name, $fontSize, $maxchar = 100)
	{
		$this->SetFont($font_name, '', $fontSize);
		$line = 0;
		while (mb_strlen($text, 'UTF-8') > 0 && $line < $n)
		{
			$lf_pos = mb_strpos($text, "\n", 0, 'UTF-8');
			$put_length = min(mb_strlen($text, 'UTF-8'), ($lf_pos !== false)?$lf_pos:$maxchar);
			$put_text = mb_substr($text, 0, $put_length, 'UTF-8');
			while ($w < $this->GetStringWidth($put_text, $font_name, '', $fontSize))
			{
				$put_length--;
				$put_text = mb_substr($text, 0, $put_length, 'UTF-8');
			}
			$this->Text($x, $y + $line * $h, $put_text);
			$text = mb_substr($text, $put_length, mb_strlen($text, 'UTF-8') - $put_length, 'UTF-8');
			if (mb_strlen($text, 'UTF-8') > 0 && $text[0] == "\n")
			{
				// 改行で切られた場合は、"\n"を捨てる
				$text = mb_substr($text, 1, mb_strlen($text, 'UTF-8') - 1, 'UTF-8');
			}
			$line++;
		}
		
		return $text;
	}

}
RuledCell()は、1文字ずつ減らしていく処理のため、入力テキストが改行なしで長い場合は、適切な長さが見つかるまで時間がかかってしまいます。そのため、$maxchar を入れて適度な長さから調べ始めるようにしています。
“l”などフォント幅が狭い文字が多数入力される可能性がある場合は、大きめにする必要があります。
印字領域に出力し切れなかったテキストは、そのまま捨てられます。エラーとする場合や残りのテキストを別領域に出力する場合は、return を調べて対処します。

FuelPHPのコントローラからは、以下の様に利用します。
		$objPdf = new Model_Mergepdf();

		// page 1
		$objPdf->AddPage();
		$objPdf->setSourceFile(DOCROOT.'sample.pdf');
		$iIndex = $objPdf->importPage(1);
		$objPdf->useTemplate($iIndex);

		$text = "...";
		$objPdf->MultiCell(115, 0, $text, 0, 'L', 0, 1, 20, 165);
		...

		// page 2
		$objPdf->AddPage();
		$iIndex = $objPdf->importPage(2);
		$objPdf->useTemplate($iIndex);

		$text = "...";
		$x = 20; 
		$y = 120;
		$w = 170;
		$h = 10;
		$n = 10;
		$fontSize = 9;
		$objPdf->RuledCell($text, $x, $y, $w, $h, $n, self::$font_name, $fontSize);
		...

		// 出力
		$objPdf->Output('newpdf.pdf', 'I');


Elgg メールの文字コード

Elgg から送信されるメールには、

  • 自分または友達のコンテンツに更新があったことを知らせる通知メール
  • プラグイン “User Validation by Email” による登録確認メール
  • プラグイン “Invite Friends” による招待メール
があります。この他にも導入しているプラグインによってメールが送信される場合があります。
これらのメールの文字コードはUTF-8となっているので、UTF-8を扱えるメールソフトやWEBMAILであれば日本語でも問題ありません。

ユーザーによっては、UTF-8を扱えない環境であることも考えられるので、日本語メールの一般的な文字コードJISに変換して送信するようにします。

Elgg メール送信処理

伝言板(messageboard)に書き込みがあった場合の通知を例にメール送信処理を見ていきます。
function messageboard_add($poster, $owner, $message, $access_id = ACCESS_PUBLIC) {
	$result = $owner->annotate('messageboard', $message, $access_id, $poster->guid);

	if (!$result) {
		return false;
	}

	add_to_river('river/object/messageboard/create',
				'messageboard',
				$poster->guid,
				$owner->guid,
				$access_id,
				0,
				$result);

	// only send notification if not self
	if ($poster->guid != $owner->guid) {
		$subject = elgg_echo('messageboard:email:subject');
		$body = elgg_echo('messageboard:email:body', array(
						$poster->name,
						$message,
						elgg_get_site_url() . "messageboard/" . $owner->username,
						$poster->name,
						$poster->getURL()
						));

		notify_user($owner->guid, $poster->guid, $subject, $body);
	}

	return $result;
}
129行目でnotify_user() を呼び出して通知処理を行います。
notify_user() は engine/lib/notification.php にあり、ユーザーの通知設定を確認して適切な通知処理を行います。今回はメール通知の設定をしてあるので、email_notify_handler() が呼び出されます。
function email_notify_handler(ElggEntity $from, ElggUser $to, $subject, $message,
array $params = NULL) {

	global $CONFIG;

	if (!$from) {
		$msg = elgg_echo('NotificationException:MissingParameter', array('from'));
		throw new NotificationException($msg);
	}

	if (!$to) {
		$msg = elgg_echo('NotificationException:MissingParameter', array('to'));
		throw new NotificationException($msg);
	}

	if ($to->email == "") {
		$msg = elgg_echo('NotificationException:NoEmailAddress', array($to->guid));
		throw new NotificationException($msg);
	}

	// To
	$to = $to->email;

	// From
	$site = get_entity($CONFIG->site_guid);
	// If there's an email address, use it - but only if its not from a user.
	if (!($from instanceof ElggUser) && $from->email) {
		$from = $from->email;
	} else if ($site && $site->email) {
		// Use email address of current site if we cannot use sender's email
		$from = $site->email;
	} else {
		// If all else fails, use the domain of the site.
		$from = 'noreply@' . get_site_domain($CONFIG->site_guid);
	}

	return elgg_send_email($from, $to, $subject, $message);
}
from、to の確認を行った後、最後に elgg_send_email() を呼び出します。
function elgg_send_email($from, $to, $subject, $body, array $params = NULL) {
	global $CONFIG;

	if (!$from) {
		$msg = elgg_echo('NotificationException:MissingParameter', array('from'));
		throw new NotificationException($msg);
	}

	if (!$to) {
		$msg = elgg_echo('NotificationException:MissingParameter', array('to'));
		throw new NotificationException($msg);
	}

	// return TRUE/FALSE to stop elgg_send_email() from sending
	$mail_params = array(
							'to' => $to,
							'from' => $from,
							'subject' => $subject,
							'body' => $body,
							'params' => $params
					);

	$result = elgg_trigger_plugin_hook('email', 'system', $mail_params, NULL);
	if ($result !== NULL) {
		return $result;
	}

	$header_eol = "\r\n";
	if (isset($CONFIG->broken_mta) && $CONFIG->broken_mta) {
		// Allow non-RFC 2822 mail headers to support some broken MTAs
		$header_eol = "\n";
	}

	// Windows is somewhat broken, so we use just address for to and from
	if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
		// strip name from to and from
		if (strpos($to, '<')) {
			preg_match('/<(.*)>/', $to, $matches);
			$to = $matches[1];
		}
		if (strpos($from, '<')) {
			preg_match('/<(.*)>/', $from, $matches);
			$from = $matches[1];
		}
	}

	$headers = "From: $from{$header_eol}"
		. "Content-Type: text/plain; charset=UTF-8; format=flowed{$header_eol}"
		. "MIME-Version: 1.0{$header_eol}"
		. "Content-Transfer-Encoding: 8bit{$header_eol}";


	// Sanitise subject by stripping line endings
	$subject = preg_replace("/(\r\n|\r|\n)/", " ", $subject);
	if (is_callable('mb_encode_mimeheader')) {
		$subject = mb_encode_mimeheader($subject, "UTF-8", "B");
	}

	// Format message
	$body = html_entity_decode($body, ENT_COMPAT, 'UTF-8'); // Decode any html entities
	$body = elgg_strip_tags($body); // Strip tags from message
	$body = preg_replace("/(\r\n|\r)/", "\n", $body); // Convert to unix line endings in body
	$body = preg_replace("/^From/", ">From", $body); // Change lines starting with From to >From

	return mail($to, $subject, wordwrap($body), $headers);
}
339行目にメールヘッダの設定があり、
text/plain; charset=UTF-8;
Content-Transfer-Encoding: 8bit;
が設定されています。ここをiso-2022-jp/7bitとなるようにします。
348行目のmime変換も”iso-2022-jp”に変更します。
$body を”iso-2022-jp”に変換してから mail() 関数を呼び出せば、”iso-2022-jp”のメールが送信されます。

315行目でプラグインフックを起動しているので、プラグイン側でこれらの修正を行うことにします。

日本語メール送信プラグイン

プラグインをsendmail_jaとして、mod/sendmail_ja/start.php を作成します。
<?php
/*
 *
 * Send Japanese(charset is iso-2022-jp) mail
 *
 * @author oo-foo
 * @copyright Copyright (c) 2013, oo-foo
 *
 */
 
elgg_register_event_handler('init','system','sednmail_ja_init');

function sednmail_ja_init() {

	elgg_register_plugin_hook_handler('email', 'system', 'sendmail_ja_hook');
}

/**
 * Send email hook
 * 
 * @param string $hook
 * @param string $type
 * @param array  $return
 * @param array  $params
 */
function sendmail_ja_hook($hook, $type, $return, $params) {

	global $CONFIG;

	// 言語が日本語('ja')でなければ何もしない
	if (get_current_language() != 'ja') {
		return;
	}

	// mb_languageが使えなければ何もしない
	if (!is_callable('mb_language')) {
		return;
	}
	mb_language('Japanese');
	mb_internal_encoding('utf-8');

	extract($params);

	$header_eol = "\r\n";
	if (isset($CONFIG->broken_mta) && $CONFIG->broken_mta) {
		// Allow non-RFC 2822 mail headers to support some broken MTAs
		$header_eol = "\n";
	}

	// Windows is somewhat broken, so we use just address for to and from
	if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
		// strip name from to and from
		if (strpos($to, '<')) {
			preg_match('/<(.*)>/', $to, $matches);
			$to = $matches[1];
		}
		if (strpos($from, '<')) {
			preg_match('/<(.*)>/', $from, $matches);
			$from = $matches[1];
		}
	}

	$headers = "From: $from{$header_eol}"
		. "Content-Type: text/plain; charset=iso-2022-jp; format=flowed{$header_eol}"
		. "MIME-Version: 1.0{$header_eol}"
		. "Content-Transfer-Encoding: 7bit{$header_eol}";


	// Sanitise subject by stripping line endings
	$subject = preg_replace("/(\r\n|\r|\n)/", " ", $subject);
	if (is_callable('mb_encode_mimeheader')) {
		$subject = mb_encode_mimeheader($subject, "iso-2022-jp", "B");
	}

	// Format message
	$body = html_entity_decode($body, ENT_COMPAT, 'UTF-8'); // Decode any html entities
	$body = elgg_strip_tags($body); // Strip tags from message
	$body = preg_replace("/(\r\n|\r)/", "\n", $body); // Convert to unix line endings in body
	$body = preg_replace("/^From/", ">From", $body); // Change lines starting with From to >From

	// convert to iso-2022-jp
	$body = mb_convert_encoding($body, 'iso-2022-jp');
	
	return mail($to, $subject, wordwrap($body), $headers);
}
31行目で言語設定が日本語になっているか確認しています。このプラグインを有効にしている場合は、常に”iso-2022-jp”で送信するという事にしてチェックを行わなくても良いかもしれません。
36行目では mb_language() が呼べるかどうかをチェックして、日本語環境があるかを確認しています。
後は、
  • メールヘッダを”iso-2022-jp”メールに設定
  • “iso-2022-jp”でMIMEエンコード
  • $bodyを”iso-2022-jp”に変換
という処理になっています。

今回のプラグインファイルは、このstart.phpとmanifest.xmlのみです。


Elgg cron設定

Elgg のcron については、http://docs.elgg.org/wiki/Cron(日本語) に日本語の説明があります。

Elgg cronハンドラーは特定のページを読み込むと実行されます。例えば、http://example.com/pg/cron/hourly/ がWebブラウザ等で読み込まれると1時間単位のフックが実行されます。この一連の流れを自動化するには、cronジョブを設定して適当なタイミングでこのページを読み込むようにします。このcronジョブはcrontabにて設定します。

CLIからの起動ではなく、HTTPアクセスによりcron処理が起動されます。

標準で入っているプラグイン、”Garbage Collector”や”Log Rotate”、はプラグインの設定を行っても、ブラウザからアクセスすることにより実行はされますが、cronの設定を行わないと定期的には実行されません。

cronの設定方法は上記ページに例があります。

 # GETコマンドのパス
 GET='/usr/bin/GET'
 
 # サイトのURL(最後のスラッシュをお忘れなく!)
 ELGG='http://www.example.com/'
 
 # The crontab
 @reboot $GET ${ELGG}pg/cron/reboot/
 @hourly $GET ${ELGG}pg/cron/hourly/
 @daily $GET ${ELGG}pg/cron/daily/
 @weekly $GET ${ELGG}pg/cron/weekly/
 @monthly $GET ${ELGG}pg/cron/monthly/
 @yearly $GET ${ELGG}pg/cron/yearly/

ソースを見るとこれ以外に、’minute’(1分毎), ‘fiveminute’(5分毎), ‘fifteenmin’(15分毎), ‘halfhour’(30分毎)、が定義されています。
この設定については、https://github.com/melvincarvalho/elgg/blob/master/crontab.example に例が載っています。これを基にcurlを使って設定した例が以下になります。
# Elgg cron
GET='/usr/bin/curl -s -S'

ELGG='http://elgg.oo-foo.com/'

@reboot $GET ${ELGG}cron/reboot/
* * * * * $GET ${ELGG}cron/minute/
*/5 * * * * $GET ${ELGG}cron/fiveminute/
15,30,45,59 * * * * $GET ${ELGG}cron/fifteenmin/
30,59 * * * * $GET ${ELGG}cron/halfhour/
@hourly $GET ${ELGG}cron/hourly/
@daily $GET ${ELGG}cron/daily/
@weekly $GET ${ELGG}cron/weekly/
@monthly $GET ${ELGG}cron/monthly/
@yearly $GET ${ELGG}cron/yearly/
ドキュメントでは、${ELGG}pg/cron/reboot/、の様になっていますが、”pg”は後方互換の記述の様なので必要ありません。

HTTPアクセスによる起動なので、Elggをインストールしたサーバーとは異なるサーバー上のcronから呼び出すことも可能です。

cronのアクセス制限

ElggのcronはHTTPアクセスにより起動するため、”/cron/hourly/”にアクセスされると誰にでもhourlyに設定された処理が実行出来てしまいます。誰にでもcron処理が実行可能なのは問題がありそうなので、これを制限する方法を考えます。

“/cron”へのアクセスは”engine/lib/cron.php”のcron_page_handler()で処理されます。
function cron_page_handler($page) {
	global $CONFIG;

	if (!isset($page[0])) {
		forward();
	}

	$period = strtolower($page[0]);

	$allowed_periods = array(
		'minute', 'fiveminute', 'fifteenmin', 'halfhour', 'hourly',
		'daily', 'weekly', 'monthly', 'yearly', 'reboot'
	);

	if (!in_array($period, $allowed_periods)) {
		throw new CronException(elgg_echo('CronException:unknownperiod', array($period)));
	}

	// Get a list of parameters
	$params = array();
	$params['time'] = time();

	// Data to return to
	$std_out = "";
	$old_stdout = "";
	ob_start();

	$old_stdout = elgg_trigger_plugin_hook('cron', $period, $params, $old_stdout);
	$std_out = ob_get_clean();

	echo $std_out . $old_stdout;
	return true;
}
$pageに”/cron/”以下のURLが配列形式で入り、$page[0]に’hourly’や’daily’などのperiodが入ってきます。これをパラメータとして58行目で’cron’をフックしているプラグイン関数を呼び出しています。
34行目でperiod指定がない場合、cron処理を行わないで”/”にリダイレクトするようになっているので、不正なアクセスの場合もこれと同じようにします。

アクセスを制限する方法は、”/cron/hourly/{secretkey}”の様にURLに特定のパラメータがある時のみ、cron処理を実行するようにします。

$pageにURLが配列で入ってくるので、{secretkey}は$page[1]に入ってきます。これが指定した値と一致しないときは、forward()してcron処理を行わないようにします。
	if (!isset($page[0]) || !isset($page[1]) || $page[1] != '{secretkey}') {
		forward();
	}
独自のサイト設定を行っている箇所が見当たらなかったので、ソースに直接’{secretkey}’を書き込んでいます。

cron 設定もこれに合わせて書き換えます。
# Elgg cron
GET='/usr/bin/curl -s -S'

ELGG='http://elgg.oo-foo.com/'
SECRETKEY='{secretkey}'

@reboot $GET ${ELGG}cron/reboot/${SECRETKEY}
* * * * * $GET ${ELGG}cron/minute/${SECRETKEY}
*/5 * * * * $GET ${ELGG}cron/fiveminute/${SECRETKEY}
15,30,45,59 * * * * $GET ${ELGG}cron/fifteenmin/${SECRETKEY}
30,59 * * * * $GET ${ELGG}cron/halfhour/${SECRETKEY}
@hourly $GET ${ELGG}cron/hourly/${SECRETKEY}
@daily $GET ${ELGG}cron/daily/${SECRETKEY}
@weekly $GET ${ELGG}cron/weekly/${SECRETKEY}
@monthly $GET ${ELGG}cron/monthly/${SECRETKEY}
@yearly $GET ${ELGG}cron/yearly/${SECRETKEY}
これで{SECRETKEY}を知らない第三者からcron処理を実行される事はなくなります。

プラグインでの制限

engine/lib/cron.php を直接書き換えるのではなく、プラグインで上の制限を行うようにします。

プラグインをsafe_cronとして、mod/safe_cron/start.php を作成します。
elgg_register_event_handler('init','system','safecron_init');

function safecron_init() {

	elgg_unregister_page_handler('cron');
	elgg_register_page_handler('cron', 'safecron_page_handler');
}

function safecron_page_handler($page) {
	global $CONFIG;

	if (!isset($page[0]) || !isset($page[1]) || $page[1] != '{secret}') {
		forward();
	}

	$period = strtolower($page[0]);

	$allowed_periods = array(
		'minute', 'fiveminute', 'fifteenmin', 'halfhour', 'hourly',
		'daily', 'weekly', 'monthly', 'yearly', 'reboot'
	);

	if (!in_array($period, $allowed_periods)) {
		throw new CronException(elgg_echo('CronException:unknownperiod', array($period)));
	}

	// Get a list of parameters
	$params = array();
	$params['time'] = time();

	// Data to return to
	$std_out = "";
	$old_stdout = "";
	ob_start();

	$old_stdout = elgg_trigger_plugin_hook('cron', $period, $params, $old_stdout);
	$std_out = ob_get_clean();

	echo $std_out . $old_stdout;
	return true;
}
5行目でcoreのcronページハンドラを削除して、6行目で新たなcronページハンドラ”safecron_page_handler”を設定します。
12行目で{secret}のチェックを行っている以外は、engine/lib/cron.php の cron_page_handler() と同じです。

今回のプラグインファイルは、このstart.phpとmanifest.xmlのみです。


Elgg プロフィール項目の変更

Elgg のユーザープロフィール項目は、デフォルトで以下の項目になっています。

  • 氏名
  • 自己紹介
  • ちょっと一言
  • 住所・地域
  • 趣味
  • 特技
  • 電子メール
  • 電話番号
  • 携帯番号
  • Website
  • Twitterユーザ名
プロフィール項目は、管理ページの「見た目 > プロフィール項目を編集」で変更する事が出来、プロフィール項目を編集した場合は、氏名以外のデフォルトのプロフィール項目はすべて削除されて編集で追加した項目のみになります。

プロフィール項目の編集は、追加したいプロフィール項目のラベルと項目のタイプを選択して追加します。
Elgg:プロフィール項目編集

Elgg:プロフィール項目編集



複数URL指定

プロフィールに複数のURLを登録したいとき、最大3つまで、の様に数が決まっている場合はラベルを”URL 1″,”URL 2″,”URL 3″の様にしてタイプをすべてWebアドレスとすれば、最大3つまでのURLの登録が可能です。
が、固定数ではなく登録数を可変にできる様にします。

プロフィール保存処理

プロフィール編集ページのform actionは、/action/profile/edit となっているので、mod/profile/start.php を確認します。start.php には action ハンドラの設定がないので、coreの actions/profile/edit.php でプロフィールの保存処理が行われます。
入力値のチェック等の後、データを保存しているのは以下の部分です。
// go through custom fields
if (sizeof($input) > 0) {
	foreach ($input as $shortname => $value) {
		$options = array(
			'guid' => $owner->guid,
			'metadata_name' => $shortname
		);
		elgg_delete_metadata($options);
		if (isset($accesslevel[$shortname])) {
			$access_id = (int) $accesslevel[$shortname];
		} else {
			// this should never be executed since the access level should always be set
			$access_id = ACCESS_DEFAULT;
		}
		if (is_array($value)) {
			$i = 0;
			foreach ($value as $interval) {
				$i++;
				$multiple = ($i > 1) ? TRUE : FALSE;
				create_metadata($owner->guid, $shortname, $interval, 'text', $owner->guid, $access_id, $multiple);
			}
		} else {
			create_metadata($owner->getGUID(), $shortname, $value, 'text', $owner->getGUID(), $access_id);
		}
	}

	$owner->save();

	// Notify of profile update
	elgg_trigger_event('profileupdate', $owner->type, $owner);

	system_message(elgg_echo("profile:saved"));
}
metadataとして登録されるので、76行目で既存データを削除、$value(入力値)が配列であるか否かを判定、88,91行目で入力された値を保存しています。
$valueが配列であるか判定して保存処理を行っているので、可変数の項目を保存するための処理は入っていることになります。

つまり、プロフィールの入力フォームで「name=”website”」となっているところを「name=”website[]“」とすれば複数URLの登録が可能になります。

プロフィール登録フォーム

ユーザーのプロフィール編集ページは、/profile/{username}/edit となっていて、mod/profile/start.php では以下のようになっています。
function profile_page_handler($page) {

	if (isset($page[0])) {
		$username = $page[0];
		$user = get_user_by_username($username);
		elgg_set_page_owner_guid($user->guid);
	}

	// short circuit if invalid or banned username
	if (!$user || ($user->isBanned() && !elgg_is_admin_logged_in())) {
		register_error(elgg_echo('profile:notfound'));
		forward();
	}

	$action = NULL;
	if (isset($page[1])) {
		$action = $page[1];
	}

	if ($action == 'edit') {
		// use the core profile edit page
		$base_dir = elgg_get_root_path();
		require "{$base_dir}pages/profile/edit.php";
		return true;
	}
こちらもcoreのページ、pages/profile/edit.php から views/default/forms/profile/edit.php が呼び出されてフォーム表示されます。
<?php

$profile_fields = elgg_get_config('profile_fields');
if (is_array($profile_fields) && count($profile_fields) > 0) {
	foreach ($profile_fields as $shortname => $valtype) {
		$metadata = elgg_get_metadata(array(
			'guid' => $vars['entity']->guid,
			'metadata_name' => $shortname
		));
		if ($metadata) {
			if (is_array($metadata)) {
				$value = '';
				foreach ($metadata as $md) {
					if (!empty($value)) {
						$value .= ', ';
					}
					$value .= $md->value;
					$access_id = $md->access_id;
				}
			} else {
				$value = $metadata->value;
				$access_id = $metadata->access_id;
			}
		} else {
			$value = '';
			$access_id = ACCESS_DEFAULT;
		}

?>
<div>
	<label><?php echo elgg_echo("profile:{$shortname}") ?></label>
	<?php
		$params = array(
			'name' => $shortname,
			'value' => $value,
		);
		echo elgg_view("input/{$valtype}", $params);
		$params = array(
			'name' => "accesslevel[$shortname]",
			'value' => $access_id,
		);
		echo elgg_view('input/access', $params);
	?>
</div>
<?php
	}
}
?>
実際のフォーム表示は43行目からで、その前にvalueとアクセスレベルの処理を行っています。
ここでも24行目で配列確認の処理が入っています。配列の場合は”,”区切りに変換して、ひとつのテキストボックスとして表示しています。

“,”区切りは複数データとして扱われている様なので、再度保存処理を確認してみます。
	if ($valuetype == 'tags') {
		$value = string_to_tag_array($value);
	}
プロフィール項目のタイプが「タグ」であれば、”,”区切りを配列にする処理が入っています。

ということで、複数URLを登録するには、
・プロフィール項目のタイプを「タグ」として
・複数ある場合は、”,”区切りで入力
とすれば、一つの「Website」という項目に対して複数のURLが登録可能となります。

一つのテキストボックスに”,”区切りで入力する、という仕様にするのであれば、これで終わりです。
複数のテキストボックスを表示して入力するには、入力フォームを変更する必要があります。

プロフィール項目のタイプ追加

actions/profile/edit.php 19行目の elgg_get_metadata() で配列(複数データ)が返ってきた場合、25行目からの処理で無条件に”, “区切りの一つの文字列とされてしまいます。
複数のテキストボックスを表示する場合は、一つの結合された文字列ではなく配列データとして分割されている必要があるので、「タグ」と見分けが付くようにプロフィール項目のタイプを追加します。

プロフィール項目のタイプは、views/default/forms/profile/fields/add.php で定義されています。
$type_control = elgg_view('input/dropdown', array('name' => 'type', 'options_values' => array(
	'text' => elgg_echo('profile:field:text'),
	'longtext' => elgg_echo('profile:field:longtext'),
	'tags' => elgg_echo('profile:field:tags'),
	'url' => elgg_echo('profile:field:url'),
	'email' => elgg_echo('profile:field:email'),
	'location' => elgg_echo('profile:field:location'),
	'date' => elgg_echo('profile:field:date'),
)));
ここを書き換えれば良いのですが、このファイルを書き換えるのではなくプラグインとして処理する様にします。

プラグインをcustom_profileとして、mod/custom_profile/views/default/forms/profile/fields/add.php を作成します。
<?php
/**
 * Add a new field to the set of custom profile fields
 */

$label_text = elgg_echo('profile:label');
$type_text = elgg_echo('profile:type');

$label_control = elgg_view('input/text', array('name' => 'label'));
$type_control = elgg_view('input/dropdown', array('name' => 'type', 'options_values' => array(
	'text' => elgg_echo('profile:field:text'),
	'longtext' => elgg_echo('profile:field:longtext'),
	'tags' => elgg_echo('profile:field:tags'),
	'url' => elgg_echo('profile:field:url'),
	'email' => elgg_echo('profile:field:email'),
	'location' => elgg_echo('profile:field:location'),
	'date' => elgg_echo('profile:field:date'),
	'multiple' => '複数項目',
)));

$submit_control = elgg_view('input/submit', array('name' => elgg_echo('add'), 'value' => elgg_echo('add')));

$formbody = <<< END
		<div>$label_text: $label_control</div>
		<div class="elgg-foot">$type_text: $type_control
		$submit_control</div>
END;

echo autop(elgg_echo('profile:explainchangefields'));
echo $formbody;
18行目で ‘multiple’ => ‘複数項目’ として、プロフィール項目に「複数項目」を追加しています。それ以外は、views/default/forms/profile/fields/add.php と同じです。
‘複数項目’は本来であれば言語ファイルに記述して、elgg_echo(‘profile:field:multiple’)、とすべきですが、とりあえずここではそのまま’複数項目’ としています。

これで、views/default/forms/profile/edit.php のelgg_get_metadata()で複数データが帰ってきたときに「タグ」として”,”区切りとするか、「複数項目」としてそのまま配列データとするかの判断が可能になりました。

プロフィール編集フォームの変更

フォーム表示もプラグインにして、views/default/forms/profile/edit.php を置き換えます。
<?php

$profile_fields = elgg_get_config('profile_fields');
if (is_array($profile_fields) && count($profile_fields) > 0) {
	foreach ($profile_fields as $shortname => $valtype) {
		$metadata = elgg_get_metadata(array(
			'guid' => $vars['entity']->guid,
			'metadata_name' => $shortname
		));
		if ($metadata) {
			if (is_array($metadata)) {
				if ($valtype == 'multi') {
					$value = array();
					foreach ($metadata as $md) {
						$value[] = $md->value;
						$access_id = $md->access_id;
					}
				} else {
					$value = '';
					foreach ($metadata as $md) {
						if (!empty($value)) {
							$value .= ', ';
						}
						$value .= $md->value;
						$access_id = $md->access_id;
					}
				}
			} else {
				$value = $metadata->value;
				$access_id = $metadata->access_id;
			}
		} else {
			$value = '';
			$access_id = ACCESS_DEFAULT;
		}

?>
<div>
	<label><?php echo elgg_echo("profile:{$shortname}") ?></label>
	<?php
		$params = array(
			'name' => $shortname,
			'value' => $value,
		);
		echo elgg_view("input/{$valtype}", $params);
		$params = array(
			'name' => "accesslevel[$shortname]",
			'value' => $access_id,
		);
		echo elgg_view('input/access', $params);
	?>
</div>
<?php
	}
}
?>
25行目から30行目で、プロフィール項目のタイプが「複数項目(multi)」であれば、配列形式で値を渡すようにしています。アクセスレベルはすべて同じなので最後の値が設定されます。

<input>等の実際のHTMLタグは58行目の”input/{$valtype}”、複数項目の場合は”input/multi”、なので views/default/input/multi.php が呼び出されて出力されますが、multiは今回追加したものなのでこのファイルは存在しません。プラグインで mod/custom_profile/views/default/input/multi.php を作成します。
<?php

if (isset($vars['class'])) {
	$vars['class'] = "elgg-input-url {$vars['class']}";
} else {
	$vars['class'] = "elgg-input-url";
}

$defaults = array(
	'value' => '',
	'disabled' => false,
);

$vars = array_merge($defaults, $vars);

$shortname = $vars['name'];
$multivalue = $vars['value'];
?>

<?php if (is_array($multivalue)): ?>
<?php foreach ($multivalue as $i => $value):
$vars['internalname'] = $shortname.'['.$i.']';
$vars['value'] = $value;
?>
<input type="text" <?php echo elgg_format_attributes($vars); ?> />
<?php endforeach; ?>
<?php if (reset($multivalue)):
$vars['internalname'] = $shortname.'[new]';
$vars['value'] = '';
?>
<input type="text" <?php echo elgg_format_attributes($vars); ?> />
<?php endif; ?>

<?php else:
$vars['internalname'] = $shortname.'[0]';
$vars['value'] = '';
?>
<input type="text" <?php echo elgg_format_attributes($vars); ?> />
<?php endif; ?>
今回は複数URLの登録という事なので、views/default/input/url.php をコピーして書き換えています。
20行目の配列判定は、プロフィールを一度も更新していない場合の対応です。
21行目から25行目で登録済みのURL分のテキストボックスを出力、28行目から31行目で空以外のURLが登録されていれば追加用のテキストボックスを一つ出力しています。
「追加」ボタンを作成してJavascriptでテキストボックスを追加した方が良さそうですが、複数URLの登録が主眼なので端折ります。
35行目から38行目でデータが登録されていない場合に、テキストボックスを一つ出力しています。
プロフィール 複数URL

空白入力の除去

追加用の空白テキストボックスを設置したため、このまま保存すると空白のURLが登録されて、プロフィールの編集のたびにテキストボックスが増えていってしまいます。また、登録済みのurlを削除するにはurl欄を空白にする、という仕様にするため、空白の場合は登録されないようにする必要があります。

プロフィールの保存前に入力値から空白の項目を除去するために、プラグインフックを使用します。
elgg_register_event_handler('init','system','custom_profile_init');
 
function custom_profile_init() {

	$plugin = elgg_get_plugin_from_id('custom_profile');
	
	elgg_register_plugin_hook_handler('action', 'profile/edit', 'profile_edit_action_hook');
}

function profile_edit_action_hook() {

	$profile_fields = elgg_get_config('profile_fields');
	if (is_array($profile_fields) && count($profile_fields) > 0) {
		foreach ($profile_fields as $shortname => $valtype) {
			if ($valtype == 'multi') {
				$multival = $_REQUEST[$shortname];
				foreach (array_keys($multival) as $i) {
					if ($multival[$i] == '')
						unset($multival[$i]);
				}
				if (!$multival)
					$_REQUEST[$shortname] = array('');
				else
					$_REQUEST[$shortname] = $multival;
			}
		}
	}
	return true;
}
7行目のelgg_register_plugin_hook_handler()でプロフィール編集のプラグインフックを登録して、profile_edit_action_hook()で処理します。

profile_edit_action_hook()の処理内容は、
  • 12行目でプロフィール項目を取得
  • 15行目でプロフィール項目のタイプが複数項目であるか判定
  • 空であればunset
  • 空でない入力が一つもなければ一つの空文字列の配列とする
  • trueをreturn
となっています。

プラグインファイルの構成

今回作成したcustom_profile プラグインは、以下の様になります。
mod/custom_profile
│  manifest.xml
│  start.php
│
└─views
    └─default
        ├─forms
        │  └─profile
        │      │  edit.php
        │      │
        │      └─fields
        │              add.php
        │
        └─input
               multi.php


せん茶請求書 領収書(控)を印字しない

せん茶請求書 で、領収書を発行すると1枚の用紙に「領収書」と「領収書(控)」が印字されたPDFが作成されます。
印刷して渡す場合はこれで良いのですが、メール添付等で領収書を発行する場合は控えが一緒に印字されているのはうまくないので、「領収書(控)」を印字しないようにします。

領収書印刷、画面上では「保存する」ボタン、に相当するアクションは、
formのactionが”/bills/receipt/{bill_no}” なので、
app/controllers/bills_controller.php のreceipt()関数になります。
ここを見ていくと以下の記述があります。

				App::import('Vendor','pdf/receiptpdf');

				//インスタンス化
				$pdf = new RECEIPTPDF();

				$pdf->AddMBFont(MINCHO ,'SJIS');

				//ページの作成
				$pdf->AddPage();
804行目の’pdf/receiptpdf’がPDFの出力処理の様なので、このファイルを確認。
		//大枠(控)
		$this->SetXY(21,110);
		$this->SetFont(MINCHO,'',16);
		$this->Cell( 168, 80, NULL, 1, 'L');

		$this->SetFont(MINCHO,'B',14);
		$this->SetXY(31, 115);
		$str = "領 収 書 (控)";
		$str = $this->conv($str);
		$this->Write(5,$str);
		$this->SetLineWidth(0.2);
		//控え

		//下マージンの設定
		$this->SetAutoPageBreak(true, 5);



		//枠及びラインの色の設定 ※ここをいじれば以下すべての枠の色が変わります。


		//顧客名枠
		$font_size = $this->customer_font(mb_strlen($_param['Bill']['CST_ID']));

		$this->SetXY(31, 125);
		$this->SetFont(MINCHO,'', $font_size);
		$str = "様";
		$str = $this->conv($str);
		$this->Cell( 85, 4, $str, 'B', 1, 'R');

		//顧客名
		$this->SetXY(31, 125);
		$this->SetFont(MINCHO,'', $font_size);
		$str = $_param['Bill']['CST_ID'];
		$str = $this->conv($str);
		$this->Cell( 85, 4, $str,'', 0, 'L');

		//請求金額枠
		$this->SetXY(31,138);
		$this->SetFont(MINCHO,'B',16);
		$str =  $_param['Bill']['TOTAL'] ? '\\'.number_format($_param['Bill']['TOTAL']).'-' : '\\0-';
		$str = $this->conv($str);
		$this->Cell( 150, 8, $str, 0, 1, 'C', 1);

		//但書き枠
		$this->SetXY(71, 150);
		$this->SetFont(MINCHO,'',11);
		$str = "但 ".$_param['Bill']['PROVISO'];
		$str = $this->conv($str);
		$this->Cell( 75, 4, $str, 'B', 1, 'L');

		//発行日
		$this->SetXY(71, 155);
		$this->SetFont(MINCHO,'',11);
		$str = "発行日 ".substr($_param['Bill']['DATE'],0,4)."年".substr($_param['Bill']['DATE'],5,2)."月".substr($_param['Bill']['DATE'],8,2)."日\n";
		$str = $this->conv($str);
		$this->Write(5,$str);

		//自社項目
		$this->SetLeftMargin(130);
		$this->SetY(160);
		$this->SetFont(MINCHO,'B',8);
		$str = $_param['Company']['NAME']."\n";
		$str = $this->conv($str);
		$this->Write(5,$str);

		$this->SetLeftMargin(130);
		$this->SetY(165);
		$this->SetFont(MINCHO,'',8);
			$str  = "〒";
		if(empty($_param['Company']['POSTCODE1']) && empty($_param['Company']['POSTCODE2']) && empty($_param['Company']['POSTCODE3'])) {

		}else {
			$str .= $_param['Company']['POSTCODE1']."-".$_param['Company']['POSTCODE2']."\n";
		}

		if($_param['Company']['CNT_ID']) {
			$str .= $_county[$_param['Company']['CNT_ID']];
		}else {

		}
		$str .= $_param['Company']['ADDRESS']."\n";
		$str .= $_param['Company']['BUILDING']."\n";


		$str .= "TEL ";

		if(empty($_param['Company']['PHONE_NO1']) && empty($_param['Company']['PHONE_NO2']) && empty($_param['Company']['PHONE_NO3'])) {

		}else {
			$str .= $_param['Company']['PHONE_NO1']."-".$_param['Company']['PHONE_NO2']."-".$_param['Company']['PHONE_NO3']."\n";
		}		$str = $this->conv($str);
		$this->Write(4,$str);

		//印鑑枠
		$this->SetXY(31,75);
		$this->SetFont(MINCHO,'',16);
		$this->Cell( 22, 22, NULL, 1, 'L');

		//領収書番号枠
		$this->SetLeftMargin(152);
		$this->SetY(116);
		$this->SetFont(MINCHO,'',9);
		$str = "No.".$_param['Bill']['RECEIPT_NUMBER'];
		$str = $this->conv($str);
		$this->Cell( 35, 4, $str, '', 1, 'L');
この2箇所をコメントアウトすれば、領収書(控)が印字されなくなります。


Elgg デザイン変更

Elggのデザインを変更してみます。

http://community.elgg.org/plugins/category/themes にテーマファイルがアップロードされています。この中からFacebook Theme for Elgg 1.8 を試してみます。

ダウンロードしてzipを展開すると、以下のようなディレクトリ構造になっています。

facebook_theme
│  CONTRIBUTORS.md
│  manifest.xml
│  README.md
│  start.php
│
├─languages
│      en.php
│      fr.php
│
├─pages
│  │  dashboard.php
│  │
│  ├─groups
│  │      info.php
│  │      wall.php
│  │
│  └─profile
│          info.php
│          wall.php
│
├─screenshots
│      dashboard.png
│      group-wall.png
│      index.png
│      user-wall.png
│
├─tests
│      FacebookThemeTest.php
│
└─views
    └─default
        ├─annotation
        │      generic_comment.php
        │
        ├─blog
        │      composer.php
        ... 以下省略
これらのファイルをmod ディレクトリに配置、管理ページのプラグイン管理で「Facebook Theme 1.4」を起動するとデザインが変更されます。

start.php でviewを切り替えるための処理をいくつか行っていますが、基本的には”mod/facebook_theme/views”以下のファイルで、デフォルトのビューであるElggルートの”views”以下のファイルを置き換えることにより、表示の切り替えを行っています。

実際に使用する場合は、languages/en.php を翻訳してlanguages/ja.php を用意する必要があります。

メニューの変更

メニュー項目は、管理ページの「見た目 > メニュー項目」で変更する事が出来ます。
見た目 : メニュー項目

「使用しない項目は、メニューリストの最後の”More”以下に追加されます。」とありますが、使用しない項目は”More”以下にも表示したくないので、プラグインを作成して表示しないようにしてみます。
Moreありメニュー

Moreを表示しないプラグイン

メニューの表示は、views/default/navigation/menu/site.php で行われているので、このファイルをプラグインで置き換えて”More”を表示しないようにします。

views/default/navigation/menu/site.php の確認

<?php
/**
 * Site navigation menu
 *
 * @uses $vars['menu']['default']
 * @uses $vars['menu']['more']
 */

$default_items = elgg_extract('default', $vars['menu'], array());
$more_items = elgg_extract('more', $vars['menu'], array());

echo '<ul class="elgg-menu elgg-menu-site elgg-menu-site-default clearfix">';
foreach ($default_items as $menu_item) {
	echo elgg_view('navigation/menu/elements/item', array('item' => $menu_item));
}

if ($more_items) {
	echo '<li class="elgg-more">';

	$more = elgg_echo('more');
	echo "<a href=\"#\">$more</a>";
	
	echo elgg_view('navigation/menu/elements/section', array(
		'class' => 'elgg-menu elgg-menu-site elgg-menu-site-more', 
		'items' => $more_items,
	));
	
	echo '</li>';
}
echo '</ul>';
10行目でmore項目を取得して、17-29行目でその表示を行っています。
この処理を削除したものをプラグインとして作成します。

プラグインのファイル構成

作成するプラグインのファイル構成は以下の様になります。
mod/mycustom
│  manifest.xml
│  start.php
│
└─views
    └─default
        └─navigation
            └─menu
                    site.php

start.php

このプラグインでは、start.php で何もしません。プラグインの管理でstart.phpがないとエラーになるため、中身が空(0バイト)のファイルを設置しておきます。

site.php

site.phpは、上で書いたように”More”に関する処理を削除しただけのものです。
<?php
/**
 * Site navigation menu
 *
 * @uses $vars['menu']['default']
 * @uses $vars['menu']['more']
 */

$default_items = elgg_extract('default', $vars['menu'], array());

echo '<ul class="elgg-menu elgg-menu-site elgg-menu-site-default clearfix">';
foreach ($default_items as $menu_item) {
	echo elgg_view('navigation/menu/elements/item', array('item' => $menu_item));
}
echo '</ul>';

これでメニューから”More”がなくなります。
Moreなしメニュー


FuelPHPでmroongaを使う

MySQLで高速に全文検索するためのオープンソースのストレージエンジン「mroonga」をFuelPHPで使ってみました。

mroongaのインストール自体は、ドキュメントに詳しく載っていますので、特に問題ないかと思います。MySQLがインストール済みの場合は、削除してからmroongaのパッケージを利用した方が良さそうです。

クエリ構築

全文検索の構文自体は、MyISAMのものと同じです。
SELECT * FROM table_name WHERE MATCH(column_name) AGAINST("word")
が、FuelPHPのORMでは、MyISAMの全文検索構文にも対応していないので、DB::expr()を使ってクエリを構築する必要があります。(http://fuelphp.com/forums/discussion/11587)
$query = Model_Sample::find()
	->where(DB::expr('MATCH(column_name)'),
		 '', DB::expr('AGAINST('.DB::quote($word).')'));
where()の2番目のパラメータは演算子(‘=’や’>'等)が入るところですが、ここを空にして
MATCH(column_name) AGAINST("word")
となるようにしています。

その他は、ストレージエンジンがmroongaだからと言って、特別な処理は必要ありません。


FuelPHP 特定IPからのみプロファイルを有効にする

Fuelphp 付属のPHP Quick Profiler をテスト用のマシンから接続した時のみ有効にする。

        'profiling' => isset($_SERVER['REMOTE_ADDR']) && ($_SERVER['REMOTE_ADDR'] == '192.168.1.101'),
Queryを確認したい場合は、fuel/app/config/db.phpの’profiling’も同様に設定。

さらにブラウザを特定する場合(Firefox 16.0のときのみ有効にする)
        'profiling' => isset($_SERVER['REMOTE_ADDR']) && ($_SERVER['REMOTE_ADDR'] == '192.168.1.101') && isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], 'Firefox/16.0') !== false),