PHPの標準コーディング規約いろいろ

標準コーディング規約(コーディング規約とかコーディング標準などとも言う)とは、プログラムソースに関して、変数名やメソッド名、改行やタブなどの、コーディングに対する各種のルールを定めたものである。

この、コーディング標準のような明快なルールがあると、開発効率やメンテナンス性の向上に役立つ。

というわけで、いくつかの標準をご紹介。

PEAR標準コーディング規約

Manual::標準コーディング規約

WordPressにもコーディング規約があって、そちらと矛盾する部分もあるのが悩ましい。

要約すると以下のような感じ。

実際にはここでは書ききれないほど細かく定義があって、そりゃーもう大変だが、専用のチェックツールも用意されている。

インデントはスペース4つで、タブは使用しない。

こうすると、diff や patch、CVS history や annotations に問題が生じないとのことだが、タブを使っても問題は出ないような。。。

if や for などの制御構造のカッコの前後にスペースを入れる。

if ($foo) {
    bar();
}

関数コールにはカンマとパラメータの間にスペースをおく。

ちなみに、カッコの内側にはスペースを置かないことに注意。
また、イコールの前後にはスペースを入れる。

$foo = bar($par1, $par2, $par3);

値の代入

2行連続して値を代入する場合は、以下のようにイコールの位置をそろえる。

$var      = bar();
$variable = foo();

1行あたりの文字数

76文字以内とする。
これを守るのに、いろいろと工夫が必要(^^;)

関数定義

以下のように開始波カッコの前に改行する。またデフォルト値があるパラメータは最後に定義する。

function myfunc($par1, $par2 = 0)
{
  return $par1 + $par2;
}

命名規則

話せば長いのでこちらを。

WordPress コーディング基準

WordPress コーディング標準

PEAR 標準コーディング規約に似ているが、いくつかの点が違う。
また、PEAR側で触れられていない点にも言及されている。

インデントは本物のタブを使用する

「スペースはダメ」と明快に書かれていることに注意。
ただし、何かを整列させて読みやすくしたいコードブロックがあれば、最初のインデントはタブを使い、その後でスペースで差分を埋めることができるとのこと。

スペース

コンマの後ろや、論理演算子、代入演算子の両側、開きカッコ、閉じカッコの両側には、常にスペースを入れる。

$foo = bar( $par1, $par2, $par3 );

命名規則

単語と単語の間にアンダースコアを使用する。PEARではクラスメソッドのみcamelCase()のように2つ目以降の単語の先頭を大文字にする必要があるが、WordPressではこれを禁じている。

その他

WordPress のコーディング標準にはSQL文などについても記述されている。

株式会社 社会式株 PHPコーディング規約

このコーディング規約はとてもユニーク。

以前に「これはネタですよね?」と言われたことがあるが、これはこれで理にかなっていて、興味深かった。

プロジェクトチームのスキルが低い場合は、こういう選択肢もありだと思う。

株式会社 社会式株 PHPコーディング規約

要約すると以下のような感じ。

  • PEARやSmartyなどのツールは学習コストが高いので使用しない。
  • PHPUNIT で自動テストを行うが、あまり期待しすぎないこと。
  • DBアクセスにのみPEARを使用する。それ以外ではPEARを使用しない。
  • オブジェクト指向禁止。(オブジェクト指向にしたいなら選択する言語が間違っているという趣旨だと思う。)
  • ライブラリは使用禁止。あまり優秀でないプログラマに引き継ぐ際に足かせになるとのこと。(バグ修正などで影響範囲が広くなってしまうことを言っているのだと思う。)
  • HTMLとPHPコードの分離。WordPress のテンプレートタグのようなイメージで書けということだと思う。
  • ファイル構造は単純に。

その他のコーディング規約

以上いくつかのコーディング規約を紹介したが、それぞれ思想があって、とても興味深いと思う。

phpで10進数及び62進数の相互変換

数学力の無さが災いして、すごく手こずった。。。
ちょっと、自信がない気もするのでバグがあれば教えていただけると超感謝します。

