忍者ブログ
[PR]
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。



2025/01/18 09:52 |
PHP1-21:KittenAuthを実装してみる
Text_CAPTCHAは味気ないし対抗技術の進歩であんまり意味がなくなってきているということで次に有望視されているのがKittenAuthゲイツ認証なわけですが、Pearライブラリにはまだありません。
また、紹介や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
    #初期設定
    session_start();
    header("Content-type: text/html; charset=utf-8");
   
    #引数があれば認証チェック
    if(isset($_REQUEST) && is_array($_SESSION['images'])){
        $error=0;
        foreach($_SESSION['images'] as $key=>$val){
            if($val['flg']==0 && isset($_REQUEST[$key])){$error=1;break;}
            if($val['flg']==1 && empty($_REQUEST[$key])){$error=1;break;}
        }
        if($error==1){print("認証失敗\n");}else{print("認証成功\n");}
    }
   
    #猫クラス
    require_once('./catAuth.class.php');
    $cat = new catAuthModel();
   
    #オプション変更方法
    //$cat->setOptions(array('cat_ok'=>'2','cat_ng'=>'4'));
   
    #認証用画像取得
    $_SESSION['images']=$cat->makeCats();
   
    #表示
    print('<form method="POST" action="'.htmlspecialchars($_SERVER["PHP_SELF"]).'">');
        foreach($_SESSION['images'] as $key=>$val){
            print('<input type="checkbox" id="'.$key.'" name="'.$key.'" value="1">');
            print('<label for="'.$key.'"><img src="'.$val['filename'].'"></label>'."\n");
        }
    print('<input type="submit" value="送信">');
    print('</form>');

    print("<pre>");var_dump($_SESSION['images']);
?>

<!--[if IE]><script type="text/javascript">
    window.onload=function(){
        var lbs = document.getElementsByTagName('label');
        for(var i=0;i<lbs.length;i++){
            var cimgs = lbs[i].getElementsByTagName('img');
            for(var j=0;j<cimgs.length;j++){
                cimgs[j].formCtrlId = lbs[i].htmlFor;
                cimgs[j].onclick = function(){document.getElementById(this.formCtrlId).click()};
            }
        }
    }
</script><![endif]-->


最後にくっついているJavaScriptは、IEではlabelタグでimageタグを括っても機能してくれないというバグの対処用です。
↓のスクリプトをそのまま使わせて頂きました。
http://www.nitoka.net/archives/2006/08/ieimglabel.html

このまま実装すると、ひとつだけチェックを入れて認証失敗した場合、リロードすると1/4の確率で認証成功してしまいます。
裏でワンタイムトークンを発行するなど別の対策が必要となります。
適当にセッションに突っ込めばいいだけですが、まあ所詮個人レベルですのでそこまでする必要もないでしょう。

エラー処理とか細かい部分は端折っていますが、とりあえず猫認証が完成しました
あとはレイアウトを揃えるなりJavaScriptで飾り立てるなりしてみてください。
 

PR


2008/08/27 14:04 | Comments(0) | TrackBack() | PHP
PHP1-20:Pear::Text_CAPTCHA_Numeralのようなもの
前回の続き。
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    
    #初期設定
    session_start();
    require_once('./captcha.class.php');
    $captcha = new pearCaptchaModel();    
    
    #表示文字列を設定
    $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];
   
    #オプション変更
    $captcha->setOptions(array('phrase'=>$phrase));
   
    #画像作成
    $cap['image']=$captcha->makeImage();
    $cap['phrase']=$captcha->getPhrase();
    $cap['filename']=$captcha->getImageFile();

    #表示
    print('<img src="'.$cap['filename'].'">');
    print("<pre>");var_dump($cap,$_SESSION);die();


見てのとおり、$options['phrase']に表示させたい値を突っ込んだだけです。
表示は「35+84」のような文字列となり、$_SESSION['phrase']の値は119みたいな数値となります。
Text_CAPTCHA_Numeralとか入れるよりよっぽど簡単ではないでしょうか。


