Text_Figlet 1.0.0
http://pear.php.net/manual/ja/package.text.text-figlet.php
_ _ _ _ _ ___ _ _ ./ / /_//_// / / /__/_/_/ / / _ _/ / / / / / / // _ /_/ /_/ / / フ /⌒ヽ . /  ̄ ̄ ̄/ ./ / / /  ̄ ̄/ / / __ ヽ  ̄/ // / / / / ̄/ / ̄ / / ./ / / /  ̄ | |/ /) ) / / // /  ̄ / / / / .| | | | | | ̄ \/ /_ / / /_ / / / / \ \ \ \ | | / / ( /  ̄  ̄  ̄  ̄  ̄  ̄  ̄
を作成できるという噂のText_Figlet。
誰だよこんなの登録した奴。あと許可した奴。
まずpearフォルダ内にText_Figletフォルダを作ってその中にTextフォルダ、fontsフォルダを移動します。
Text_Figletには実質的にメソッドが二つしかないのでわざわざラッパクラスを作らなくてもいいといえばいいのですが、手動でインストールしている関係上include_pathが通ってなかったりで面倒なのでとりあえず作ります。
figlet.class.php
| <?php //初期設定 define('PEAR_DIR',$_SERVER['DOCUMENT_ROOT'].'/src/php/pear/'); define('PEAR_FIGLET_FONT_DIR',PEAR_DIR.'Text_Figlet/fonts/'); ini_set('include_path',PEAR_DIR.PATH_SEPARATOR.ini_get('include_path')); require_once(PEAR_DIR.'Text_Figlet/Text/Figlet.php'); class pearFigletModel { //メンバ変数 private $figlet = array(); //コンストラクタ public function __construct(){ $this->figlet = new Text_Figlet(); $a=$this->figlet->loadFont(PEAR_FIGLET_FONT_DIR.'slant.flf'); } //フォントをセット public function loadFont($fontname){ $this->figlet->loadFont(PEAR_FIGLET_FONT_DIR.$fontname); } //表示 public function lineEcho($str){ return $this->figlet->lineEcho($str); } #↓クラスのおわり } |
Text_Figletは、fontsフォルダではなく、include_pathおよびPearのコンフィグファイルで設定した場所からしかフォントを探してくれません。
include_pathを通してもいいのですが、今回は手っ取り早く定数にしています。
例によってPear::isErrorなんかは自力でどうにかしてください。
さっそく実行。
figlet.php
| <?php #初期設定 header("Content-type: text/html; charset=utf-8"); require_once('./figlet.class.php'); $figlet = new pearFigletModel(); $str1='Hello, world'; $str2='STOP!地球温暖化。'; $ret1=$figlet->lineEcho($str1); $ret2=$figlet->lineEcho($str2); print("<pre>"); print($ret1.'<hr>'.$ret2); |
__ __ ____ __ __ / / / /__ / / /___ _ ______ _____/ /___/ / / /_/ / _ \/ / / __ \ | | /| / / __ \/ ___/ / __ / / __ / __/ / / /_/ / | |/ |/ / /_/ / / / / /_/ / /_/ /_/\___/_/_/\____( ) |__/|__/\____/_/ /_/\__,_/ |/ -------------------------------------------------------------------------------- /\// ___ /\// /\// __ /\// /\// _//\/_|_ |_//\/__//\/__/ _)_//\/__//\/_ / __ `/ __// __ `/ __ `/ | |/ __ `/ __ `/ / /_/ /____/ /_/ / /_/ / || / /_/ / /_/ / \__,_/ \__,_/\__,_/| |_/\__,_/\__,_/ (__/ |
う、読めない。
まあ当たり前で、同梱されているフォントには日本語が入っていません。
ではどうすればいいかとフォントファイルであるところのflfを覗いてみると、これが単なるテキストファイルです。
___ @ / |@ / /| |@ / ___ |@ /_/ |_|@ @@ ____ @ / __ )@ / __ |@ / /_/ / @ /_____/ @ @@ ______@ / ____/@ / / @ / /___ @ \____/ @ @@ |
よし、日本語は諦めよう。
Date_Holidays 0.20.1 (alpha)
Date_Holidays_Japan 0.1.0 (alpha)
PHPに限らずプログラマーにとって休日判定は頭の痛い問題のひとつです。
元日や春分秋分などの固定された日はまだいいとして、面倒なのが年によって日付が変更になる休日です。
体育の日を10月10日以外に変更するとかいう愚行のせいで、ある年までは祝日を固定にして、それ以降は毎年祝日を算出する必要があります。
また振替休日のチェックも面倒です。
例によってPearに祝日を扱うクラスがあるので使用してみましょう。
Pear::Date_Holidaysで祝日を扱うことが出来ます。
α版だけあって融通の利かない部分もあるのですが、祝日判定を間違うような致命的なバグは無いようですので使ってみましょう。
こちらはプラグイン的に言語を追加できるのですが、つい先日の2008/08/23に日本語言語ファイルであるところのDate_Holidays_Japanが追加されました。
いつものように解凍した後Date_Holidays_JapanフォルダをDate_Holidaysフォルダに上書きコピーし、フォルダ名をDateと変更します。
holiday.class.php
| <?php //初期設定 define('CURRENT_DIR',getcwd().'/'); define('PEAR_DIR',$_SERVER['DOCUMENT_ROOT'].'/src/php/pear/'); ini_set('include_path',PEAR_DIR.PATH_SEPARATOR.ini_get('include_path')); require_once(PEAR_DIR.'Date/Holidays.php'); class pearHolidayModel { //メンバ変数 private $japan = array(); //コンストラクタ public function __construct($year=null){ $this->japan = &Date_Holidays::factory('Japan',$year,'ja_JP'); $this->japan->addTranslationFile(PEAR_DIR . 'Date/lang/Japan/ja_JP.xml', 'ja_JP'); if (Date_Holidays::isError($this->japan)){die('ファクトリでドライバオブジェクトを作成できませんでした');} } //一年の全祝日取得 public function getHolidays($year){ if(is_null($year)){$year=date('Y');} $this->japan->setYear($year); return $this->japan->getHolidays(); } //祝日ならその情報を取得//祝日でなかったらNULL public function getHolidayForDate($date=null){ if(is_null($date)){$date=date('Y-m-d');} return $this->japan->getHolidayForDate($date); } #↓クラスのおわり } |
Date_Holidaysはfactoryでインスタンスを作成します。
話は変わりますがどうもこのFactoryMethodパターンが理解できないのでどなたか教えてください。
利用側では常にインターフェイスのfactoryを呼んでおいてfactoryで実装クラスをnewすれば、実装クラスのメソッド名を変更してもfactoryを変更するだけで利用側を変える必要が無くて楽ちん、という理解で大丈夫なの?
以下簡単にメソッドを紹介すると、コンストラクタで国名と年を引き渡して祝日を決定します。
setYearで年を変更し、getHolidaysでその年の全祝日を取得します。
isHolidayは引数の日付が祝日かどうかを判定します。
getHolidayで祝日名から情報を取得、getHolidayForDateで日付から情報を取得します。
getHolidaysForDatespanで特定の期間中の祝日を取得、ということも出来るようです。
祝日を追加する関数については、_addStaticHolidays、_addHoliday、_addTranslationForHoliday、_addStringPropertyForHoliday、_addStringPropertiesForHoliday等ありますが違いがよくわからないし_なので触らぬほうがよさげ。
で、肝心のセットする関数がどれかはわかりませんでした。
デフォルトでは記念日名が英語ですが、addTranslationFileでDate_Holidays_Japanに入っていた言語ファイルを追加しています。
これを適用することで祝日名を日本語で取得することが可能になります。
さて呼び出してみましょう。
holiday.php
| <?php #初期設定 header("Content-type: text/html; charset=utf-8"); require_once('./holiday.class.php'); $holiday = new pearHolidayModel(); $day='2007-01-01'; $today=$holiday->getHolidayForDate($day); var_dump($today); |
NULL
あれ?
日付から休日情報を取得するgetHolidayForDateは、引数にはY-m-dを要求するのにそれを見ていません。
setYearでセットした年とgetHolidayForDateに引き渡した年が違う場合、常にNULLが返ってきます。
isHolidayのほうはしっかり年もチェックしているのに。
見比べてみたところ、isHolidayには入っている以下のようなルーチンがgetHolidayForDateには入っていませんでした。
| $compare_year = $date->getYear(); $this_year = $this->getYear(); if ($this_year !== $compare_year) { $this->setYear($compare_year); } |
このままでは不便きわまりないのでholiday.class.phpを変更。
| //休日ならその情報を取得//祝日でなかったらNULL public function getHolidayForDate($date=null){ if(is_null($date)){$date=date('Y-m-d');} $year=explode('-',$date); $this->japan->setYear($year[0]); return $this->japan->getHolidayForDate($date); } |
これで年間の全祝日の取得、および日付から祝日情報を取得できるようになりました。
他にも必要なものがあればその都度追加しましょう。
つうかこのクラスは中身がよくわかんないんだよなあ…
何に使うのかよくわからないメソッドや定数がいっぱいあるし。
また、紹介やAPIを提供しているサイトはあれど、ソースや実装方法を公開しているところは何故かありません。
というわけで手っ取り早く作ってしまいましょう。
必要なのは沢山の画像。
今回は「みーたんの素材屋」のアイコン素材を使わせて頂きました。
簡単な作成方針としては、OKフォルダとNGフォルダから画像をいくつか選び、OKフォルダの画像であれば正しいと判断するというものです。
ただフォルダ名がOKとかNGのままでは正解がバレバレですので、一旦別のフォルダに移し、ファイル名もランダム化してから表示させることにします。
まずcatフォルダとcat_okフォルダ、cat_ngフォルダを作成し、そちらに各画像を配置します。
そして猫認証クラスを作成。
catAuth.class.php
| <?php //初期設定 define('CAT_BASE_DIR',dirname(__FILE__).'/'); define('CAT_DIR',CAT_BASE_DIR.'cat/'); define('CAT_OK_DIR',CAT_BASE_DIR.'cat_ok/'); define('CAT_NG_DIR',CAT_BASE_DIR.'cat_ng/'); class catAuthModel { //メンバ変数 private $options=array(); private $images=array(); //コンストラクタ public function __construct(){ //前回の画像削除 $image_file=scandir(CAT_DIR); foreach($image_file as $val){ if($val=='.' || $val=='..'){continue;} unlink(CAT_DIR.$val); } //デフォルト指定 $this->options = array( 'cat_ok' => '1', 'cat_ng' => '3', ); } //設定を上書きセット public function setOptions($options=array()){ foreach($options as $key=>$val){ $this->options[$key]=$val; } return true; } //出力用画像作成 public function makeCats(){ $this->makeOKCats(); $this->makeNGCats(); shuffle($this->images); return $this->images; } //OK画像作成ルーチン private function makeOKCats(){ //OK画像一覧 $cat_ok_dir=scandir(CAT_OK_DIR); foreach($cat_ok_dir as $key=>$val){ if($val=='.' || $val=='..'){unset($cat_ok_dir[$key]);} } //OK画像選択 if($this->options['cat_ok']==1){ $cat_ok_key[]=array_rand($cat_ok_dir,$this->options['cat_ok']); }else{ $cat_ok_key=array_rand($cat_ok_dir,$this->options['cat_ok']); } //OK画像コピー foreach($cat_ok_key as $val){ $file_rand='ok_'.rand(); copy(CAT_OK_DIR.$cat_ok_dir[$val],CAT_DIR.md5($file_rand.$cat_ok_dir[$val])); $this->images[]=array('flg'=>'1','filename'=>CAT_DIR.md5($file_rand.$cat_ok_dir[$val])); } } //NG画像作成ルーチン private function makeNGCats(){ //NG画像一覧 $cat_ng_dir=scandir(CAT_NG_DIR); foreach($cat_ng_dir as $key=>$val){ if($val=='.' || $val=='..'){unset($cat_ng_dir[$key]);} } //NG画像選択 if($this->options['cat_ng']==1){ $cat_ng_key[]=array_rand($cat_ng_dir,$this->options['cat_ng']); }else{ $cat_ng_key=array_rand($cat_ng_dir,$this->options['cat_ng']); } //NG画像コピー foreach($cat_ng_key as $val){ $file_rand='ng_'.rand(); copy(CAT_NG_DIR.$cat_ng_dir[$val],CAT_DIR.md5($file_rand.$cat_ng_dir[$val])); $this->images[]=array('flg'=>'0','filename'=>CAT_DIR.md5($file_rand.$cat_ng_dir[$val])); } } #↓クラスのおわり } |
設定できるオプションはcat_okとcat_ngのふたつで、それぞれ正解画像の枚数と間違い画像の枚数となります。
makeOKCats()とmakeNGCats()は全く同じことをやってるので引数なりforeachなりで纏められるのは確定的に明らかなのですが、まあいいや。
array_randはエントリが複数の場合配列で返りますが、ひとつの場合は何故か配列ではなくキーだけが返ってくるので一々チェックして配列に揃える作業が必要です。
そのまま使用するには問題となる部分がいくつかあります。
CAT_BASE_DIRは実行環境にあわせて書き換える必要があります。
排他制御をしていないので、同時に複数のアクセスがあった場合どんな挙動をするかわかりません。
cat_okやcat_ngに存在する画像枚数より多い数を指定するとエラーになります。
0以下の値とか文字列とかを入れてもエラーになります。
次にこの猫認証クラスを表示する部分。
cat.php
|
<?php print("<pre>");var_dump($_SESSION['images']); <!--[if IE]><script type="text/javascript"> |
最後にくっついているJavaScriptは、IEではlabelタグでimageタグを括っても機能してくれないというバグの対処用です。
↓のスクリプトをそのまま使わせて頂きました。
http://www.nitoka.net/archives/2006/08/ieimglabel.html
このまま実装すると、ひとつだけチェックを入れて認証失敗した場合、リロードすると1/4の確率で認証成功してしまいます。
裏でワンタイムトークンを発行するなど別の対策が必要となります。
適当にセッションに突っ込めばいいだけですが、まあ所詮個人レベルですのでそこまでする必要もないでしょう。
エラー処理とか細かい部分は端折っていますが、とりあえず猫認証が完成しました。
あとはレイアウトを揃えるなりJavaScriptで飾り立てるなりしてみてください。
CAPTCHAによる画像認証は、既に破られています。
今時デジカメで表情判別が出来るくらいですから、文字の解読も比較的容易なのです。
判別されないよう画像を歪めすぎると、今度は人間にすら読めなくなりますし。
というわけで単純に表示するだけではなく別のロジックを導入してみましょう。
例えばPearにはText_CAPTCHA_Numeralというパッケージがあります。
これは「21+32」みたいな文字列を作成し、その答えを入力させることで認証させるというものですが、……そんなん自力で作ったほうが早いですから!
| <?php #初期設定 session_start(); #表示文字列を決定 $phrase[0]=rand(10,99); $phrase[1]='+'; $phrase[2]=rand(10,99); $_SESSION['phrase']=$phrase[0]+$phrase[2]; $phrase=$phrase[0].$phrase[1].$phrase[2]; var_dump($_SESSION,$phrase); |
たったこれだけのものをPear化ってそれ需要あるんかい。
せっかくなので前回のText_CAPTCHAに同じことをさせてみましょう。
captcha.php
|
<?php #表示 |
見てのとおり、$options['phrase']に表示させたい値を突っ込んだだけです。
表示は「35+84」のような文字列となり、$_SESSION['phrase']の値は119みたいな数値となります。
Text_CAPTCHA_Numeralとか入れるよりよっぽど簡単ではないでしょうか。
Text_CAPTCHA 0.3.1
Text_CAPTCHA_Numeral 1.2.0
Image_Text 0.6.0beta
コメント投稿欄をフリーパスにしておいたらコメントスパムされ放題です。
まあここも認証はありませんが、弱小サイト故に飛んできていません。
めでたしめでたし?
というわけでコメントスパム防止のため、文字を画像として表示し、それを入力させるCAPTCHA認証技術があります。
PHPではPear::Text_CAPTCHAというライブラリが存在するので使ってみましょう。
ただ、Text_CAPTCHAはGDという画像ライブラリを使用します。
phpinfoで使用できることを確認しておきます。
とりあえずText_CAPTCHAをダウンロード、解凍したらいつものようにpear/Text_CAPTCHAフォルダに入れます。
表示用にフォントが必要になるのでとりあえずWindows/Fontsあたりからcour.ttfを拾って↓と同じフォルダに入れておきます。
まずは動作確認のために最低限のモデルクラス。
captcha.class.php
|
<?php |
captcha.php
| <?php require_once('./captcha.class.php'); $rss = new pearCaptchaModel(); |
見つからないエラー。
Warning: Text_CAPTCHA::include_once(Text/CAPTCHA/Driver/Image.php) [function.Text-CAPTCHA-include-once]: failed to open stream: No such file or directory in C:\xampp\htdocs\src\php\pear\Text_CAPTCHA\CAPTCHA.php on line 166
Warning: Text_CAPTCHA::include_once() [function.include]: Failed opening 'Text/CAPTCHA/Driver/Image.php' for inclusion (include_path='C:/xampp/htdocs/src/php/pear/;.;C:\xampp\php\pear\') in C:\xampp\htdocs\src\php\pear\Text_CAPTCHA\CAPTCHA.php on line 166
Fatal error: Class 'Text_CAPTCHA_Driver_Image' not found in C:\xampp\htdocs\src\php\pear\Text_CAPTCHA\CAPTCHA.php on line 169
全体的に、PearからのパスがTextでハードコートされてしまっているので、Text_CAPTCHAフォルダの名前をTextに変更します。
また、Pear::Image_Textを利用しているようなので、これもダウンロードし、pear/Image/Text.phpに配置します。
ついでに作成した画像を入れる用にimagesフォルダを作成しておきます。
エラーが出なくなったら適当にメソッドを追加していきます。
captcha.class.php
|
<?php |
captcha.php
|
<?php |
導入さえうまくいけば、画像認証機能が簡単に作成できます。
画像作成に最低限必要なオプションは$options['output']だけで、他は何も入っていなければCAPTCHAが適当にデフォルト値を使ってくれます。
$options['phrase']で表示する値を設定できます。
固定値を与えると認証の意味が全くありませんが。
画像を作成するばかりだとセッションがくるたびに際限なく画像が増えていくので、コンストラクタで画像削除処理を入れています。
本当はデストラクタでファイル名を指定して削除できるといいのですが、デストラクタで削除するとimgタグで表示も出来なくなってしまいます。
もしかしたらob_end_flushとsleepとかでできるかもしれません。
あとは投稿後の画面で$_SESSION['phrase']と入力値を比較するだけです。
簡単にCAPTCHA認証を設置できました。
めでたし。
RSSをAjaxで動的に表示するようにしてみました。
rssreader.php
|
<?php #チャンネル情報 #内容 |
rssreader.class.php
|
<?php if(isset($this->rss_array[$rss['name']]->channel['dc']['date'])){ |
rsslist.php
| <?php $rsslist[0]=array(); $rsslist[]=array('name'=>'弱小PHPerの憂鬱0.91','url'=>'http://yuubiseiharukana.blog.shinobi.jp/RSS/091/'); $rsslist[]=array('name'=>'弱小PHPerの憂鬱1.0','url'=>'http://yuubiseiharukana.blog.shinobi.jp/RSS/100/'); $rsslist[]=array('name'=>'弱小PHPerの憂鬱2.0','url'=>'http://yuubiseiharukana.blog.shinobi.jp/RSS/200/'); $rsslist[]=array('name'=>'日経トレンディ','url'=>'http://trendy.nikkeibp.co.jp/rss/trendy.rdf'); $rsslist[]=array('name'=>'ITMediaD+','url'=>'http://rss.itmedia.co.jp/rss/2.0/plusd.xml'); $rsslist[]=array('name'=>'ITMediaNews','url'=>'http://rss.itmedia.co.jp/rss/2.0/news_bursts.xml'); $rsslist[]=array('name'=>'CnetJapan','url'=>'http://feed.japan.cnet.com/rss/index.rdf'); |
index.php
|
<?php |
index.js
|
//グローバル変数 |
RSSの取得対象を、ひとつの共通の設定ファイルから動的に呼び出せるようにしました。
取得対象を変更したいときはrsslist.phpを書き換えるだけで済みます。
rssreader.phpおよびrssreader.class.phpは、summaryだっつうてるのに本文全文を突っ込んできたりする行儀の悪いRSSを適当に打ち切るためにmb_substrを追加したり、$rsslistを外部化したり、最大10件で打ち切りにしたりしている他は、前回とほぼ同じです。
そしてフロント側の表示のためにindex.phpおよびindex.jsを追加しました。
単にテキストを取得して見栄えを整えているだけなのでAjaxの活用とかそういうのからは程遠いですが、なんとなくそれなりのものはできたのではないか感はあります。
RSS0.91と1.00と2.00何れのバージョンに対しても、MagpieRSSが差異を吸収してくれるので、ほぼ同じように扱うことが出来ました。
これ以上いじくっても日付順に並べてとかそういう細々した話にしかなりそうにないので、RSSについてはこんなところで終了ということで。
今回使用したバージョン
magpierss-0.72
昔からホームページの更新のチェックに対する需要は高かったのですが、昔の巡回ソフトは更新のあったページを丸ごとダウンロードするものが主流でした。
ファイルの更新日時の取得は比較的容易なのに対し、更新内容の取得は非常に難しかったからです。
2ちゃんねるブラウザを使用している人はわかると思いますが、2ちゃんねるブラウザは2ちゃんねる専用で、他の掲示板を読むことは出来ません。
ほとんど共通のシステムを使用しているしたらばBBS等は読むことが出来る可能性もありますが、基本的には非対応です。
プログラムはアバウトな解析が心底苦手で、人間の目には殆ど同じにしか映らないソースもプログラムには全然別物に見えるので、個別に対応させないと理解してくれないのです。
掲示板だけでもそのような状態ですから、まるで違う構造の各ホームページやブログの内容を取得するなどほとんど夢物語でした。
そのような状況に対処するため、多くのサイトで共通して使えるフォーマットとしてRSSが発展してきました。
このブログもそうですが、多くの企業サイトやブログではRSSが標準で用意されています。
これを読み取るだけで簡単に更新状況がわかるので便利です。
XML形式で書かれているのでパースもさほど難しいものではありません。
というわけでとりあえずRSSを取得してみることにしましょう。
ただ、やはりというかなんというかRSSも拡張の度に互換性が失われており、現在ではRSS0.91、RSS1.0、RSS2.0という三権分立状態になっています。
一々バージョンをチェックしてそれに対応するパーサを用意して、なんてことは面倒なので既存ライブラリで解決してしまいましょう。
PearのRSSリーダにはXML_RSSとXML_Feed_Parserがありますが、PearではないMagpieRSSというライブラリがキャッシュ機能もついていて高性能だったりするのでそちらを使用してみます。
Pearではありませんが、とりあえずPearのフォルダに丸ごと突っ込んでおきます。
色々ファイルがありますが、必要なのはrss_*.incの4ファイルとextlibフォルダだけなのであとは削除しても構いません。
あとキャッシュを使用するためにcacheフォルダを作成しておきます。
とりあえずはMagpieRSSを読み込むクラスを作成。
rssreader.class.php
|
<?php |
引数として$rss=array('name'=>'弱小PHPerの憂鬱',url='http://yuubiseiharukana.blog.shinobi.jp/RSS/200/')
というような配列を渡すことになっています。
何も考えずに作っていたらメンバ変数のキーとして日本語を入れてしまうという相当気持ち悪い実装になってしまったのですが、まあ気にしない。
最初のdefineはMagpieRSSの動作を決定しています。
最初のPEAR_MAGPIERSS_DIRは自作の定数で、MagpieRSSの位置を指定しています。
残りはMagpieRSS内で使用されるもので、MAGPIE_OUTPUT_ENCODINGで文字コードを、MAGPIE_CACHE_DIRでキャッシュファイルの保存先を、MAGPIE_CACHE_AGEでキャッシュの保存期間を秒で指定します。
キャッシュを設定しないとリロードのたびにRSSを呼びにいって負担になるので必ず設定するようにしましょう。
そんな頻繁に更新しても仕方がないのでMAGPIE_CACHE_DIR=3600秒、一時間に設定しました。
定数になってるせいで、サイトAではキャッシュ1時間、サイトBではキャッシュ30分、みたいなことが出来ないのですが、そんな細かいことはどうでもいいや。
日付に関してはRSSのもっとも厄介なところのひとつで、バージョンによってフィールドがあったりなかったり、実装によってフィールドがあったりなかったりでもう大変です。
また、日付形式はW3CDTFと呼ばれる形式で、「2003-12-13T18:30:02Z」というような形になっていてPHPでは理解しにくいです。
parse_w3cdtfはMagpieRSSに用意されている関数で、タイムスタンプ形式にパースしてくれます。
さてとりあえず動作確認してみます。
引数が妙なことになっているのは動的取得を考えてのことですが、とりあえずは固定で。
rssreader.php
|
<?php |
さて実行。
Warning: gmmktime() expects parameter 3 to be long, string given in C:\xampp\htdocs\src\php\pear\MagpieRSS\rss_utils.inc on line 35
rss_utils.incの35行目は
$epoch = gmmktime( $hours, $minutes, $seconds, $month, $day, $year);
日付の生成に失敗しています。
上を見てみると、preg_matchでマッチしているのですが、
$pat = "/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2}))?(?:([-+])(\d{2}):?(\d{2})|(Z))?/";
これ間違ってない?
デバッグ用にprint("<pre>");var_dump( $hours, $minutes, $seconds, $month, $day, $year);を入れてみると、
string(2) "13"
string(2) "02"
string(3) ":45"
string(2) "08"
string(2) "19"
string(4) "2008"
$secondsがおかしい。
正しくは
$pat = "/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})?(?:([-+])(\d{2}):?(\d{2})|(Z))?/";
だと思うのだがこの余計な()は何なのだろう?
とりあえず
array( $match[1], $match[2], $match[3], $match[4], $match[5], $match[6]);
となっているところを
array( $match[1], $match[2], $match[3], $match[4], $match[5], $match[7]);
として応急処置したところ事なきを得た。
とりあえず完成はしたものの、直に呼び出すだけというのも味気ないので次回はAjaxで呼び出してみようかと。
今回のように単発のSQL文を発行する場合にはあまり意味がありませんが、たとえば最初に別のファイルからIDかなにかのリストを取得して、その全リストに対してforeachでSQLを発行したい、などという場合に非常に早くなるそうです。
まあDBのバージョンや対応状況によってはキャッシュが効かないとか逆に遅くなるとか問題点もあるみたいだけれど気にしない。
peardb.class.php
|
<?php |
peardb.php
|
<?php |
まずコンストラクタで一気にSQL文を作成してしまいます。
一般には、
$sql_insert =' INSERT INTO books (name,author,publisher,price,isbn,date,comment,created_at) values ( ? , ? , ? , ? , ? , ? , ? , now() )';
というふうに?を使用します。
各メソッドでは、最初に用意したSQL文を呼び出し、executeメソッドに突っ込みます。
execute($sql_array)を実行すると、$sql_array[0]が最初の?に、$sql_array[1]がその次の?に、というふうに自動的に割り振られた上でSQL文を実行してくれます。
今回はかわりに:nameという風に指定しています。
この場合は連想配列のキーを参照してくれ、:nameの部分には$sql_array['name']を、:authorには$sql_array['author']を、という風に割り当てを行ってくれます。
なんといっても便利なのは、この場合エスケープの処理を書かなくてもMySQL側で自動的にエスケープしてくれることです。
ついうっかり一箇所エスケープし忘れた、等という事がなくなります。
逆にプリペアードステートメント最大の欠点は、可変数の引数を設定出来ないことです。
例えば上記のSELECT文は、検索しないカラムがあった場合、LIKE '%%'というまったく無意味な条件で検索を行うことになります。
nameだけで検索を行いたいのでそれ以外のカラムはSELECT文に入れたくない、という設定が不可能です。
SELECTならまだいいですが、特に面倒なのがUPDATE。
ひとつのカラムだけをUPDATEしたいという場合にも、全てのカラムの内容を用意する必要があります。
priceはかわってないからそのままでいいよ、というときに、',price= ?'の部分だけを削除する、というようなことができないのです。
このような場合はプリペアードステートメントを使用しないselectやupdateメソッドのほうがいいかもしれません。
まあ正直プリペアードステートメントはわかりにくいので無理に使う必要はないかもしれません。
エラーがでた場合の特定も困難ですし。
勿論使用しない場合は忘れずにインジェクション対策を行う必要があります。
今回使用したバージョン。
Pear:MDB2 2.4.1 (stable)
Pear:MDB2_Driver_mysqli 1.4.1 (stable)
昨今何をするにもデータの保存は外せません。
ホームページのデータそれ自身はともかく、ユーザ情報、書き込みログ、買い物や検索のログ、その他諸々のデータはどこかに保存しておかねばなりません。
簡単な一時データならファイルでもいいかもしれませんが、ファイルは一旦全部を読み込まなければ検索すらままならず、また一行だけ書き換えたいというときも全体を書き換えなければならず、そして何と言っても遅いので、あまり使わないほうがよいでしょう。
そこらへんを便利に扱うために登場したのがデータベースです。
検索なら100万件のデータからでも1秒以内で取り出せてしまうという早さ、一行ごとの追加や変更が簡単に行えるという便利さから、Webサービスをはじめあらゆるオンラインシステムに、なくてはならない存在です。
そのかわりSQLというまた面倒な言語を覚えなければいけませんが、まあ単純な追加検索程度なら簡単ですのでさくっといきましょう。
さて、作るといっても一から作ると車輪の再発明です。
自作するよりよっぽど質のいいライブラリが既に出回っているので使わせてもらいましょう。
今回使用するのはPear:MDB2です。
PearはPHPのライブラリで、有志がいろいろと便利なクラスを作成してくれています。
インストールもコマンドラインからpear install MDB2とか書くだけで勝手にインストールまで行ってくれますし、そもそもXAMPPには最初からMDB2が入っているのですが、今回はわざわざ手動でインストールしてみます。
まずPHPのフォルダにpear用フォルダを作成。
C:\xampp\htdocs\src\php\pear\MDB2
次にPearのサイトよりMDB2を取得。
http://pear.php.net/search.php?q=mdb2&in=packages
MDB2とMDB2_Driver_mysqliをダウンロードします。
MDB2はPear:MDB2の本体です。
MDB2_Driver_mysqliはPHP5でのMySQL接続用ファイルです。
PHP4ならMDB2_Driver_mysql、MySQL以外のDBを使用しているなら対応したファイルを取得します。
ダウンロードしたファイルを展開。
package.xmlとかtestsフォルダとかは要りませんので、MDB2フォルダのみを残して以下のようにします。
C:\xampp\htdocs\src\php\pear\MDB2\MDB2.phpおよびMDB2フォルダ
MDB2_Driver_mysqliも同じように展開し、MDB2の同名フォルダに上書きコピーします。
たったこれだけでPear:MDB2を使用する準備が整いました。
使うかどうかわかりませんが、とりあえずMySQLにInnoDBの設定を行っておきます。
このmy.cnfで合ってるのかどうか怪しいのだが、C:\xampp\mysql\bin\my.cnf(おそらくmy:短縮ダイアルと表示されている)のskip-innodbをコメントアウトし、その下のディレクトリのコメントを外します。
my.cnf
| # Comment the following if you are using InnoDB tables #skip-innodb innodb_data_home_dir = "C:/xampp/mysql/" innodb_data_file_path = ibdata1:10M:autoextend innodb_log_group_home_dir = "C:/xampp/mysql/" innodb_log_arch_dir = "C:/xampp/mysql/" |
PhpMyAdminからPHPで使用するデータベースおよびテーブルを作成しておきます。
PHPからでも作成できますが、どうせ最初の一回しか使わないので、配布するパッケージでもない限り手動で行ったほうが手っ取り早いです。
| CREATE DATABASE testdb DEFAULT CHARACTER SET utf8; GRANT ALL ON testdb.* to testuser@localhost IDENTIFIED BY 'testpass'; USE testdb; CREATE TABLE books( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(128) NOT NULL, author VARCHAR(128) NOT NULL, publisher VARCHAR(128) DEFAULT NULL, price int(10) DEFAULT 0, isbn VARCHAR(16) DEFAULT NULL, date DATE DEFAULT NULL, comment TEXT DEFAULT NULL, created_at TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT NULL, deleted_at DATETIME DEFAULT NULL, KEY index_name (name), KEY index_author (author) ) ENGINE=InnoDB,DEFAULT CHARACTER SET utf8; |
SQLの区切りはPHPと同じく;です。
SQLの命令は小文字でも通るのですが、基本的に大文字で書くことになっています。
逆にテーブル名やカラム名は小文字で統一していたほうが安全です。
MySQLは大文字小文字にはアバウトですが、アバウトが故に問題が発生することがあります。
上記のSQLでは、まずCREATE DATABASEで今回使用するデータベースを作成します。
次にGRANT文で、ID:testuser、パスワード:testpassのユーザに、testdbデータベースに対する全アクセス権を与えています。
drop table等のプログラムから行う必要のない権限も与えてしまっているため、公開する場合はきちんとINSERT、UPDATE等の必要最低限に抑えておきましょう。
最後にCREATE TABLE文でテーブルを作成します。
早速Pear::MDB2を呼び出してみます。
今回は直接書いていますが、define等はコンフィグファイルを作成し、そちらにまとめて書くといいです。
まずはPear:MDB2にアクセスするモデル。
peardb.class.php
|
<?php |
コントローラ。
peardb.php
|
<?php |
$db_dsnはDBに接続するための定型文で、各パラメータを配列で指定します。
一行で指定する書き方もありますが、配列のほうがわかりやすく変更も容易なのでお勧めです。
MDB2::singletonで実際に接続します。
一般的にはMDB2::connectが多く使われますが、singletonのほうが都合がいいのでこちらにしましょう。
setFetchModeでSELECT文を発行した場合の受け取り方を決定します。
デフォルトでは数値の配列で返ってきて非常にわかりにくいのですが、MDB2_FETCHMODE_ASSOCと指定することで連想配列のキーにカラム名を入れて返してきてくれるようになります。
query('SET NAMES UTF8');は、これから送信するデータがUTF-8で書かれているという宣言です。
PHP+MySQLの組み合わせは文字コードに纏わる話題に事欠かず、幾つもの自動変換が入っていたり非常に複雑で面倒なのですが、とりあえず最初にこれを書いておけば基本的に大丈夫です。
勿論受け渡しのデータがUTF-8でかかれていなければ文字化けします。
if(PEAR::isError($this->db))はPEARでエラーが出た場合共通して使用できるコードで、SQLを打った後は基本的にこれで成功したかどうかのチェックを行います。
function insertではデータベースに情報をインサートする処理を記入しています。
沢山の引数を並べるとわかりにくくなるので配列にしたほうがいいのですが、今回はそのままです。
quoteはデータベースにデータを突っ込むときには必須の処理で、SQL上問題になりそうな文字をエスケープしてくれます。
PHP側でもmysql_real_escape_stringやpg_escape_string等の関数はありますが、データベース毎にエスケープ方法が違い、それに伴って使用する関数も違うため、使用するデータベースを変更した場合全ての関数を変更しないといけなくなり非常に面倒です。
まあラッパ関数作ればいいのですが、Pear:MDB2ではそこらへんを最初のDSNに従って自動的に行ってくれるので使わない手はありません。
作成したSQL文はexecで実行を行い、返り値には実行の結果変更された行数が返ってきます。
function getAllBooksではbooksテーブルから全ての値を取得しています。
内容が返ってくるクエリはqueryで実行を行います。
返り値は長大なオブジェクトとなっていてなんだかよくわかりません。
fetchAllとすることで、実際のSQL文の返り値を配列として取得できます。
他にもfetchOneやfetchCol等で返り値の一部分だけを取得することができます。
想定どおりに動けば、insertメソッドでテーブルbooksにデータが挿入され、getAllBooksメソッドでそのデータが取得できるはずです。
さっそく実行。
Catchable fatal error: Object of class MDB2_Error could not be converted to string in C:\xampp\htdocs\src\php\etc\peardb.class.php on line 35
お?
35行目は
$sql.=$this->db->quote($name,'text');
です。
MDB2.phpのソースを追ってましょう。
function quote()内でloadModuleに失敗している
↓
loadModule内でMDB2::fileExistsに失敗している。
↓
fileExists内でis_readableに失敗している。
困ったときのvar_dump、MDB2.phpに一行を挿入して何故失敗しているのかパスを表示してみます。
MDB2.php
| var_dump($dir . DIRECTORY_SEPARATOR . $file); if (is_readable($dir . DIRECTORY_SEPARATOR . $file)) { |
string(33) ".\MDB2\Driver\Datatype\mysqli.php"
string(50) "C:\xampp\php\pear\\MDB2\Driver\Datatype\mysqli.php"
string(19) ".\MDB2\Datatype.php"
string(36) "C:\xampp\php\pear\\MDB2\Datatype.php"
なんだその\\は。
peardb.php
| var_dump(ini_get('include_path')); |
string(20) ".;C:\xampp\php\pear\"
XAMPPがインストールされたときに、include_pathの最後に\を付けてしまっていたのが原因でした。
これ見つけるのに丸一日かかるとかもうね…
というかそれ以前にこれ、include_pathしか見てくれていません。
これでは最初から入っていたほうのPear:MDB2を参照してしまいます。
どうしてMDB2フォルダからの相対パスを見てくれていないのかは謎。
仕方ないのでpeardb.class.phpにinclude_pathを追加。
peardb.class.php
| public function __construct(){ ini_set('include_path',PEAR_MDB2_DIR.PATH_SEPARATOR.ini_get('include_path')); $this->db= & MDB2::connect($this->db_dsn,'charset=utf8'); if (PEAR::isError($this->db)) { die($this->db->getMessage()); } $this->db->setFetchMode(MDB2_FETCHMODE_ASSOC); $this->db->query('SET NAMES UTF8'); } |
これでとりあえずDBへの書き込み、および表示が出来るようになりました。
めでたしめでたし。
次回は早くて便利だけど使いにくくてわかりにくいプリペアードステートメントの話でも。
前回あれれ?な状態で終わったのでその続き。
保存できていなかったはずのfile.txtですが、予想外なところで発見されました。
C:\xampp\apache\file.txt
どういうことか。
実はデストラクタの実行されるタイミングでカレントディレクトリが変わってしまうのです。
明示的にunset等を利用して参照を切断すると、その時点でデストラクタが実行されます。
この場合のカレントディレクトリは、通常のメソッドと同じくプログラムのある場所です。
何も書かずに終了した場合、参照が切断されるのはプログラムの終了後です。
従ってデストラクタもプログラムの終了後に実行されます。
この場合のカレントディレクトリは、何故かApacheの作業フォルダになってしまうのです。
試してみましょう。
デストラクタを変更します。
filereader.class.php
| public function __destruct(){ var_dump(getcwd()); } |
呼び出し側
file.php
| <?php require_once('./filereader.class.php'); $fp=new fileReaderClass('file.txt'); unset($fp); ?> |
実行結果。
string(23) "C:\xampp\htdocs\src\php"
file.php
| <?php require_once('./filereader.class.php'); $fp=new fileReaderClass('file.txt'); ?> |
実行結果。
string(15) "C:\xampp\apache"
なんてこった。
unsetのような一見無関係に見える命令で実行結果が変わってしまいます。
こんな不安なクラスを放置しておくわけにはいきません。
常にフルパスを取得し、同じファイルを参照できるように変更します。
filereader.class.php
|
public function __construct($file_name){ |
dirname(__FILE__)で絶対パスを取得して設定することで、以後ファイルには常にフルパスでアクセスすることができます。
unsetの有無によって保存先が変わるようなことももうありません。
ちなみにWindowsのパス区切りは\であり、Linux環境との差異を吸収するDIRECTORY_SEPARATOR定数なんかも用意されていますが、PHPではそこらへんは自動的に変換してくれるのであまり気にしなくてもよかったりします。
このクラスを使用することにより、今後はファイルを通常の変数と同じように扱うことが出来るようになります。
実用とするには、タブや改行のエスケープを追加したりディレクトリトラバーサル対策を考えたりといった改良が必要となりますが、個人ユースならこのくらいでいいでしょう。