2010年5月26日水曜日

PHP Excelで自動改行&行高さ自動調整

PHP Excelで文字の自動改行をしたい。

ここに記載されているプロパティの設定でOK。
http://phpexcel.codeplex.com/Thread/View.aspx?ThreadId=18434

$objPHPExcel->getActiveSheet()->getStyle('A1')->getAlignment()->setWrapText(true);

ただ、PHPExcelをnewするときは問題ないけれど、テンプレートファイルをロードして書き込むとテンプレートファイルの行高さがずっと保持されたままの状態になるらしい。

自動改行設定した後に行高さを調整しないとキレイに出力されない。

// テンプレートファイルの読み込み
$reader = PHPExcel_IOFactory::createReader('Excel5');
$excel = $reader->load(TEMPLATE_PATH);

// シートの設定
$excel->setActiveSheetIndex(0);
$sheet = $excel->getActiveSheet();
$sheet->getDefaultStyle()->getFont()->setName('MS Pゴシック');
$sheet->getDefaultStyle()->getFont()->setSize(9);
$sheet->setTitle('sheet name');

// 書き込み
$sheet->setCellValue('A1', 'あいうえおかきくけこさしすせそ');

// 自動改行
$sheet->getStyle('A1')->getAlignment()->setWrapText(true);

// 行高さ設定(setRowHeightにパラメタ指定しないことで自動調整)
$sheet->getRowDimension(1)->setRowHeight();

PHPでExcel出力

WEBからExcel出力させたい!
既に色々なライブラリや参考WEBサイトがありますが、備忘録として。。

※参考
http://d.hatena.ne.jp/saicologic/20080606/1212715767
http://www.syuhari.jp/blog/archives/1621
http://journal.mycom.co.jp/articles/2009/03/06/phpexcel/index.html

以下、PHP Excel + Zend Frameworkの例です。

ここここなんかを参考にPHP Excelのダウンロードと展開、パスの設定。

Excel95形式(.xls)を扱う限りはphp_zipは必要なさそうです。

PHP Excelに身を委ねてExcelファイルを生成。
生成した一時ファイルを返却。
Content-Typeは「application/vnd.ms-excel」らしい。

include 'PHPExcel.php';
class ExcelController extends Zend_Controller_Action
{
    
    public function testAction()
    {
        // 一時ファイル
        $fileName = '/tmp/output.xls';
        
        $excel = new PHPExcel();
        // シートの設定
        $excel->setActiveSheetIndex(0);
        $sheet = $excel->getActiveSheet();
        $sheet->setTitle('sheet name');
        // セルに値を入れる
        $sheet->setCellValue('A1', 'あいうえお');
        // Excel95 形式で出力
        $writer = PHPExcel_IOFactory::createWriter($excel, 'Excel5');
        $writer->save($fileName); // ホントはファイル出力せずにバイナリで返して欲しい
        
        // ダウンロード
        $this->getResponse()
             ->setHeader('Content-Type', 'application/vnd.ms-excel')
             ->setHeader('Content-Disposition', 'attachment; filename="test.xls"')
             ->appendBody(file_get_contents($fileName))
             ->sendResponse();
        exit;
    }
    
}

こんな感じで、
example.com/excel/test/
にアクセスでtest.xlsのダウンロード。

========================
2010/06/27追記

IEの場合、キャッシュ関係のヘッダがないとダウンロードできないらしい。
Pragmaヘッダが無いと、「このインターネットサイトを開くことができませんでした。要求されたサイトが使用できないか、見つけることができません。後でやり直してください。」とのエラー。

include 'PHPExcel.php';
class ExcelController extends Zend_Controller_Action
{
    