2008/08/26 14:17 | Comments(0) | TrackBack() | PHP
PHP1-19:CAPTCHA認証

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
    //初期設定
    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.'Text_CAPTCHA/CAPTCHA.php');

    class pearCaptchaModel {
        //メンバ変数
        private $imageOptions = array();
       
        //コンストラクタ
        public function __construct(){
           
            //出力テキスト設定
            $imageOptions = array(
                'font_size'        => 24,
                'font_path'        => './',
                'font_file'        => 'COUR.TTF',
                'text_color'       => '#DDFF99',
            );
        $c = Text_CAPTCHA::factory('Image');
    }


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
    //初期設定
    define('IMAGE_DIR',getcwd().'/images/');
    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.'Text/CAPTCHA.php');

    class pearCaptchaModel {
        //メンバ変数
        private $options = array();
        private $filename = '';
        private $c = '';
       
        //コンストラクタ
        public function __construct(){
            //前回の画像を削除
            $image_file=scandir(IMAGE_DIR);
            foreach($image_file as $val){
                if($val=='.' || $val=='..'){continue;}
                unlink(IMAGE_DIR.$val);
            }
            //デフォルトの出力テキスト設定
            $imageOptions = array(
                'font_size'        => 24,
                'font_path'        => './',
                'font_file'        => 'COUR.TTF',
                'text_color'       => '#DDFF99',
            );
            //デフォルトの出力画像設定
            $this->options = array(
                'width' => 200,
                'height' => 80,
                'output' => 'png',
                //'phrase' => 'aaaaa',
                'imageOptions' => $imageOptions
            );
        }
       
        //出力テキスト設定を上書きセット
        public function setImageOptions($imageOptions=array()){
            foreach($imageOptions as $key=>$val){
                $this->options['imageOptions'][$key]=$val;
            }
            return true;
        }
       
        //出力画像設定を上書きセット
        public function setOptions($options=array()){
            foreach($options as $key=>$val){
                $this->options[$key]=$val;
            }
            return true;
        }
       
        //イメージを作成
        public function makeImage(){
            $this->c = Text_CAPTCHA::factory('Image');
            $retval = $this->c->init($this->options);
            if (PEAR::isError($retval)) {
                printf('CAPTCHA 作成時にエラー: %s!',
                    $retval->getMessage());
                exit;
            }
            return true;
        }
       
        //作成したフレーズを取得
        public function getPhrase(){
            return $this->c->getPhrase();
        }
       
        //画像を取得
        public function getImageFile(){
            $image = $this->c->getCAPTCHA();
            if (PEAR::isError($image)) {
                echo 'CAPTCHA 作成時にエラー!';
                echo $image->getMessage();
                exit;
            }
            $this->filename=IMAGE_DIR.md5(session_id()).'.'.$this->options['output'];
            file_put_contents($this->filename,$image);
            return $this->filename;
        }
    #↓クラスのおわり
    }


captcha.php 

<?php
    #初期設定
    session_start();
    require_once('./captcha.class.php');
    $captcha = new pearCaptchaModel();
    
    #オプション変更方法
    //$captcha->setOptions(array('output'=>'jpg'));
    //$captcha->setImageOptions(array('text_color'=>'#ff00ff'));
   
    #画像作成
    $cap['image']=$captcha->makeImage();
    $cap['phrase']=$captcha->getPhrase();
    $cap['filename']=$captcha->getImageFile();
   
    #セッションに入れる
    $_SESSION['phrase']=$cap['phrase'];

    #表示
    print('<img src="'.$cap['filename'].'">');
    print("<pre>");var_dump($cap);die();


導入さえうまくいけば、画像認証機能が簡単に作成できます。
画像作成に最低限必要なオプションは$options['output']だけで、他は何も入っていなければCAPTCHAが適当にデフォルト値を使ってくれます。
$options['phrase']で表示する値を設定できます。
固定値を与えると認証の意味が全くありませんが。

画像を作成するばかりだとセッションがくるたびに際限なく画像が増えていくので、コンストラクタで画像削除処理を入れています。
本当はデストラクタでファイル名を指定して削除できるといいのですが、デストラクタで削除するとimgタグで表示も出来なくなってしまいます。
もしかしたらob_end_flushとsleepとかでできるかもしれません。

あとは投稿後の画面で$_SESSION['phrase']と入力値を比較するだけです。
簡単にCAPTCHA認証を設置できました。
めでたし。
 



2008/08/25 14:54 | Comments(0) | TrackBack() | PHP
PHP1-18:AjaxでRSSリーダ
前回の続き。

RSSをAjaxで動的に表示するようにしてみました

rssreader.php

<?php        
        #初期設定
        require_once('./rsslist.php');
        require_once('./rssreader.class.php');
        $rss = new pearMagpieRSSModel();
        $i=0;max=10;

        #引数チェック
        if(empty($_REQUEST['key']))            {die();}
        if(empty($rsslist[$_REQUEST['key']]))    {die();}
        $rsslist=$rsslist[$_REQUEST['key']];

        #RSS取得
        $ret=$rss->setUrl($rsslist);
        if(!$ret){die('rss->setUrl');}

        #チャンネル情報
        $channel=$rss->getRssChannel($rsslist);

        #内容
        $data=$rss->getAllItem($rsslist);
        //表示
        foreach($data as $key=>$val){
            if($i++>$max){die();}
            $val['summary']=mb_substr(strip_tags($val['summary']),0,500);
            print('<div class="title"><a href="'.htmlspecialchars($val['link']).'" target="_blank">'.htmlspecialchars($val['title']).'</a></div>');
            print('<div class="summary">'.htmlspecialchars($val['summary']).'</div>');
        }



rssreader.class.php

