WHAT'S NEW?
Loading...

PHP とjQueryライブラリ「jqPlot」で綺麗なグラフを描画する【9/10】

ビュースクリプトでビジネスロジックを書いてしまうと言うのがどうしてダメなのか、もしくはどうして気持ちが悪いのか、というのを考えて見よう。

そもそもPHP君からしてみれば、動かしているPHPファイルがMだろうがVだろうがCだろうが、全く関係ない。もっとプリミティブな階層で動かしているので、そんな話は全く理解してないはず。

しかし、人間から見た場合、情報を扱うレイヤ、表示するレイヤ、それらを管理するレイヤという風に分けた方が、脳みそが分かりやすいからだ。

なので、只の思想と言うことになる。只の思想を実装に代えた物がMVCフレームワークだ。だからビュースクリプトにビジネスロジックを書くことに対しては、極端な話、只の個人の好き嫌いと言う事になる。

それを、「MVCフレームワークだから当然Cでビジネスロジックを実装し、Vは表示のみだろう」という話に持って言ってもし様が無いわけで、正直、全く理由になってはいない。何しろ只の好みの問題なのだから。

とはいえ、せっかく人間が作ったこれらの思想、CakePHPで言うところの規約にも当てはまるが、自ら破るのもなんだか後味が悪いし、どうせ複雑なものであるならば、最初は正攻法で攻めて見たいと思うのは多くの人がそう思っても否定出来ないと思う。



そういった思想、好み、規約なんかのレベルでフレームワークを考えると、はやり「ビュースクリプトでビジネスロジックを実装するのは“気持ち悪い”からダメ」と言う事になる。当然、俺も気持ち悪い。ただし、「悪」では決してない。

でももしかしたら、俺が気持ち悪いという部分はみんなとは違うかもしれない。

俺は基本的にWebデザイナにサーバサイドなビジネスロジックを考えさせるのはおかしいと思っている。たまにPHPが分かるWebデザイナもいる。そういう時は、ヘルパなどは説明しないでそのまま渡したりしている。

しかしPHP以外にまた別のフォーマットを覚えてもらうというケースが発生する可能性がある。それは、Smartyなどのテンプレートエンジンの存在だ。

PHP自体がテンプレートエンジンのようなものなので、テンプレートエンジンでテンプレートエンジンを動かすのはものすごい抵抗がある。特にCakePHPなどのフレームワークでSmartyを使っているのは、本当にデザイナのためなんだろうか。

実際にSmartyは何度も使っていたけど、SmartyはSmartyという方言を新たに習得する必要がある。
そして処理が嵩むので、当然ネイティブなPHPより遅くなる。ただでさえ遅くなるMVCフレームワークをさらに遅くするのもなんだかよろしさが薄い気がするわけだ。

多少話がそれたが、というわけでビューとコントローラの分離を考慮して実装して見る。CとVがほぼ完全分離という意味で、こういう例もあるという事を紹介したい。

どんなアクションでも、完全にデータが揃うまでビュースクリプトに渡さない、と言うものだ。
前回作ったvoteアクションで実装して見る。
  1. function vote()  
  2.   {  
  3.     if(!emptyempty($this->data)) {  
  4.   
  5.       if(!is_numeric($this->data['Quickpoll'])) {  
  6.         $this->redirect('index');  
  7.       }  
  8.   
  9.       // 該当するレコードの投票数を取得  
  10.       $selected = $this->Quickpoll->read('voted'$this->data['Quickpoll']);  
  11.       // 投票数をインクリメント  
  12.       $voted = ++$selected['Quickpoll']['voted'];  
  13.       // 投票を反映  
  14.       $this->Quickpoll->id = $this->data['Quickpoll'];  
  15.       $this->Quickpoll->saveField('voted'$voted);  
  16.     }  
  17.   
  18.     // quickpollsからデータを取得  
  19.     $conditions = array(  
  20.       'fields' => array(  
  21.         'Quickpoll.options',  
  22.         'Quickpoll.voted',  
  23.       ),  
  24.     );  
  25.     $quickpolls = $this->Quickpoll->find('list'$conditions);  
  26.   
  27.     // 投票データを整形する  
  28.     $data = array();  
  29.     foreach($quickpolls as $key=>$value) {  
  30.       $data[] = "['".$key."',".$value."]";  
  31.     }  
  32.   
  33.     // ビューへセット  
  34.     $this->set('data', join(","$data));  
  35.   
  36.   }  
何も選択されて無い場合、空のレコードが生成されてしまうので、その処理も追加してある(5~7行目)。この部分はモデルのvalidateで行っても良いだろう。

後は、基本的に1つの変数をdataにセットしているだけだ。前回までは配列をセットし、ビュースクリプト側で反復処理をさせていたが、今回はセットされたデータを表示するのみ、となっている。
そのため、ビュースクリプトも変更しなければならない。
  1. <div id="graph" style="width:600px;height:300px;margin:30px;"></div>  
  2. <script type="text/javascript">  
  3.   var data = [<?php echo $data;?>];  
  4.   plot = $.jqplot('graph', [data], {  
  5.     title: 'モビルスーツ人気投票',  
  6.     series:[{renderer:$.jqplot.BarRenderer}],  
  7.     axes: {  
  8.       xaxis: {  
  9.         renderer: $.jqplot.CategoryAxisRenderer,  
  10.         tickRenderer: $.jqplot.CanvasAxisTickRenderer,  
  11.         tickOptions: {  
  12.           enableFontSupport: true,  
  13.           angle: -30  
  14.         }  
  15.       }  
  16.     }  
  17.   });  
  18. </script>  
3行目で$dataを表示しているだけになった。これなら分かりやすい。

この程度の処理なら、たいした問題では無いが、たとえば複数のテーブルからログを採取し、整形して出力するような場合、コントローラ側でのファットな実装が重さの原因となるだろう。

さらに細かくみて見ると、voteアクションではセットする変数を直前で整形しているが、ここにビューの要素が出てきてしまっている。
  1. $data[] = "['".$key."',".$value."]";  
この部分だ。これは思いっきりビューだ。Vからビジネスロジックは排除できたが、CにVが入ってしまうのは宜しく無い。

幸いにもPHPには(PHP5.2からデフォルトで使用可能)連想配列をJSON形式にしてくれるPECLモジュールがあるので、それでdataをコントローラ側で作ってしまっても良い。ただし、そこまでやるのは今回の趣旨から大きく外れすぎるので、とっとと次へ進む事にする。

さて、いろいろ突っ込みどころがあるスクリプトだが、最低限のロジックだけは入れておこう。
現状、voteアクションが実行された際、voteビューが表示されるわけだが、このページは実はリロードで投票することが可能になっている。仮に「フォビドゥン」を選択した状態で投票し、voteビューでリロードすると、「フォビドゥン」へ2回投票したことになってしまう。これはマズい。

重複投稿を避ける一番簡単な方法がある。該当するアクションに対して、自分自身へリダイレクトする方法だ。アクションの最後に以下のコードを追記してみよう。
  1. $this->redirect('vote');  
これで、投票結果ページをリロードしても、追加で投票はされなくなる。ただし、アクション内が
  1. 投票された場合の処理
  2. 投票に関係なく必ず実行される処理
という処理がないと動かないので注意。リダイレクトした場合、2だけが実行されるからだ。

投票された場合、1>2の順で実行される。1は自分自身へリダイレクトし、リダイレクトした際には投票はされていないので、自動的に1が無視され(この時点で重複登録の回避になっている)、2が実行されて結果だけが表示される、という流れだ。

このような実装にしておくと、たとえば投票画面で「結果を見る」リンクを静的に設置しておいても、問題なく動くようになる。

この状態でのvoteアクション全体のソースコードは以下のようになる。
  1. function vote()  
  2.   {  
  3.     if(!emptyempty($this->data)) {  
  4.   
  5.       if(!is_numeric($this->data['Quickpoll'])) {  
  6.         $this->redirect('index');  
  7.       }  
  8.   
  9.       // 該当するレコードの投票数を取得  
  10.       $selected = $this->Quickpoll->read('voted'$this->data['Quickpoll']);  
  11.       // 投票数をインクリメント  
  12.       $voted = ++$selected['Quickpoll']['voted'];  
  13.       // 投票を反映  
  14.       $this->Quickpoll->id = $this->data['Quickpoll'];  
  15.       $this->Quickpoll->saveField('voted'$voted);  
  16.   
  17.       // 重複投稿の回避  
  18.       $this->redirect('vote');  
  19.     }  
  20.   
  21.     // quickpollsからデータを取得  
  22.     $conditions = array(  
  23.       'fields' => array(  
  24.         'Quickpoll.options',  
  25.         'Quickpoll.voted',  
  26.       ),  
  27.     );  
  28.     $quickpolls = $this->Quickpoll->find('list'$conditions);  
  29.   
  30.     // 投票データを整形する  
  31.     $data = array();  
  32.     foreach($quickpolls as $key=>$value) {  
  33.       $data[] = "['".$key."',".$value."]";  
  34.     }  
  35.   
  36.     // ビューへセット  
  37.     $this->set('data', join(","$data));  
  38.   
  39.   }  
これで、わざわざ
  1. 登録処理アクション(ビュー未使用)
  2. 結果表示アクション(ビュー使用)
なんていうアクションを書かないで済むわけだ。CakePHPを俺流に料理してみた結果、このやり方が一番スマートな気がする。

現在までの各ファイルのソースコードはこちら。

[app/controllers/quickpolls_controller.php]
  1. class QuickpollsController extends AppController {  
  2.   
  3.   var $name='Quickpolls';  
  4.   var $helpers = array('Html''Javascript');  
  5.   
  6.   function beforeFilter()  
  7.   {  
  8.     $this->layout = 'jqplot';  
  9.   }  
  10.   
  11.   function index()  
  12.   {  
  13.     $conditions = array(  
  14.       'fields' => array(  
  15.         'Quickpoll.id',  
  16.         'Quickpoll.options',  
  17.       ),  
  18.     );  
  19.     $this->set('quickpolls'$this->Quickpoll->find('list'$conditions));  
  20.   }  
  21.   
  22.   function vote()  
  23.   {  
  24.     if(!emptyempty($this->data)) {  
  25.   
  26.       if(!is_numeric($this->data['Quickpoll'])) {  
  27.         $this->redirect('index');  
  28.       }  
  29.   
  30.       $selected = $this->Quickpoll->read('voted'$this->data['Quickpoll']);  
  31.       $voted = ++$selected['Quickpoll']['voted'];  
  32.       $this->Quickpoll->id = $this->data['Quickpoll'];  
  33.       $this->Quickpoll->saveField('voted'$voted);  
  34.       $this->redirect('vote');  
  35.     }  
  36.     $conditions = array(  
  37.       'fields' => array(  
  38.         'Quickpoll.options',  
  39.         'Quickpoll.voted',  
  40.       ),  
  41.     );  
  42.     $quickpolls = $this->Quickpoll->find('list'$conditions);  
  43.   
  44.     $data = array();  
  45.     foreach($quickpolls as $key=>$value) {  
  46.       $data[] = "['".$key."',".$value."]";  
  47.     }  
  48.     $this->set('data', join(","$data));  
  49.   
  50.   }  
  51. }  
※コメントなどは削除済み

[app/models/quickpoll.php]
  1. class Quickpoll extends AppModel {  
  2.   var $name = 'Quickpoll';  
  3. }  
※バリデーションは消しておいた

[app/views/quickpolls/index.ctp]
  1. <?php echo $form->create('Quickpoll', array(  
  2.   'url' => array(  
  3.     'controller' => 'quickpolls',  
  4.     'action' => 'vote',  
  5. )));?>  
  6. <?php echo $form->input('Quickpoll',array(  
  7.   'type' => 'radio',  
  8.   'options' => $quickpolls,  
  9.   'legend' => false,  
  10.   'div' => false,  
  11.   'separator' => '  
  12. ',  
  13.   'value' => null,  
  14.   )  
  15. );?>  
  16. <?php echo $form->end('Vote');?>  

[app/views/quickpolls/vote.ctp]
  1. <div id="graph" style="width:600px;height:300px;margin:30px;"></div>  
  2. <script type="text/javascript">  
  3.   var data = [<?php echo $data;?>];  
  4.   plot = $.jqplot('graph', [data], {  
  5.     title: 'モビルスーツ人気投票',  
  6.     series:[{renderer:$.jqplot.BarRenderer}],  
  7.     axes: {  
  8.       xaxis: {  
  9.         renderer: $.jqplot.CategoryAxisRenderer,  
  10.         tickRenderer: $.jqplot.CanvasAxisTickRenderer,  
  11.         tickOptions: {  
  12.           enableFontSupport: true,  
  13.           angle: -30  
  14.         }  
  15.       }  
  16.     }  
  17.   });  
  18. </script>  

[app/views/layouts/jqplot.ctp]
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  
  2.   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">  
  3. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">  
  4. <head>  
  5. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />  
  6. <meta http-equiv="Content-Script-Type" content="text/javascript" />  
  7. <meta http-equiv="Content-Style-Type" content="text/css" />  
  8. <title>CakePHP jqPlot Test</title>  
  9. <?php echo $html->css('jquery.jqplot.min'); ?>  
  10. <!--[if IE]><?php echo $javascript->link('excanvas.min'); ?><![endif]-->  
  11. <?php echo $javascript->link('jquery-1.3.2.min'); ?>  
  12. <?php echo $javascript->link('jquery.jqplot.min'); ?>  
  13. <?php echo $javascript->link('plugins/jqplot.categoryAxisRenderer.min'); ?>  
  14. <?php echo $javascript->link('plugins/jqplot.canvasTextRenderer.min'); ?>  
  15. <?php echo $javascript->link('plugins/jqplot.canvasAxisTickRenderer.min'); ?>  
  16. <?php echo $javascript->link('plugins/jqplot.barRenderer.min'); ?>  
  17.   
  18. </head>  
  19. <body>  
  20.   
  21. <h1>CakePHP jqPlot Test</h1>  
  22. <div>  
  23. <?php echo $content_for_layout; ?>  
  24. </div>  
  25.   
  26. </body>  
  27. </html>  

次回はvoteビューに記述されたjqPlotの説明をする。

まだつづく。