    public function testAction()
    {
        // 一時ファイル
        $fileName = '/tmp/output.xls';
        
        $excel = new PHPExcel();
        // シートの設定
        $excel->setActiveSheetIndex(0);
        $sheet = $excel->getActiveSheet();
        $sheet->setTitle('sheet name');
        // セルに値を入れる
        $sheet->setCellValue('A1', 'あいうえお');
        // Excel95 形式で出力
        $writer = PHPExcel_IOFactory::createWriter($excel, 'Excel5');
        $writer->save($fileName); // ホントはファイル出力せずにバイナリで返して欲しい
        
        // ダウンロード
        $this->getResponse()
             ->setHeader('Content-Type', 'application/vnd.ms-excel')
             ->setHeader('Content-Disposition', 'attachment; filename="test.xls"')
             ->setHeader('Cache-Control', 'no-cache') // IE
             ->setHeader('Pragma', 'no-cache')        // IE
             ->appendBody(file_get_contents($fileName))
             ->sendResponse();
        exit;
    }
    
}

========================
2010/07/10追記

atachementのfilenameはそのまま送出するとブラウザによって文字化けする。

※参考
http://oku.edu.mie-u.ac.jp/~okumura/php/filename.php
http://d.hatena.ne.jp/guangda/20100106/1262762061
http://d.hatena.ne.jp/guccyon/20080530/p1

とりあえず、お手軽安全なのはSJISに変換か。。
Win環境以外は知らないことにしよう。

// ダウンロード
$fileName = mb_convert_encoding($fileName, 'SJIS');
$this->getResponse()
     ->setHeader('Content-Type', 'application/vnd.ms-excel')
     ->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"')
     ->setHeader('Cache-Control', 'no-cache') // IE
     ->setHeader('Pragma', 'no-cache')        // IE
     ->appendBody(file_get_contents($fileName))
     ->sendResponse();

2010年5月15日土曜日

javascriptのform.submitが効かないとき

form.submitアクションがどのブラウザでも効かないなんて・・・。
なんて悩んでいたところ全く同じ事象を挙げて下さっている方がいたのでリンク。
nameには気をつけろ、と。。

form.submit()ができない!?

2010年5月6日木曜日

お手軽にlightboxっぽくformをポップアップ表示してみる

エヴァンゲリオンが出てから「セカイ系」というジャンルが確立したように、lightboxが出てから「lightbox系」というポップアップ表示スクリプトのジャンルが確立しつつあるようです。

google先生に「lightbox系」でお伺いを立ててみれば、色々な方が画像ポップアップのスクリプトをまとめて下さっているのがよく分かります。

WEBにちょっとした彩を与えてくれるlightbox。
画像のポップアップだけじゃなくてformもポップアップで出したいよね!

本家lightboxはもちろん、lytebox、lightwindow、SimpleModalなどのlightbox系ライブラリでformのポップアップ表示が可能なようです。

実現方法は色々あるのですが、うぁ!すっげー!!みたいなエフェクトは要らないし、ライブラリのAPI調べるのも面倒だなー、ってな時にお勧めのformのポップアップスクリプトを見付けましたのでご紹介。

Form in a Lightbox

ここに書かれている通りなのですが、少し補足。

このライブラリはHTMLの中の<div id="box">なエレメントをポップアップっぽく表示しているだけです。
ですので、formの入力値を親ウインドウに渡したい時に親ウインドウのハンドルを取ってきたりする必要はなく、そのままdocument.getElementByIdなんかで親ウインドウやポップアップウインドウの要素も取得できます。

デモソースが格納されているアーカイブがありますので、ダウンロードして試してみます。
http://www.xul.fr/javascript/lightbox-form.zip

展開してブラウザで表示してみれば、クライアントだけで完結していることが分かります。

少し手を入れて、ポップアップformの属性を親ウインドウで取得してみます。

ダウンロードした
lightbox-form/lightbox-form-demo.html
を編集します。

親ウインドウの<div id="hoge">hoge</div>要素にポップアップウインドウで選択した都市名を表示させてみます。

<script type="text/javascript">
function hookSelect() {
  var select = document.getElementById("select_city");
  var text = select.options[select.selectedIndex].text;
  var div = document.getElementById("hoge");
  div.innerHTML = text;
}
</script>

こんな感じのスクリプトをonchangeで引っ掛けます。

<select name="select" id="select_city" onchange="hookSelect();">

最終的なHTMLは以下。