<?php
    //初期設定
    define('PEAR_MAGPIERSS_DIR',$_SERVER['DOCUMENT_ROOT'].'/src/php/pear/MagpieRSS/');
    define('MAGPIE_OUTPUT_ENCODING', 'UTF-8');
    define('MAGPIE_CACHE_DIR',PEAR_MAGPIERSS_DIR.'cache/');
    define('MAGPIE_CACHE_AGE',3600);
    require_once(PEAR_MAGPIERSS_DIR.'rss_fetch.inc');

    class pearMagpieRSSModel {

        //メンバ変数
        private $rss_array=array();
        
        //コンストラクタ
        public function __construct(){}
        
        //RSS取得
        public function setUrl($rss){
            $this->rss_array[$rss['name']] = fetch_rss($rss['url']);

                if(isset($this->rss_array[$rss['name']]->channel['dc']['date'])){
                    $this->rss_array[$rss['name']]->channel['dc']['timestamp']=parse_w3cdtf($this->rss_array[$rss['name']]->channel['dc']['date']);
                }else{
                    $this->rss_array[$rss['name']]->channel['dc']['timestamp']='';
                }
            return true;
        }

        //RSS情報
        public function getRssChannel($rss){
            if($this->rss_array[$rss['name']]){
                return $this->rss_array[$rss['name']]->channel;
            }else{
                return false;
            }
        }
        
        //各記事の内容
        public function getAllItem($rss){
            if($this->rss_array[$rss['name']]){
                foreach($this->rss_array[$rss['name']]->items as $key=>$val){
                    $ret[$key]['title']            =$val['title'];
                    $ret[$key]['link']            =$val['link'];
                    $ret[$key]['summary']        =$val['summary'];
                    $ret[$key]['desc']            =$val['description'];
                    if(isset($val['date_timestamp'])){$ret[$key]['date_timestamp']=$val['date_timestamp'];}else{$ret[$key]['date_timestamp']='';}
                }
                return $ret;                
            }else{
                return false;
            }
        }

    #↓クラスのおわり
    }


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
    require_once('rsslist.php');
?>

<html xmlns="http://www.w3.org/1999/xhtml">

    <head>
    <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <META HTTP-EQUIV="Content-Style-Type" CONTENT="text/css">
    <title>RSS</title>
    <STYLE TYPE="text/css">
        <!--
        .title    {margin-bottom:5px;margin-top:10px;}
        .summary{width:90%;margin-left:10px;padding:5px;background-color:#faffff;border:1px solid;}
        -->
    </STYLE>
    <script type="text/javascript" src="./index.js"></script>
    </head>

    <body>
            <div id="rssSelect">
                <form name="rssSelectForm">
                    <select name="rssSelectOpt" id="rssSelectId" onChange="rssLoad();">
                        <option value="index">RSSを選択</option>
                        <?php
                            foreach($rsslist as $key=>$val){
                                if($key==0){continue;}
                                print('<option value="'.$key.'">'.$val['name'].'</option>');
                            }
                        ?>
                    </select>
                </form>
            </div>            
            <div id="rssPrint"></div>
    </body>
</html>


index.js

    //グローバル変数
    var xmlhttp;
    var oldquery;

    /*----------------------------------------------------------*/
    /* HTTPリクエスト作成関数 */
    function rssLoad(){        
        
        var rssSelectOpt=document.rssSelectForm.rssSelectOpt.value;
        var rssPrint=document.getElementById("rssPrint");

        //0番目なら終了
        if(rssSelectOpt=="index"){rssPrint.style.display = "none";return;}
        //既にXMLHttpRequestがあったら破棄
        if (xmlhttp){xmlhttp.abort();}

        xmlhttp = createXmlHttpRequest();

        //検索文字列が空なら何も出力しない
        if (rssSelectOpt==""){
            rssPrint.style.display = "none";

        //検索文字が更新されたら
        } else if (oldquery != rssSelectOpt) {
            //クエリを作成
            xmlhttp.open("GET", "rssreader.php?key="+rssSelectOpt, true);
            //クエリの返答が帰ってきたら表示を更新する無名関数
            xmlhttp.onreadystatechange = function() {
                //正しいレスポンスが来た
                if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
                    //返り値が入っていれば
                    if(xmlhttp.responseText){
                        rssPrint.style.display = "block";
                        rssPrint.innerHTML = xmlhttp.responseText;
                    //正しく終了していなければ
                    }else{
                        rssPrint.style.display = "block";
                        rssPrint.innerHTML = "ファイルが見つかりません"+xmlhttp.status;
                    }
                }
            }
                //クエリ送信
            xmlhttp.send(null);
        }
            //検索文字のチェック用
        oldquery = query;
    }

    /*==========================================================*/
    //サブルーチン

        /*----------------------------------------------------------*/
        /* HTTPリクエスト作成関数 */
        function createXmlHttpRequest() {
            var xmlhttp = false;
                if( window.XMLHttpRequest) {
                    xmlhttp = new XMLHttpRequest();
                } else if(window.ActiveXObject) {
                    try {
                        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
                    } catch(e) {
                        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
                    }
                }
            return xmlhttp;
        }



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についてはこんなところで終了ということで。



2008/08/21 19:28 | Comments(0) | TrackBack() | PHP
PHP1-17:RSSリーダを作ってみる

今回使用したバージョン
magpierss-0.72

昔からホームページの更新のチェックに対する需要は高かったのですが、昔の巡回ソフトは更新のあったページを丸ごとダウンロードするものが主流でした。
ファイルの更新日時の取得は比較的容易なのに対し、更新内容の取得は非常に難しかったからです。
2ちゃんねるブラウザを使用している人はわかると思いますが、2ちゃんねるブラウザは2ちゃんねる専用で、他の掲示板を読むことは出来ません。
ほとんど共通のシステムを使用しているしたらばBBS等は読むことが出来る可能性もありますが、基本的には非対応です。
プログラムはアバウトな解析が心底苦手で、人間の目には殆ど同じにしか映らないソースもプログラムには全然別物に見えるので、個別に対応させないと理解してくれないのです。
掲示板だけでもそのような状態ですから、まるで違う構造の各ホームページやブログの内容を取得するなどほとんど夢物語でした。

そのような状況に対処するため、多くのサイトで共通して使えるフォーマットとしてRSSが発展してきました。
このブログもそうですが、多くの企業サイトやブログではRSSが標準で用意されています。
これを読み取るだけで簡単に更新状況がわかるので便利です。
XML形式で書かれているのでパースもさほど難しいものではありません。
というわけでとりあえずRSSを取得してみることにしましょう。

ただ、やはりというかなんというかRSSも拡張の度に互換性が失われており、現在ではRSS0.91、RSS1.0、RSS2.0という三権分立状態になっています。
一々バージョンをチェックしてそれに対応するパーサを用意して、なんてことは面倒なので既存ライブラリで解決してしまいましょう。

PearのRSSリーダにはXML_RSSXML_Feed_Parserがありますが、PearではないMagpieRSSというライブラリがキャッシュ機能もついていて高性能だったりするのでそちらを使用してみます。

Pearではありませんが、とりあえずPearのフォルダに丸ごと突っ込んでおきます。
色々ファイルがありますが、必要なのはrss_*.incの4ファイルとextlibフォルダだけなのであとは削除しても構いません。
あとキャッシュを使用するためにcacheフォルダを作成しておきます。

とりあえずはMagpieRSSを読み込むクラスを作成。

rssreader.class.php 

<?php

    //初期設定
    define('PEAR_MAGPIERSS_DIR',$_SERVER['DOCUMENT_ROOT'].'/src/php/pear/MagpieRSS/');
    define('MAGPIE_OUTPUT_ENCODING', 'UTF-8');
    define('MAGPIE_CACHE_DIR',PEAR_MAGPIERSS_DIR.'cache/');
    define('MAGPIE_CACHE_AGE',3600);

    require_once(PEAR_MAGPIERSS_DIR.'rss_fetch.inc');

class pearMagpieRSSModel {

    //メンバ変数
    private $rss_array=array();
    
    //コンストラクタ
    public function __construct(){}
   
    //RSS取得
    public function setUrl($rss){
        $this->rss_array[$rss['name']] = fetch_rss($rss['url']);
            if(isset($this->rss_array[$rss['name']]->channel['dc']['date'])){
                $this->rss_array[$rss['name']]->channel['dc']['timestamp']=parse_w3cdtf($this->rss_array[$rss['name']]->channel['dc']['date']);
            }else{
                $this->rss_array[$rss['name']]->channel['dc']['timestamp']='';
            }
        return true;
    }

    //RSS情報
    public function getRssChannel($rss){
        if($this->rss_array[$rss['name']]){
            return $this->rss_array[$rss['name']]->channel;
        }else{
            return false;
        }
    }
    
    //各記事の内容
    public function getAllItem($rss){
        if($this->rss_array[$rss['name']]){
            foreach($this->rss_array[$rss['name']]->items as $key=>$val){
                $ret[$key]['title']            =$val['title'];
                $ret[$key]['link']            =$val['link'];
                $ret[$key]['summary']        =$val['summary'];
                $ret[$key]['desc']            =$val['description'];
                if(isset($val['date_timestamp'])){$ret[$key]['date_timestamp']=$val['date_timestamp'];}
            }
            return $ret;            
        }else{
            return false;
        }
    }

#↓クラスのおわり
}


引数として$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

    #テスト用に固定値を与える
    $_REQUEST['key']=1;
    $rsslist[1]=array('name'=>'日経トレンディ','url'=>'http://trendy.nikkeibp.co.jp/rss/trendy.rdf');
    
    #初期設定
    //require_once('./rsslist.php');
    require_once('./rssreader.class.php');
    $rss = new pearMagpieRSSModel();

    #引数チェック
    if(empty($_REQUEST['key']))                {die();}
    if(empty($rsslist[$_REQUEST['key']]))    {die();}
    $rsslist=$rsslist[$_REQUEST['key']];

    #RSSをセット
    $ret=$rss->setUrl($rsslist);
    if(!$ret){die('rss->setUrl');}

    #チャンネル情報
    $channel=$rss->getRssChannel($rsslist);

    #内容
    $data=$rss->getAllItem($rsslist);

    #表示
    foreach($data as $key=>$val){
        print('<div class="title"><a href="'.$val['link'].'" target="_blank">'.$val['title'].'</a></div>');
        print('<div class="summary">'.$val['summary'].'</div>');
    }


さて実行。

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で呼び出してみようかと。



