[PHP5] OOPで掲示板を作ってみる – Step1~Step2

「PHP ○○ 作り方」みたいなキーワード検索するとべた書きの作り方がHitしまくる。
それなのにOOPなサンプルはあんまり見つからない。言語を英語にしても。
キーワードをMVCにすると結構ヒットするんだけど、殆どがフレームワークの使い方だったりして。

OOPの例としてよくあるのはオブジェクト指向プログラミングで書いたようなクラス単体のものだと思う。
でも実際何か作ろうとしたらクラスひとつで足りるわけがないし、クラス書いたすぐ真下で実行なんてしないじゃん?
ZendとかCakeとかPEARみたいなフレームワークを使って何かを作るにしても
クラスをどうアレすれば掲示板とかになるの?って疑問は解決しない。
フレームワーク使えばそりゃ掲示板の一つや二つさくっと出来ますよ?
出来るけどフレームワーク使うほどでもない時もあるし、OOPで組めって言われた時困るから本質が知りたい。
それで、探して見つからないなら形にして達人からのコメント待てばいいじゃない!って結論に至ったので
独学で得た情報を元にオブジェクト指向な掲示板を作ってみることにしました。

暇を持て余したOOP達人が見てくれることを期待しながら 😀

なお、この記事はある程度ベタ書き経験があり、オブジェクト指向プログラミングに書いてあることが一通り理解出来ている人じゃないと意味不明だと思います。

目次

  • 序章
  • Step1 : MVCを揃える
  • Step2 : テンプレート作成
  • Step3 : 書き込み
  • Step4: ログの表示
  • Step5: 設定ファイルと細かな修正
  • Step6: 編集&削除画面
  • Step7: 編集&削除機能
  • Step8: ページング

Step1

もう4は無いだろと思うんでPHP5で組みます。サポートもしません。
バージョンは5.2.6以上かな~と思うんだけど、うちのローカルが5.3.1だから
5.2で動かない関数がうっかり混じっちゃうかもしれない。

BBSのMVC

タイトルにあるように作るのは掲示板。
何で掲示板かっていうと書道で言うところの永字八法みたいなもんだと思ってるからです。

永字八法に倣って必要な機能を8つ挙げてみる。

  • フォーム送信
  • バリデーション
  • 書き込み
  • 読み込み
  • 削除
  • 編集
  • 表示
  • ページネーション

で、上記に挙げた必要機能を表示とログ操作に分けると、

ログ操作:

  • 書き込み
  • バリデーション
  • 読み込み
  • 削除
  • 編集

表示:

  • フォーム送信
  • ログの表示
  • ページネーション

になる。

これにMVCパターンを当てはめると
ログ操作 → データ扱うからModel
表示 →  出力を担当するからView
となります。分けただけなのにオブジェクト指向な雰囲気になってきましたよ!

この二つは常に連動しているが、モデルとビューを直接結びつけたら元のべた書きになってしまうので
MVCらしく中間に立って実行を振り分けるコントローラーを作って、それに処理を任せることにする。

で、どんな掲示板でも大抵は$_POSTをSwitchとかで処理して、$_GETで表示の仕方を切り替えてる場合が殆どだと思う。
よってコントローラーは、POSTでモデルに、GETでビューに処理を振り分けるのが仕事ということになりますね。

Hello World

じゃ早速お約束のHelloWorldを表示してみる 😀

適当なディレクトリにindex.phpを作ったら、書き込みフォームにnameとcommentエリアを配置。
デザイン的なことは後でやっつけることにしてYUI CSSをざっくりリンクしておきました。

index.php

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>OOPdeBBS</title>
<meta http-equiv="Content-Style-Type" content="text/css" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.1.1/build/cssreset/reset-min.css" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.1.1/build/cssfonts/fonts-min.css" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.1.1/build/cssbase/base-min.css" />
<style type="text/css">
#wrapper {
    width:960px;
    margin:0 auto;
}
</style>
</head>
<body>
<div id="wrapper">
<h1>OOPdeBBS</h1>
<form action="" method="post">
<table>
<tr>
    <th scope="row"><label for="name">Name</label></th>
    <td><input type="text" name="name" /></td>
</tr>
<tr>
    <th scope="row"><label for="comment">Comment</label></th>
    <td><textarea name="comment" rows="4" cols="50"></textarea></td>
</tr>
</table>
<div class="button"><button type="submit" name="mode" value="write">Submit</button></div>
</form>
</div>
</body>
</html>

index.phpを作ったらその先頭でコントローラーを呼び出す。
ついでにエラー表示もONにしてNOTICEに備える。

クラスファイルはlibディレクトリに入れることにします。

index.php

ini_set("display_errors", "On");//エラー表示ON
error_reporting(E_ALL);

require_once('lib/controller.php');

$controller = new Controller();
$controller->execute();