<html>
<head>
<title>Form in a Lightbox, the Demonstration</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link type="text/css" rel="stylesheet" href="lightbox-form.css">
<script src="lightbox-form.js" type="text/javascript"></script>
<script type="text/javascript">
function hookSelect() {
  var select = document.getElementById("select_city");
  var text = select.options[select.selectedIndex].text;
  var div = document.getElementById("hoge");
  div.innerHTML = text;
}
</script>
</head>

<body>
<div id="hoge">hoge</div>
<h1>Form in a Lightbox, the Demonstration</h1>
<p>Demo of the use of a lightbox to show a form to fill.</p>

<br>

<div id="filter"></div>
<div id="box">
  <span id="boxtitle"></span>
  <form method="GET" action="lightbox-formulaire-test.html" target="_parent">
      
    <p>Email adress: 
      <input type="text" name="email" value="myself@somedomainname.com" maxlength="60" size="60">
      </p>
      
    <p>Male 
      <input type="radio" name="genre" value="man" checked>
      Female 
      <input type="radio" name="genre" value="woman">
    </p>
      
    <p> City of current residence 
      <select name="select" id="select_city" onchange="hookSelect();">
        <option selected>New York</option>
        <option>Chicago</option>
        <option>Miami</option>
        <option>Los Angeles</option>
        <option>Dallas</option>
      </select>
      </p>
    <p> 
      <input type="submit" name="submit">
      <input type="button" name="cancel" value="Cancel" onclick="closebox()">
    </p>
    </form>
</div>


<p> To make the form appearing, <a href="#" onclick="openbox('Title of the Form', 1)">click 
  here</a>. <br>
  In this example, a fading effect is applied, this may be disabled, to do so, 
  click on the <a href="#" onclick="openbox('Title of the Form', 0)">form with 
  no fading effect</a>.</p>


<br>

<hr>

<p>(c) 2008 <a href="http://www.xul.fr/xul.html" target="_parent">xul.fr</a></p>
</body>
</html>

これで選択した都市名が親ウインドウにも反映されるようになります。
ウインドウハンドルとか何にも要らないし、楽チン~~。

2010年5月5日水曜日

HTMLのテーブル(table)を行列固定でスクロールさせる

技術屋さんの視点からは「結構キツイな~」と思うことでも、お客様からは「えっ!?それぐらいできないの?」なんて思われることが結構あります。

その代表格はtableをExcelっぽくして欲しい、という要望ではないでしょうか。
なまじっかGooleDocsなんてものがあったりするのでExcelを日常的に使用されているお客様はWEBでもそのようなI/Fを求められます。

ExcelっぽいHTMLテーブル、、
結構キツイのよね・・・。

Flash使える場合はその方向になりますが、1画面だけ、なんて要件でしたらJavascriptで何とかしたい。

JSでテーブルをExcelっぽくするライブラリには、まとまったものではYahooUIやDojoがありますが、行列を固定したスクロールは調べた限りでは出来なさそうでした。

そんな時のJSライブラリ。


・Super Tables

http://www.matts411.com/post/super_tables/

※参考
http://c-brains.jp/blog/wsg/09/06/16-151445.php

1. CSSとJSのロード

こんな感じで。

<style type="text/css"> 
  @import "/js/Super_Tables_0_30beta_compressed/stylesheets/superTables_compressed.css";
</style> 
<script src="/js/Super_Tables_0_30beta_compressed/javascripts/superTables_compressed.js" type="text/javascript"></script>

2. テーブル全体のスタイル

こんな感じで。

<style type="text/css">
.fakeContainer {
    margin: 0 0 20px;
    border: none;
    width: 400px;
    height: 100px;
    overflow: hidden;
}
</style>

3. テーブル作成

<div class="fakeContainer">
<table id="demoTableA">
  <tr>
    <th>header00</th>
    <th>header01</th>
    <th>header02</th>
    <th>header03</th>
    <th>header04</th>
    <th>header05</th>
    <th>header06</th>
    <th>header07</th>
    <th>header08</th>
    <th>header09</th>
  </tr>
  <tr>
    <th>header10</th>
    <th>header11</th>
    <th>header12</th>
    <th>header13</th>
    <th>header14</th>
    <th>header15</th>
    <th>header16</th>
    <th>header17</th>
    <th>header18</th>
    <th>header19</th>
  </tr>
  <tr>
    <td>data00</td>
    <td>data01</td>
    <td>data02</td>
    <td>data03</td>
    <td>data04</td>
    <td>data05</td>
    <td>data06</td>
    <td>data07</td>
    <td>data08</td>
    <td>data09</td>
  </tr>
  <tr>
    <td>data10</td>
    <td>data11</td>
    <td>data12</td>
    <td>data13</td>
    <td>data14</td>
    <td>data15</td>
    <td>data16</td>
    <td>data17</td>
    <td>data18</td>
    <td>data19</td>
  </tr>
  <tr>
    <td>data20</td>
    <td>data21</td>
    <td>data22</td>
    <td>data23</td>
    <td>data24</td>
    <td>data25</td>
    <td>data26</td>
    <td>data27</td>
    <td>data28</td>
    <td>data29</td>
  </tr>
  <tr>
    <td>data30</td>
    <td>data31</td>
    <td>data32</td>
    <td>data33</td>
    <td>data34</td>
    <td>data35</td>
    <td>data36</td>
    <td>data37</td>
    <td>data38</td>
    <td>data39</td>
  </tr>
</table>
</div>

4. 行列固定設定

HTMLを読み込んでからこんな感じでスクリプト実行。
(</body>直前に書けばいい)

<script type="text/javascript">
//<![CDATA[
(function () {
 new superTable("demoTableA", {
  cssSkin : "sOrange", // eg. "sDefault", "sSky", "sOrange", "sDark"
  headerRows : 1, // 固定する行数
  fixedCols : 1 // 固定する列数
  // onStart : function(), // スクリプトを実行できるらしい(試してない)
  // onFinish : function(), // スクリプトを実行できるらしい(試してない)
 });
})();
//]]>
</script>


・jquery

※元ネタ
http://fixed-header-using-jquery.blogspot.com/2009/05/scrollable-table-with-fixed-header-and.html

上述のSuper Tablesでほとんどの場合は事足りることと思われますが、テーブルにFormを入れ込まないといけなくなると途端に使えなくなります。
Super Tablesは普通のtable記述にclass打つだけで実現できるので非常に分かりやすいのですが、固定スクロールを実現するために裏でダミーのtableを複製したりしており、formをsubmitした際に新しいデータが飛んできません。(同一nameやidを持つformエレメントがSuper Tablesによって裏でいくつか複製されます。)

Super Tablesに比べてやや分かりにくくはなりますが、Pavanさんが作成してくれたリンク先のスクリプトを適用すればFormが入ったスクロールテーブルも実現可能です。
確かめてはいませんが、IE6、7、8、FF3、3.5、Chrom2で動作するとのことです。

また、リンク先の記述ではjquery-1.3.2.jsを使用していますが、jquery-1.4.2.min.jsでも動作しています。

※デモ
http://acatalept.com/common/test/fixed-table.html
※ソース
http://snipt.org/loz

2010年5月3日月曜日

Zend_Validateのメッセージを日本語化

※参考
http://framework.zend.com/manual/ja/zend.validate.introduction.html
http://framework.zend.com/manual/ja/zend.validate.messages.html

Zend Framework標準のバリデータはちゃんと使いこなせばすごく便利なのですが、デフォルトのエラーメッセージは英語になっています(当然ですが)。

実際に使用する際には日本語化する必要がありますし、また、標準のメッセージを改変しないといけないこともままあります。
このメッセージを日本語化するのがやや面倒で、以下のようなコードを記述する必要があります。

$validator = new Zend_Validate_StringLength(array('encoding' => 'utf8', 'min' => 0, 'max' => 40));
$validator->setMessage('文字列 \'%value%\'は%min%文字以上 %max%文字以下で入力してください。');

メッセージは200個ほどあるようですし、バリデータ生成するたびにメッセージ設定していればそれこそやってられません。

Zendのドキュメントにメッセージを一括で多言語化する方法が記載されていましたが、そこで紹介されているコードでは動作しない部分がありましたので以下メモ。

Zend Framework is shipped with more than 45 different validators with more than 200 failure messages. It can be a tendious task to translate all of these messages. But for your convinience Zend Framework comes with already pre-translated validation messages. You can find them within the path /resources/languages in your Zend Framework installation.
So to translate all validation messages to german for example, all you have to do is to attach a translator to Zend_Validate using these resource files.

$translator = new Zend_Translate(
'array',
'/resources/languages',
$language,
array('scan' => Zend_Locale::LOCALE_DIRECTORY)
);
Zend_Validate_Abstract::setDefaultTranslator($translator);

※参考「http://framework.zend.com/manual/ja/zend.validate.messages.html」より


1. 言語ファイルの取得

ZendFramework-1.10.4/resources/languages
に多言語化ファイルが含まれています。

ただ、Full Packageにしか含まれていないようです。
ダウンロードした資材に言語ファイルが含まれていなければ、リポジトリから取得するかFull Packageをダウンロードします。

・リポジトリ:http://framework.zend.com/code/browse/Zend_Framework
/standard/trunk/resources/以下のファイル
・パッケージ:http://framework.zend.com/download/latest


2. 言語ファイルの配置

日本語にしたい場合、必要なのは以下のファイルのみです。
ZendFramework-1.10.4/resources/languages/ja/Zend_Validate.php

任意の場所に言語ファイルを配置します。

後からメッセージも変更することでしょうし、ここではZendのインストールディレクトリではなく以下のアプリケーションディレクトリ配下に配置することとします。
/APP_ROOT/library/lang/Zend_Validate.php


3. 言語ファイルのロード

言語ファイルをロードし、Zend_Validateのメッセージを日本語化します。
これもどこで行ってもいいわけですが、Bootstrapで実行することにします。

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initLang()
    {
        // 言語ファイルのロード
        $translator = new Zend_Translate(
            // Arrayアダプタを使って言語定義を取得
            'array',
            // 言語リソースのパス
            realpath(APPLICATION_PATH . '/../library/lang/Zend_Validate.php'),
            // 日本語ロケール
            'ja',
            // ファイル指定してますよー
            array('scan' => Zend_Translate::LOCALE_FILENAME)
        );
        // デフォルトのトランスレータを設定
        Zend_Validate_Abstract::setDefaultTranslator($translator);
    }
}

バージョンが違うのかもしれませんが、Zendのリファレンスに記載されている以下のコードでは動作しませんでした(Zend1.10.4)。

$translator = new Zend_Translate(
    'array',
    // インクルードパスの問題かもしれないが、フルパスでないと見れなかった。
    '/resources/languages',
    $language,
    // Zend_Locale::LOCALE_DIRECTORYなんて無い。
    // Zend_Translate::LOCALE_DIRECTORYの間違い?
    array('scan' => Zend_Locale::LOCALE_DIRECTORY)
);
Zend_Validate_Abstract::setDefaultTranslator($translator);


4. バリデートしてみる

class HogeController extends Zend_Controller_Action
{
    public function fugaAction()
    {
        $validator = new Zend_Validate_StringLength(array('min' => 0, 'max' => 3));
        $errors = array();
        if (!$validator->isValid('12345')) {
            foreach ($validator->getMessages() as $messageId  => $message) {
                $errors[] = $message;
            }
        }
        var_dump($errors);
        exit;
    }
}

・多語化前
array(1) {
  [0]=>
  string(38) "'12345' is more than 3 characters long"
}

・多語化後
array(1) {
  [0]=>
  string(39) " '12345' は 3 文字より長いです"
}

となればOK。
後は、言語ファイルを直接編集していけばメッセージの変更も一箇所で管理可能。

2010年5月2日日曜日

削除フラグって必要?

休日なので、結論の出ない軽い話題を。

DB設計の時に、場合によっては全テーブルに「delete_flag」なんていう名前でフラグを持たせることがままあります。
テーブルのデータを実際に削除せずに、「削除フラグ(delete_flag)」をONにすることで「削除したものとして扱いますよ」というフラグです。

ちなみに、実際にデータを削除することを「物理削除」、削除フラグを立てて削除したものとみなすことを「論理削除」と呼んだりもします。

この削除フラグ、特に意識せずについつい付けてしまうことが多いのですが、本当に必要なのかね・・・?というのが趣旨です。

※恐らく日本では、ある程度の規模以上のシステム開発の常識では削除フラグは絶対必要だと考えられていますので、開発リーダーに「削除フラグ要らないんじゃないっすか」みたいなことを言ってみて一蹴されても責任は負いません。


■削除フラグの根拠

削除フラグを付ける根拠としては、

1. DELETE処理よりUPDATE処理の方が早い
2. 業務データは消さない(or 履歴を残したい)

の2点が真っ先に挙げられると思います。

1.の場合、バッチ処理で後から物理削除したりもします。


■UPDATEの方が早い?

DELETE処理よりUPDATE処理の方が早いので削除フラグを持たせる、という理屈は納得できます。
ただ「そこの差」がクリティカルになるシステムは極めて限定的なものだと思われます。
DELETEとUPDATEでベンチマーク取った訳ではないですが、SQL改良したり色々キャッシュさせたり、他にするべきこと一杯あるんじゃないかな、と思っちゃったりします。


■業務データは消さない?

履歴を持ちたいから削除フラグを付ける、という理屈は納得できません。
履歴が欲しければ履歴テーブルにINSERTするべきで削除フラグで代用すべきではないと思います。
実際、今まで開発してきたシステムで履歴を参照する必要がある場合は必ず履歴テーブルを別途用意していましたので、削除フラグなんて有っても無くても一緒でした。

履歴テーブルがないテーブルの削除データを参照することは稀ですし、その必要性が出てきた段階で何だか設計間違ってるような気がします(感覚的なものですが)。

「データ復旧するかもしれないしな~」なんて思いながら削除フラグを付けることも多いのですが、データ復旧が必要な時点でそもそもシステムとして不備がある訳で・・・。

「削除したことを知る」ためのオペレーションログのような使い方も削除フラグにはあるようですが、その場合でも、正攻法はロギングの処理をちゃんと実装するべきでしょう。


■削除フラグの弊害

削除フラグがある為にいちいちSQLが面倒なことになります。
where delete_flag = '0'
って絶対書かないといけない。

SELECTの時だけでなく、UPDATEの時も毎回々々
where delete_flag = '0'

joinしてるとテーブルの数だけ削除フラグを条件に追加しないといけない。

ORマッパーなんて使ってみた日には、更にひどいことになります。
ORマッパーの機能でテーブルエンティティ間のリレーションを定義してみたものの、削除フラグだけ別に処理しないと意図するデータが取得できなくなります。
※この辺考えてくれてるORMって無いような気がします。削除フラグって日本独特なのかな・・・?

「多言語化するかも」と思ってつけておいた言語IDなんかも同じような悲劇を生みますよね?
毎回々々
where lang_id = 'ja'
みたいな・・・。
結局多言語化なんてしたことないし。

また、データを削除する際も削除フラグだけONにすれば良いわけではなく、個人情報が含まれているとデータのマスク処理も必要になります。


■削除フラグは要らない、かも

まぁ、好き好きなので結論は出ないんですが、仮に

A. DELETE処理よりUPDATE処理の方が早い、なんてことは気にしない
B. 履歴を残したい、なら履歴テーブルを作るべき

とするのであれば、削除フラグを追加すると、

1. SELECT、UPDATEのクエリ発行時に無駄な手間が毎回必ず必要になる
2. 物理削除する場合はバッチを別途作成する必要がある
3. 論理削除の際にデータにマスクをかける必要がある
4. JOINが面倒
5. 履歴として参照することはない

なんて削除フラグはわざわざシステムを複雑にするため以外の何者でもないように思えてきます。

ただ、「心持ちの問題」として稼動していた業務データを永久に葬り去る物理削除には多少のためらいもあります。
getterやsetter結構好きだったり、更新日時と作成日時をテーブルに持たせたりしますので、どちらかと言えば「削除フラグ擁護派」なのですが、やっぱり冷静に考えれば削除フラグ意味無いんじゃないかと思い出した、そんな今日この頃です。これからはためらいの心にケリをつけて、「削除フラグ撲滅派」に鞍替えします。

おしまい。