10進数の数字をradixEncode()でアルファベットも含めた62進数に変換し、radixDecode()でもとに戻す。

極端に大きな数字では上手く動作しないが、これはphpのコンパイルオプションか何かで解決すべき問題のような気がする。

クラスにした方がいいような気もするけど。。。

<?php

function radixEncode($n){
    $char = array_merge(
        range(0,9),
        range('a','z'),
        range('A','Z')
    );
    $cn = count($char);
    $str = '';
    while ($n !== 0) {
        $str = $char[$n % $cn].$str;
        $n = (int) ($n / $cn);
    }
    return $str;
}

function radixDecode($n){
    $char = array_merge(
        range(0,9),
        range('a','z'),
        range('A','Z')
    );
    $cn = count($char);
    for ($i=0; $i<$cn; $i++) {
        $chars[$char[$i]] = $i;
    }
    $str = 0;
    for ($i=0; $i<strlen($n); $i++) {
        $str += $chars[substr($n, ($i+1)*-1, 1)] * pow($cn, $i);
    }
    return $str;
}

?>

phpで画像をリサイズして出力

phpで画像を一定の横幅にリサイズして出力する。

わりと使うことが多いわりに、毎回あっちこっち探すので。。。
これだけでは動作しないので、とても中途半端な情報で恐縮です。

今回はこれで使った。

詳細

IMG_DIR内のファイルを読み込んで、IMG_Wで指定された横幅にリサイズした画像を出力するとともに、CACHE_DIR内にリサイズ後の画像を保存する。

mod_rewriteと組み合わせることで、次回以降は普通に画像が出力されるので、phpスクリプトのオーバーヘッドがない。

//
// display image data
//
function show_image(){
    $cache_dir = CACHE_DIR;
    if( !is_dir($cache_dir) && !is_writable($cache_dir) ){
        if( !mkdir($cache_dir) ){
            trigger_error($cache_dir.' is not writable', E_USER_ERROR);
            exit;
        }
    }
    $fname = basename($_SERVER['REQUEST_URI']);
    $cache = $cache_dir . '/' . $fname;
    if(is_file(IMG_DIR.'/'.$fname)){
        list($w,$h) = getimagesize(IMG_DIR.'/'.$fname);
        $bin = file_get_contents(IMG_DIR.'/'.$fname);
        $_w = IMG_W;
        $resize = $_w / $w;
        if( $resize < 1 ){
            $width = round($w * $resize);
            $height = round($h * $resize);
            $im = imagecreatetruecolor($width, $height);
            $_im = imagecreatefromstring($bin);
            imagecopyresampled($im, $_im, 0, 0, 0, 0, $width, $height, $w, $h);
            header('Content-type: image/jpeg');
            imagejpeg($im, $cache);
            print file_get_contents($cache);
        }else{
            $fp = fopen($cache, 'w');
            fwrite($fp, $bin);
            fclose($fp);
            header('Content-type: image/jpeg');
            print $bin;
        }
        exit;
    } else {
        header('Content-type: image/jpeg');
        readfile(IMG_NOTFOUND);
        exit;
    }
}

.htaccessは以下のような感じ

# BEGIN WordPress

RewriteEngine On
RewriteBase /twt/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /twt/index.php [L]

# END WordPress

php5でdomxmlを使う

もう何年も前に納品して、幸か不幸か保守も切れていた製品の修正作業をうけてしまった。

ところが、このシステムはPHP4でつくられていて、サーバーもいまだにPHP4だったのが運のつきで、予想外に苦戦してしまった。

php5ではdomxmlが使えない

あー、すっかりこのことを忘れてた。

いまさらなんだけど、こういうのって納得できないぞ。

http://jp.php.net/manual/ja/intro.domxml.phpより

注意: この拡張モジュールは » PECL レポジトリに移動 されており、以下のバージョン以降 PHP にバンドルされなくなっています。 PHP 5.0.0.

注意: この拡張は実験的なものではありません。しかしながら、PHP 5 版は決してリリースされないでしょう。PHP 4 でのみ配布されます。 もし PHP 5 でDOM XML をサポートする必要がある場合、 DOM 拡張を使用することができます。 この domxml 拡張は DOM 拡張と互換性はありません。