libディレクトリからcontroller.phpを呼び出し、executeというメソッドを実行するというソースをindex.phpの先頭に書いた。
これはPHPでOOPのサンプルを見習ってます。

index.phpではこれ以上のことはしません。
必要なファイルの読み込みと、コントローラー(と実行メソッド)を呼ぶだけ。

ソースの通りlibディレクトリとcontrollerファイルを作成する。

lib/controller.php

class Controller {
  
  
    public function __construct()
    {
    }
  
    /**
    * index.phpから実行されるメソッド
    */
    public function execute()
    {
    }
}

executeは外から実行されるメソッドなのでvisibilityはpublicで。
コントローラーがモデルとビューに仕事を振り分けるとなれば、
MとVのクラスインスタンスを作るのは最初に実行されるexecuteメソッドということになるから、
executeの中にそういうソースを追加する。

lib/controller.php

public function execute()
{
    require_once('model.php');
    require_once('view.php');

    $modelInstance = new Model();//モデル
    $viewInstance = new View();//ビュー
}

モデルとビューのクラスファイルは次のように。

lib/model.php

class Model {
    
    public function __construct()
    {
    }
    
    /**
     * 仮のメッセージを表示する
     * @test
     */
    public function dispatch()
    {
        print 'Hello Model!';
    }
}

lib/view.php

class View {
    
    public function __construct()
    {
    }
    
    /**
     * 画面を表示する
     */
    public function display()
    {
        print 'Hellow View!';
    }
    
}

※PHPのタグは省略してます。

Viewはそのままdisplayメソッドを実行すればいいけど、
モデルの方はPOST送信されたときにだけdispatchというメソッドを実行させたいからifで仕分け。

lib/controller.php (class Controller)

public function execute()
{
    require_once('model.php');
    require_once('view.php');

    $modelInstance = new Model();//モデル
    $viewInstance = new View();//ビュー
    
    if(!empty($_POST)){
        $modelInstance->dispatch();
    }
    
    $viewInstance->display();
  
}

これでindex.phpを表示するとHello View!が出て、送信ボタンを押すとHello Model!が出るようになった。

step1-preview

実にそれっぽい!でもソースを見るとDOCTYPEの上に出ちゃってる現実…。
コントローラーの実行関数が先頭にあるから当然といえば当然なんだけど、これでは困る。
ならindex.phpのHTMLをテンプレートに移してViewでテンプレートの操作をすれば
メッセージはタイトルとフォームの間に、ログはフォームの下に配置出来るんじゃない?

ディレクトリ構成

step1-directory

Step1 Download

ソースファイルのライセンスは Creative Commons BY-NC です。
ローカルサーバーでテストしてください。

“OOPでBBS Step1” をダウンロード

oop-bbs-step1.zip – 1369 回のダウンロード – 4.26 KB

Step2

テンプレート作成

じゃindex.phpのHTMLソースをまるっとテンプレートファイルに移すぞー。

Step1の状態のままindex.phpをビューファイルにしてしまってもいいのだが、
Smatryみたいなテンプレートエンジンを使わない場合だと、テンプレートエンジンがやってくれる細々としたことをどこかでやらねばならないので
この掲示板ではそのロジックをviewクラスに書くつもりでクラスとテンプレートの二段構えにします。

テンプレートファイルの拡張子は有名どころのtplにしときます。Dreamweaverでコードカラーリングされるしね。
ファイルでごっちゃになると美しくないので、テンプレートはtemplateというディレクトリに入れることにした。

template/index.tpl

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>OOPでBBS</title>
<meta http-equiv="Content-Style-Type" content="text/css" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.1.1/build/cssreset/reset-min.css" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.1.1/build/cssfonts/fonts-min.css" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.1.1/build/cssbase/base-min.css" />
<style type="text/css">
#wrapper {
    width:960px;
    margin:0 auto;
}
</style>
</head>
<body>
<div id="wrapper">
<h1>OOP-BBS</h1>
<?php echo($message); ?>
<form action="" method="post">
<table>
<tr>
    <th scope="row"><label for="name">Name</label></th>
    <td><input type="text" name="name" /></td>
</tr>
<tr>
    <th scope="row"><label for="comment">Comment</label></th>
    <td><textarea name="comment" rows="4" cols="50"></textarea></td>
</tr>
</table>
<div class="button"><button type="submit" name="mode" value="write">Submit</button></div>
</form>
<?php echo($logdata); ?>
</div>
</body>
</html>

エラーとかのメッセージを$messageという変数に、
ログの表示を$logdataという変数に渡して出力するようにPHPを書いておく。

Viewの変更

新たに「テンプレート」という情報が増えた。必要なのはディレクトリ名とファイル名。
表示のことはViewの仕事だからViewクラスに任せる事にします。
ディレクトリは変えないからオブジェクト定数にして、ファイル名はメンバ変数として作成。

lib/view.php (class View)

const TEMPLATE_DIRECTORY = 'template/'; //テンプレートディレクトリ名
private $_templateName; //テンプレートファイル名
	