2008/08/20 19:02 | Comments(0) | TrackBack() | PHP
PHP1-16:Pear::MDB2/プリペアードステートメント
プリペアードステートメントとは、まずSQL文の雛形を用意しておいて、実行時には動的に変更する部分だけを送り込むというSQLの実行方法です。
今回のように単発のSQL文を発行する場合にはあまり意味がありませんが、たとえば最初に別のファイルからIDかなにかのリストを取得して、その全リストに対してforeachでSQLを発行したい、などという場合に非常に早くなるそうです。
まあDBのバージョンや対応状況によってはキャッシュが効かないとか逆に遅くなるとか問題点もあるみたいだけれど気にしない。

peardb.class.php 

<?php
    define('PEAR_MDB2_DIR',$_SERVER['DOCUMENT_ROOT'].'/src/php/pear/MDB2/');
    require_once(PEAR_MDB2_DIR.'MDB2.php');

    class pearMDB2Model {
        
        //メンバ変数
        protected $db_dsn = array(
            'phptype'  => 'mysqli',
            'username' => 'testuser',
            'password' => 'testpass',
            'hostspec' => 'localhost',
            'database' => 'testdb'
            );
        protected $db='';
        protected $res='';
        protected $sql_insert='';
        protected $sql_update='';
        protected $sql_select='';

        //コンストラクタ
        public function __construct(){
            //接続
                ini_set('include_path',PEAR_MDB2_DIR.PATH_SEPARATOR.ini_get('include_path'));
                $this->db= & MDB2::singleton($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');          

            //insert文の用意
                $sql_insert =' INSERT INTO books ';
                $sql_insert.=' (name,author,publisher,price,isbn,date,comment,created_at) ';
                $sql_insert.=' values ';
                $sql_insert.=' ( :name , :author , :publisher , :price , :isbn , :date , :comment , now() )';
                $this->sql_insert=$this->db->prepare($sql_insert);
                
            //update文の用意
                $sql_update ='UPDATE books SET ';
                $sql_update.='  name= :name ';
                $sql_update.=', author= :author ';
                $sql_update.=', publisher= :publisher ';
                $sql_update.=', price= :price ';
                $sql_update.=', isbn= :isbn ';
                $sql_update.=', date= :date ';
                $sql_update.=', comment=:comment ';
                $sql_update.=', updated_at=now() ';
                $sql_update.=' where id= :id ';
                $this->sql_update=$this->db->prepare($sql_update);
                
            //select文の用意
                $sql_select =' SELECT * FROM books ';
                $sql_select.=' WHERE ';
                $sql_select.=' name LIKE :name ';
                $sql_select.='and author LIKE :author ';
                $sql_select.='and publisher LIKE :publisher ';
                $sql_select.='and price LIKE :price ';
                $sql_select.='and isbn LIKE :isbn ';
                $sql_select.='and date LIKE :date ';
                $sql_select.='and comment LIKE :comment ';
                $this->sql_select=$this->db->prepare($sql_select);
        }

        //インサート
        public function insert_prepare($sql_array){
            $res=$this->sql_insert->execute($sql_array);
            if (PEAR::isError($this->res)){
                return false;
            }else{
                return $this->db->lastInsertId();
            }
        }

        //セレクト
        public function select_prepare($sql_array){
            if(isset($sql_array['name']))        {$sql_array['name']='%'.$sql_array['name'].'%';                }else{$sql_array['name']='%%';}
            if(isset($sql_array['author']))        {$sql_array['author']='%'.$sql_array['author'].'%';            }else{$sql_array['author']='%%';}
            if(isset($sql_array['publisher']))    {$sql_array['publisher']='%'.$sql_array['publisher'].'%';    }else{$sql_array['publisher']='%%';}
            if(isset($sql_array['price']))        {$sql_array['price']='%'.$sql_array['price'].'%';            }else{$sql_array['price']='%%';}
            if(isset($sql_array['isbn']))        {$sql_array['isbn']='%'.$sql_array['isbn'].'%';                }else{$sql_array['isbn']='%%';}
            if(isset($sql_array['comment']))    {$sql_array['comment']='%'.$sql_array['comment'].'%';        }else{$sql_array['comment']='%%';}

            $res=$this->sql_select->execute($sql_array);
            $res=$res->fetchAll();
            if (PEAR::isError($this->res)){
                return false;
            }else{
                return $res;
            }
        }

        //アップデート
        public function update_prepare($id,$sql_array){
            $sql_array['id']=$id;
            $res=$this->sql_update->execute($sql_array);
            if (PEAR::isError($this->res)){
                return false;
            }else{
                return $this->db->lastInsertId();
            }
        }       

        //プリペアードステートメント使わないセレクト
        public function select($sql_array){
            $sql =' select * from books ';
            $sql.=' where deleted_at is null ';
            foreach($sql_array as $key=>$val){
                $sql.=" and ".$key." like ".$this->db->quote('%'.$val.'%','text');
            }
            $res=$this->db->query($sql);
            $res=$res->fetchAll();
            if (PEAR::isError($this->res)){
                return false;
            }else{
                return $res;
            }
        }

        //プリペアードステートメント使わないアップデート
        public function update($id,$sql_array){
            $sql =' update books set update_date=now(), ';
            foreach($sql_array as $key=>$val){
                $sql.=$key."=".$this->db->quote($val,'text')." , ";
            }
            $sql.=' where id='.$this->db->quote($id,'text');            
            $res=$this->db->query($sql);
            $res=$res->fetchAll();
            if (PEAR::isError($this->res)){
                return false;
            }else{
                return $res;
            }
        }

#↓クラスのおわり
}


peardb.php

<?php
    require_once('./peardb.class.php');
    $db = new pearMDB2Model();

    $ins_array['name']            ='恋空〈上〉';
    $ins_array['author']        ='美嘉';
    $ins_array['publisher']        ='スターツ出版';
    $ins_array['price']            ='1050';
    $ins_array['isbn']            ='978-4883810451';
    $ins_array['date']            ='2006-10-01';
    $ins_array['comment']        ='スイーツ(笑)';
    
    $ret1=$db->insert_prepare($ins_array);
    $ret2=$db->select_prepare($ins_array);
    print("<pre>");var_dump($ret1,$ret2);


まずコンストラクタで一気に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メソッドのほうがいいかもしれません。


まあ正直プリペアードステートメントはわかりにくいので無理に使う必要はないかもしれません。
エラーがでた場合の特定も困難ですし。
勿論使用しない場合は忘れずにインジェクション対策を行う必要があります。



2008/08/18 20:38 | Comments(1) | TrackBack() | PHP
PHP1-15:データベースを使う

今回使用したバージョン。
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

    define('PEAR_MDB2_DIR',$_SERVER['DOCUMENT_ROOT'].'/src/php/pear/MDB2/');
    require_once(PEAR_MDB2_DIR.'MDB2.php');

    class pearMDB2Model {
        //メンバ変数
        protected $db_dsn = array(
            'phptype'  => 'mysqli',
            'username' => 'testuser',
            'password' => 'testpass',
            'hostspec' => 'localhost',
            'database' => 'testdb'
        );
        protected $db='';
        protected $res='';

        //接続
        public function __construct(){
            $this->db=MDB2::singleton($this->db_dsn);
            if (PEAR::isError($this->db)) {
                die($this->db->getMessage());
            }
            $this->db->query('SET NAMES UTF8');
            $this->db->setFetchMode(MDB2_FETCHMODE_ASSOC);
        }

        //インサート
        public function insert($name,$author,$publisher,$price,$comment,$isbn,$date){
            $sql =' INSERT INTO books ';
            $sql.=' (name,author,publisher,price,isbn,date,comment,created_at) ';
            $sql.=' values ';
            $sql.=' ( ';
            $sql.=$this->db->quote($name,'text');
            $sql.=' ,'.$this->db->quote($author,'text');
            $sql.=' ,'.$this->db->quote($publisher,'text');
            $sql.=' ,'.$this->db->quote($price,'integer');
            $sql.=' ,'.$this->db->quote($isbn,'text');
            $sql.=' ,'.$this->db->quote($date,'text');
            $sql.=' ,'.$this->db->quote($comment,'text');
            $sql.=' ,now() ';
            $sql.=' )';

            $this->res=$this->db->exec($sql);
            if (PEAR::isError($this->res) || $this->res==0){
                return false;
            }else{
                return true;
            }
        }

    //読み取り
        public function getAllBooks(){
            $sql =' select * from books ';
            $sql.=' where deleted_at is null ';

            $ret=$this->db->query($sql);
            $ret=$ret->fetchAll();

            if (PEAR::isError($ret)){
                return false;
            }else{
                return $ret;
            }
        }
    

    #↓クラスのおわり
    }


コントローラ。

 peardb.php

<?php
    require_once('./peardb.class.php');
    $db = new pearMDB2Model();

    $name='恋空〈上〉';
    $author='美嘉';
    $publisher='スターツ出版';
    $price='1050';
    $isbn='978-4883810451';
    $date='2006/10/01';
    $comment='スイーツ(笑)';
    
    $db->insert($name,$author,$publisher,$price,$comment,$isbn,$date);

    $a=$db->getAllBooks();
    print("<pre>");var_dump($a);


$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への書き込み、および表示が出来るようになりました。
めでたしめでたし。

次回は早くて便利だけど使いにくくてわかりにくいプリペアードステートメントの話でも。



2008/08/15 16:16 | Comments(2) | TrackBack() | PHP
PHP1-14:ファイルに保存クラスの追記

前回あれれ?な状態で終わったのでその続き。


保存できていなかったはずの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){

            $file_name=dirname(__FILE__)."/".$file_name;

            $tmp=file($file_name,FILE_SKIP_EMPTY_LINES);
            
            if($tmp===false){
                  return false;
            }
            
            foreach($tmp as $tmp_loop){
                  $tmp_ex=explode("\t",$tmp_loop);
                  $tmp_array[$tmp_ex[0]]=trim($tmp_ex[1]);
            }

            $this->file_name=$file_name;
            $this->contents=$tmp_array;
            return true;
      }


dirname(__FILE__)で絶対パスを取得して設定することで、以後ファイルには常にフルパスでアクセスすることができます。
unsetの有無によって保存先が変わるようなことももうありません。

ちなみにWindowsのパス区切りは\であり、Linux環境との差異を吸収するDIRECTORY_SEPARATOR定数なんかも用意されていますが、PHPではそこらへんは自動的に変換してくれるのであまり気にしなくてもよかったりします。

このクラスを使用することにより、今後はファイルを通常の変数と同じように扱うことが出来るようになります。
実用とするには、タブや改行のエスケープを追加したりディレクトリトラバーサル対策を考えたりといった改良が必要となりますが、個人ユースならこのくらいでいいでしょう。



2008/08/14 11:21 | Comments(0) | TrackBack() | PHP
PHP1-13:クラスを作ってみる:ファイルに保存

とりあえず役に立つクラスを作成してみましょう。

PHPでは変数へのデータ保存は非常に簡単に行えます。
多くの言語では面倒な、セッションへのデータの保存や取り出しも無闇に簡単です。
しかしファイルに対する操作は多少の手間が必要です。
ファイルに保存するクラスを作って簡単に読み書きできるようにしてしまいましょう。

尤もこれが実用的かというと、すぐDBに移行するので微妙なところですが。

filereader.class.php 

class fileReaderClass{

#------------------------------------------------------------
#内部変数
    protected $file_name=NULL;
    protected $contents=NULL;

#------------------------------------------------------------
#コンストラクタ
    public function __construct($file_name){

        $tmp=file($file_name,FILE_SKIP_EMPTY_LINES);
        if($tmp===false){
            return false;
        }
        
        foreach($tmp as $tmp_loop){
            $tmp_ex=explode("\t",$tmp_loop,2);
            $tmp_array[$tmp_ex[0]]=trim($tmp_ex[1]);
        }

        $this->file_name=$file_name;
        $this->contents=$tmp_array;
        return true;
    }

#------------------------------------------------------------
#デストラクタ
    public function __destruct(){

        $tmp='';
        foreach($this->contents as $key=>$val){
            $tmp.=$key."\t".$val."\n";
        }
        file_put_contents($this->file_name,$tmp);
    }

#------------------------------------------------------------
#変数読み出し
    public function getParam($key){
        
        if(isset($this->contents[$key])){
            return $this->contents[$key];
        }else{
            return false;
        }
    }

#------------------------------------------------------------
#変数書き込み
    public function setParam($key,$val){
        $this->contents[$key]=$val;
        return true;
    }

#↓クラスのおわり
}


まず__constructですが、これはコンストラクタと呼ばれ、newでクラスをインスタンス化した時点で自動的に実行されます。
newするときにファイル名を与えると、そのファイルの中身が連想配列に読み込まれます。

その後、getParamおよびsetParamでファイルの読み書きを行います。
最後にデストラクタですが、これはクラスへの参照が無くなったときに自動的に実行されます。
「クラスを最後に使用したとき」ではなく「参照が無くなったとき」ですので、順番を厳密に管理するときは注意が必要です。

使用する側では以下のようにします。

file.php 

    require_once('./filereader.class.php');
    $fp=new fileReaderClass('sample.txt');
    
    $a=$fp->getParam('b');
    $fp->setParam('d','sample2');

    print($a);


sample.txt(空白ではなく、タブ区切りです) 

a    sample1
b    sample2
c    sample3


$fp=new fileReaderClass('a.txt');
の行を実行した時点でコンストラクタが実行され、sample.txtが読み込まれ、クラス内のメンバ変数$contentsに入れられます。
次のgetParamでは、$contents['b']の値、すなわち'sample2'を取得します。
setParamで$contentsに('d'=>'sample4')という配列を追加しています。
 
unset($fp)でこのクラスへの参照を削除すると、同時にデストラクタが実行され、現在の$contentsの値をファイルに書き込みます。

これで一通りファイルを読み書きするクラスが完成しました。
タブを含む変数名、改行を含む変数内容は使用できませんが、とりあえずはいいでしょう。
さて、書き込み後のsample.txtを見てみます。
 

sample.txt

a    sample1
b    sample2
c    sample3


 ってあれ?
デストラクタをちょこっと変更して確認してみます。
 

    public function __destruct(){
        $tmp='';
        foreach($this->contents as $key=>$val){
            $tmp.=$key."\t".$val."\n";
        }
        file_put_contents($this->file_name,$tmp);
        var_dump($this->file_name);
        var_dump(file_get_contents($this->file_name));
    }


string(8) "file.txt"
string(40) "a sample1 b sample2 c sample3 d sample4 "

あれれ?
きちんと取得できてますね。


    public function __destruct(){
        $tmp='';
        foreach($this->contents as $key=>$val){
            $tmp.=$key."\t".$val."\n";
        }
        file_put_contents('C:\xampp\htdocs\file.txt',$tmp);
    }


 file.txt

a    sample1
b    sample2
c    sample3
d    sample4


 フルパスを与えると保存できてます。
$this->file_nameって何処なんだ?
はて??

今度は呼び出し側を変更してみます。

file.php

    require_once('./filereader.class.php');
    $fp=new fileReaderClass('sample.txt');
    
    $a=$fp->getParam('b');
    $fp->setParam('d','sample2');

    unset($fp);
    print($a);


 file.txt

a    sample1
b    sample2
c    sample3
d    sample4


ファイルポインタを明示的に破棄した場合、相対パスでもきちんと保存されるようです。
どういうことなのでしょう?


2008/08/11 14:01 | Comments(1) | TrackBack() | PHP
PHP1-12:array_multisort

PHPには多くのソート関数がありますが、その中でも便利でありながら使い方がわかりにくいもののひとつであるarray_multisortとusortについて調べてみます。


http://jp2.php.net/array_multisort
マニュアルを見てみても
「複数の配列を一度に、 または、多次元の配列をその次元の一つでソートする際に使用可能です。 この関数は、ソートの際にキーの相関を維持します。」
としか書かれておらず、なんのこったという感じです。

日本語訳すると、引数の前のほうの配列をソートして、そのついでに引数の後ろのほうの配列も同じ順番でソートする、ということになります。
 

$a=array(3,5,1,4,2);
$b=array('a','b','c','d','e');
array_multisort($a,$b);

と書いた場合、
まず$aが普通にソートされて
$a=array(1,2,3,4,5);
となります。
それと同時に、$bが$aと同じ順番でソートされ、
$b=array('c','e','a','d','b');
となります。

さて、この関数をわかりにくくしている要因のひとつが引数の与え方です。
array_multisort($a,SORT_DESC,$b);
と書くと、$aが降順でソートされた上で$bがそれに従ってソートされます。

 $a1=array(2,2,1,1,1);
 $a2=array(1,2,5,4,3);
 $b=array('a','b','c','d','e');
 array_multisort($a1,$a2,$b);

とすると、まず$a1がソートされ、$a2と$bがその順番に従ってソートされます。
更にその後、$a1の順番を壊さない範囲で$a2がソートされ、$bも$a2に従ってソートされます。

最終的に
$a1=array(1,1,1,2,2);
$a2=array(3,4,5,1,2);
$b=array('e','d','c','a','b');

となります。

 更に引数を増やして
array_multisort($a1,$a2,SORT_STRING,$a3,$b);
などと書いても動作してしまうのが、便利さと同時にわかりにくさを感じる一因となっていることでしょう。

さて、array_multisortで便利なのが、多重配列のソートです。
以下のようなよくある商品データがあったとして、これを価格順にソートしたい場合。

$product=array(
    array('name'=>'みかん','price'=>'60'),
    array('name'=>'りんご','price'=>'180'),
    array('name'=>'バナナ','price'=>'48'),
    array('name'=>'いちご','price'=>'590'),
    array('name'=>'ぶどう','price'=>'298'),
    array('name'=>'ドリアン','price'=>'5000')
);


必要なのは、ソート順を決定する配列と、ソート自体を行う配列です。
後者は出来てますので、前者を作ります。
 

foreach($product as $tmp){$sort_array[]=$tmp['price'];}

これでソート用の配列$sort_array=array('60','180','48','590','298','5000')が出来ました。
  

array_multisort($sort_array,$product);

$sort_array=Array(
    [0] => 48
    [1] => 60
    [2] => 180
    [3] => 298
    [4] => 590
    [5] => 5000
)

$product=Array(
    [0]=>Array([name]=>バナナ[price]=>48)
    [1]=>Array([name]=>みかん[price]=>60)
    [2]=>Array([name]=>りんご[price]=>180)
    [3]=>Array([name]=>ぶどう[price]=>298)
    [4]=>Array([name]=>いちご[price]=>590)
    [5]=>Array([name]=>ドリアン[price]=>5000)
)


めでたしめでたし。

さて、array_multisortにはソートオプションが用意されており、SORT_DESCで降順、SORT_STRINGで文字列順、SORT_NUMERICで数値順とソート順を変更することが出来ます。
しかし定義された順ではなく任意の順番でソートしたい場合、どうすればよいでしょう。

usortという、ユーザでソートオプション的なものを設定できる関数が用意されています。
二つの引数をとり、前のほうにしたい場合は-1を返し、後ろに並べるときは1を返す関数を作ってあげます。
とりあえずやってみましょう。

usort($product,'compare');

function compare($a,$b){
    if($a['price']==48){
        return -1;
    }else if($b['price']==48){
        return 1;
    }else{
        return 0;
    }
}

Array (
    [0] => Array ( [name] => バナナ [price] => 48 )
    [1] => Array ( [name] => ドリアン [price] => 5000 )
    [2] => Array ( [name] => ぶどう [price] => 298 )
    [3] => Array ( [name] => いちご [price] => 590 )
    [4] => Array ( [name] => みかん [price] => 60 )
    [5] => Array ( [name] => りんご [price] => 180 )
)


バナナを一番上に持って行きたかっただけなのですが、うまくいっていません。
バナナ以外の部分が出鱈目なことになってしまっています。
一体どういう基準でソートされたんだこれ?

どのサイトを見ても
return ($a['price'] < $b['price']) ? -1 : (($a['price'] > $b['price']) ? 1 : 0);
といったわざわざ定義する必要ないじゃん的ソート関数しか置いてないんですよね(ちなみに↑は昇順)

はてさて。
 



2008/07/30 21:03 | Comments(0) | TrackBack() | PHP

<<前のページ | HOME | 次のページ>>
忍者ブログ[PR]