php5でdomxml関数をエミュレートするクラス

というわけで、当初はphp4のサーバーを構築しようかと思ったのだが、よく考えたら、こんなもの誰かエミュレーションするクラスを作ってるんではないか?と思ったら案の定あった。ラッキー。

http://alexandre.alapetite.fr/doc-alex/domxml-php4-php5/

使い方は簡単。

if (PHP_VERSION>='5')
 require_once('domxml-php4-to-php5.php');

とするだけ。

これで、従来通りのdomxml関数が使えた。

ところで

このクラスがあるからといって、これから作るシステムでは使うのはやめましょう。

Smartyで確認画面などのhiddenを出力する。

入力フォームなどで、確認画面を用意する際に、前のページから送信されたpostデータからそのままhidden要素を生成したいときがある。

そんなときは、以下のようなテンプレートタグを設置すればよい。

{foreach from=$smarty.post|smarty:nodefaults key="key" item="item"}
{if is_array($item)}
 {foreach from=$item key="key2" item="item2"}
 <input type="hidden" name="{$key}[{$key2}]" value="{$item2}" />
 {/foreach}
{else}
 <input type="hidden" name="{$key}" value="{$item}" />
{/if}
{/foreach}

上記のソースでは配列が送信された場合でもhiddenを生成する。

多次元配列も考慮に入れたい場合は、プラグインを作る必要があるとおもう。

この記事よくわからないですよね???
今回は毎回同じようなことをしているので、わたしの覚え書きということで。

PHP5.3にアップグレードするのは要注意です!!

最近PHPに文句ばっかりいっていますが。。。

ある事情でPHP5.3でサーバーを構築したら、ちょっと古いバージョンのWordPressが動かない。。。

真っ白な画面が出ているではないか。(何も出ていないとも言う。。。)

Function split() is deprecated

エラーログを見ると、上記のような記述が。。。

うちでつくったテーマファイルに、split関数が使われていて、それで怒っているらしい。

マニュアルを見ると以下のような記述を発見した。

注意: PHP 5.3.0 以降、 regex 拡張モジュールは非推奨となりました。この関数をコールすると E_DEPRECATED が発生します。

念のため、regex拡張モジュールとやらの関数を確認すると、この子たちは全部ダメなのね。

。。。

。。。

。。。

警告じゃないじゃん、うごかないぞーーー。

[Fri Aug 21 12:50:57 2009] [error] [client 211.120.169.111] PHP Deprecated: 
Function ereg() is deprecated in /path/wp-content/themes/default/functions.php on line 73
[Fri Aug 21 12:50:57 2009] [notice] child pid 10224 exit signal Segmentation fault (11)

Segmentation faultなんていってるし。
(これは今回の環境だけのような気もするが。。。)

とりあえず、ダウングレードしたら解決した。(?)

なんじゃそりゃ。

PHPのfilter_var()で入力チェック

PHP5.2で追加されたfilter_var()関数

PHP5.2から、filter_var()という、おせっかい(?)な関数が追加された。

これを使用すると、以下のような感じでURLやメールアドレス、IPアドレスの入力チェックができる。

filter_var()によるURLのチェック

if (filter_var($url, FILTER_VALIDATE_URL)) {
 print '有効なアドレスです。';
}

マニュアルを調べると、FILTER_VALIDATE_URLの部分には他にもいくつかの検証用フィルターが定数として定義されている。

検証フィルタ

filter_var()によるメールアドレスのチェックは。。。

これらのフィルターの中には、FILTER_VALIDATE_EMAILというメールアドレス用のフィルタも定義されていた。

以下はマニュアルからの抜粋です。

<?php
$email_a = 'joe@example.com';
$email_b = 'bogus';

if (filter_var($email_a, FILTER_VALIDATE_EMAIL)) {
 echo "(email_a) はメールアドレスとして有効です。";
}
if (filter_var($email_b, FILTER_VALIDATE_EMAIL)) {
 echo "(email_b) はメールアドレスとして有効です。";
}
?>

しかーーーし、これでは我が国では不十分。

念のため、悪名高いDocomo風のメールアドレスを通してみると、やっぱりfalseがかえってきた。

echo filter_var('docomo.@example.com' FILTER_VALIDATE_EMAIL);

というわけで、使えそうで使えないfilter_var()でした。

ちなみに

こういう関数って、実装そのものは面白いんだけどコア関数として追加してしまうのはどうかと思う。

おせっかいでしょ、やっぱ。

と言いつつ使っちゃったけど。。。

PHPで画像をMySQLに保存する。

CMSなどで画像をアップロードする機能を実装する際にその画像をDBに保存すると、サーバーを冗長化する際やサムネールを作成する機能を実装する際にいろいろと便利になるので、そのやり方をご紹介します。

DBに保存するメリット

  1. サーバーを冗長化する際に、画像をファイルとして保存するとそれぞれのサーバー間でアップロードデータの同期やNASの導入が必要となったりするが、DBに保存してしまえばその必要がなくなる。
  2. サムネール画像を作成するなどの機能で、途中で仕様変更等によりサムネール画像のサイズを変更したい場合などの、画像の出力フォーマットの変更などにも柔軟に対応できる。

DBに保存するデメリット

  1. DBに保存してそれを読み出すプログラムを経由する分、処理が多くなり必然的に負荷の増大につながる。
  2. DBのメンテナンス性の低下につながりやすい。
  3. レンタルサーバーなどで制限が出てくる。(たとえばMySQLのMAX_ALLOWED_PACKETなど)

DBに保存するときのコツ

上記のデメリットのうち付加が増大する点については、mod_rewriteを使用することで解決できます。

  1. 画像を出力する際にキャッシュとして出力結果を保存する。
  2. mod_rewriteでキャッシュファイルがある場合はPHPスクリプトにアクセスさせないで画像に直接アクセスするようにする。

DBスキーマサンプル

以下のようなテーブルを作成する。

画像のアップロードプログラムであらかじめ、mime_typeや画像の高さや幅を取得しておいてDBに保存しておくと、出力時になにかと都合がいいことが多いです。

画像のコンテンツは、m_contentに保存します。
base64でフォーマットするというような例も多いようですが、ここでは生で保存します。

CREATE TABLE `tbl_bin` (
 `m_id` varchar(32) NOT NULL,
 `m_content` longblob NOT NULL,
 `mime_type` varchar(100) NOT NULL,
 `width` smallint(5) unsigned NOT NULL,
 `height` smallint(5) unsigned NOT NULL,
 `m_modified` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
 PRIMARY KEY  (`m_id`),
 KEY `m_modified` (`m_modified`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

保存用PHPプログラム

以下はあくまでもサンプルです。
画像ファイルのコンテンツは以下のような感じでSQL文に挿入します。

$image = mysql_real_escpa_string(file_get_contents( $_FILE['image']['tmp_name'] ));

画像の幅や高さmime-typeは、getimagesize()で取得してください。

m_idには拡張子つきのファイル名を保存するのがベターです。
重複を避けるためにユーザーがつけたファイル名はさけて、自動的にファイル名を生成するようにしたほうがいいとおもいます。

出力用PHPプログラム

以下の例では、プログラム名はmedia.phpで、キャッシュを保存するディレクトリ名はmedia/であることを前提にしています。
これらを変更したい場合は、必要に応じて読み替えてください。

もっとも簡単な例では、以下のようなプログラムで画像の出力が可能です。
(あらかじめDBに接続する構文を記述してください。)

// URLからファイル名の部分を取得
$url= parse_url($_SERVER['REQUEST_URI']);
$id = basename($url['path']);

// DBから画像データを取得
$sql = "select m_content, mime_type from tbl_bin
  where m_id='".mysql_real_escape_string($id)."' limit 0,1";
$result = mysql_query($sql);

if (mysql_num_rows($result)) {
  $data = mysql_fetch_assoc($result);
    // キャッシュを保存
    $fp = fopen(dirname(__FILE__).'/media/'.$id, 'w');
    fwrite($fp, $data['m_content']);
    fclose($fp);
    // 画像を出力
    header('Content-type: '.$data['mime_type'].';');
    print $data['m_content'];
}else{
    header("HTTP/1.0 404 Not Found");
    print 'file not found';
}

.htaccessを設置

上述のPHPスクリプトで、アクセスがあった際に画像を保存するようにしたので、次はmod_rewriteでキャッシュがあればそのキャッシュを使用するようにする。

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule media/ media.php [L]

テスト

実際に画像(media.php)にアクセスしてみてキャッシュができるかどうかを確認したあと、media.phpをあえてリネームするなどして、同じURLで画像が再び表示されればOK。

以降は画像にアクセスがあってもDBへ接続は発生しません。

画像が増えすぎてディスク容量を圧迫するようなら、Cronで定期的に古い画像を削除するなどの処理を行うことで緩和されます。

Mail_CheckUser-0.1.1aリリース及びPEARへの登録について

現在このパッケージは、PEAR公式サイトで登録に向けて議論を進めており、それに伴いいくつかの修正が施されました。

今回の修正で、これまで配布していたものと互換性がなくなりましたので、既にご利用中の皆様には深くお詫びいたします。

PHP5への変更

PEARでは、PHP4でのパッケージの新規登録はおわりということで、PHP5に書き直してほしいとのことだったので、PHP5で書き直しました。

今後のPEARパッケージは全てPHP5でかつE_STRICT/E_DEPRECATEDでの警告が出ないことが条件となるそうです。

public変数の扱いの変更

public変数は全てprotected変数にするかわりに、set*()などのようにその変数を書き換えるための関数を用意しました。

たしかに、これはとても安全でよいアイディアなので、そのように変更しましたが、この修正に伴い従来とは使用方法が変わってしまったので、修正をお願いいたします。

日本語ドキュメントについて

しばらく手が回りそうもありません。
使用方法については、コメントをいただくかソースを見ていただければ幸いです。

PEARへの登録について

現時点で、DraftからProposedにステップアップし、早ければ来週には本登録に向けての投票が行われる予定です。

ダウンロードはこのページからどうぞ。

CheckUser.phpをご利用する際のご注意

CheckUser.phpを使用するにあたっては以下の設定が適切に行われている必要があります。

これらの条件を満たさないと、精度が劇的に低下しますので、ご注意ください。

  1. SPFレコードを設定すること。
    CheckUser.phpを設置したWEBサーバーのアドレスがSPFレコードに登録されている必要があります。
  2. クラス変数$senderには、mb_send_mailの第4引数で指定するメールアドレスと同じものを設定する。
  3. 複数のメールアドレスをループでチェックする際には、以下の点に注意する。
    1. 同じサーバーに対して連続でループしないようにドメイン名の部分等で分散させる。
    2. 同一のホストに対しては最大で500回程度を目安とする。
    3. 少なくとも過去に存在していたアドレスなど、精度の高いメールアドレスのリストを使用する。
  4. 各キャリアが公表している情報を熟読して厳守する。

また、このクラスを利用して得られた結果に対してメルマガを送信する際にも、以下の内容をご理解した上で、システムに反映してください。

  1. SMTPサーバーによっては、存在しないメールアドレスに対してもtrueを返してきます。(例えばYahooメールがこれに該当します。)
    したがって、エラーメールのフィルター処理はこのクラスを使用していても必要です。
  2. アドレスが存在していても、迷惑メール設定などによりエラーメールが返されることも数多くあります。(携帯3キャリアがこれに該当します。)
    したがって、この場合もエラーメールの処理は必要です。

このクラスはエラーメールを減らすという意味では大きな効果を発揮しますが、ゼロにするわけではありません。

メルマガを送信する際のリストの精度が、エラーメールの処理を行っていないことにより低下すると、接続を拒否される等の弊害がありますので、必ずエラーメールの処理をおこなうようお願いいたします。

うーーーん、こうやって考えてみると、Mixiで「あんまり意味ないのでは?」という意見が多いのも納得するような。まあいっか。