public function __construct()
{
    $this->_templateName = '';
}

※アンダースコアつけてるのはprivateの目印です。(最後に補足する)

ファイル名を入れるようの変数はコンストラクタで初期設定。
データの受け渡しはゲッターとセッターを使うっていうルールに則って
テンプレート名をメンバ変数へ渡すのに使うセッターを作る。

lib/view.php (class View)

/**
 * テンプレート名をセットする
 */
public function setTemplateName($name)
{
    $this->_templateName = $name.'.tpl';
}
/**
 * テンプレート名のゲッター
 */
public function getTemplateName()
{
   return  self::TEMPLATE_DIRECTORY . $this->_templateName;
}

テンプレートの拡張子は.tpl 固定だからセッターに渡すのは拡張子より前だけでいいよね。
もし別の拡張子にしたくなってもセッターを変更するだけでいいから楽だし。

displayメソッドをちょっと変更してテンプレートを読み込むようにする。

lib/view.php (class View)

/**
 * 画面に表示する
 */
public function display()
{
    $message = '';
    $logdata = '';
        
    include($this->getTemplateName());
}

テンプレートにタグっぽく書いた変数が無いってエラーを回避するために、
$message$logdataには空の値を代入しておく。

メッセージをViewに渡す

コントローラーのdisplayメソッド実行よりも上にテンプレートのセッタを追加。

lib/clontroller.php (class Controller – function execute)

//テンプレートをセット
$viewInstance->setTemplateName('index');

Hello View!は削ったから出なくなったけど、前回と同じ状態になった。
でもまだモデルのメッセージがタイトルよりも上に出てるから、追加でViewにメッセージのセッタを作る。
メンバ変数$systemMessageをprivateで作成。コンストラクタで空の値を代入しておく。

lib/view.php (class View)


/**
 * メッセージのセッター
 */
public function setSystemMessage($text)
{
    $this->_systemMessage = $text;
}

/**
 * システムメッセージのゲッター
 */
public function getSystemMessage()
{
    return $this->_systemMessage;
}

/**
 * 画面に表示する
 */
public function display()
{
    $message = '<p class="message">'.$this->getSystemMessage().'</p>';
    $logdata = '';
	
    include($this->getTemplateName());
}

モデルのメッセージをprintからreturnにした後でcontrollerを変更する。

lib/model.php (class Model – function dispatch)

return 'Hello Model!';

lib/controller.php (class Controller)

/**
 * index.phpから実行されるメソッド
 */
public function execute()
{

    require_once('model.php');
    require_once('view.php');

    $modelInstance = new Model();//モデル
    $viewInstance = new View();//ビュー

    $viewInstance->setTemplateName('index');//テンプレートをセット
        
    $message = '';
        
    if(!empty($_POST)){
        $message = $modelInstance->dispatch();
            
    }
        
    $viewInstance->setSystemMessage($message);//メッセージをセット
        
    $viewInstance->display();//表示
    
}

これでモデルからのメッセージがタイトルとフォームの間に表示されるようになった。

実行結果

メッセージの渡し方がModel→Controller→Viewになってて遠回りな感じがするけど
コントローラーが中間に立って実行を振り分けているから序盤に書いたことは実現できた。
修正のしやすさにOOPの良さが現れているよね。

ますますそれらしくなってきたぞ!と盛り上がりながら次回に続く。

ディレクトリ構成

step2-directory

Step2 Download

ソースファイルのライセンスは Creative Commons BY-NC です。
ローカルサーバーでテストしてください。

“OOPでBBS Step2” をダウンロード

oop-bbs-step2.zip – 1511 回のダウンロード – 5.09 KB

命名規約について

名前を規約に従ってつけると管理がよりしやすくなる。
規約は完全なるマイルールでもいいし社内規程でもいいしフレームワークの受け売りでもいい。
見るのが自分だけなら適当でもいいんだけど、今回はZendとCakeとPEARを参考にして統一。

ファイルとクラス名
今の所はクラスがキャメルケース、ファイル名は小文字。名前の単語が増えるときはファイル名だけ単語の区切りをアンダースコアにする。(CakePHPと同じ)
関数とメソッド
先頭が小文字のキャメルケース。privateとprotectedなメソッドの先頭はアンダースコアをつける。
定数
全部大文字。単語の区切りにアンダースコアを使う。
変数
privateとprotectedな変数の接頭にアンダースコアをつける。単語の区切りはキャメルケース。数字の使用は控える。
コメント
なるべく手抜きしない。phpDocumenterの構文に従う
インデント
半角スペース4つ

ファイルとクラス名はCakeだけど、他は三者共通。
PHPの関数がアンダースコア記法だからキャメルケースだと自作って分かりやすい。

「[PHP5] OOPで掲示板を作ってみる – Step1~Step2」への1件のフィードバック

  1. ピンバック: links for 2010-11-09

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください