iT邦幫忙

DAY 30
20

逐步提昇PHP技術能力系列 第 30

逐步提昇PHP技術能力 - 逐步改善軟體架構 - 套用MVC架構與結語

既然MVC都有了,就繼續調整其他程式,把架構慢慢放進去。
改完了index.php,之後就把做好的架構套用到其他程式上。以forum.php為例,他的作用是顯示單一論壇內的文章列表。

<?php
session_start();
if(isset($_SESSION['user']['account'])) {
	$member = true;
	$name = $_SESSION['user']['name'];
} else $member = false;
$conn = mysql_connect('localhost', 'root', '');
if(!$conn)
	die('mysql connection error.');

mysql_select_db('myforum');

$forum_id = mysql_real_escape_string($_GET['id']);
if(isset($_SESSION['msg'])) {
	$message = mysql_real_escape_string($_SESSION['msg']);
	unset($_SESSION['msg']);
}
$sql = "SELECT * FROM forums WHERE id=$forum_id";
$result = mysql_query($sql, $conn);
$forum = mysql_fetch_array($result);
?>


	<table width="800" border="1" cellpadding="0" cellspacing="0" align="center">
		<!-- header start -->
		<tr>
			<th align="left"><a href="index.php">我的論壇</a> - <?php echo $forum['name'];?></th>
		</tr>
		<tr>
			<td style="text-align:right" bgcolor="#336699">
				<table align="right">
					<tr>
<?php
if($member) {
?>
						<td>Welcome [<?php echo $name;?>] <button onclick='document.location.href="logout.php"'>登出</button></td>
<?php
} else {
?>
						<form method="post" action="login.php">
							<td>登入後可留言  </td>
							<td>帳號</td>
							<td><input type="text" name="account"></td>
							<td>密碼</td><td><input type="password" name="password"></td>
							<td><input type="submit" value="登入"></td>
						</form>
<?php
}
?>
					</tr>
				</table>
			</td>  
		</tr>
		<!-- header end -->
		<!-- content start-->
		<tr height="600">
			<td valign="top">
				<table width="100%" border="1" cellspacing="0" cellpadding="5">
					<tr bgcolor="#DDEEFF">
						<th>標題</th>
						<th>作者</th>
						<th>發表時間</th>
						<th>操作</th>
					</tr>
<?php
	$sql = "SELECT a.id, a.title, a.create_time, u.name FROM articles a LEFT JOIN users u ON u.id=a.users_id WHERE forums_id=$forum_id";
	$result = mysql_query($sql, $conn);
	$count = 0;
	while($row = mysql_fetch_array($result)) {
		if($count%2==0) {
			$style = 'bgcolor="#EEFFEE"';
		} else {
			$style = 'bgcolor="#FFFFFF"';
		}
?>
					<tr <?php echo $style;?>>
						<td><?php echo $row['title'];?></td>
						<td><?php echo $row['name'];?></td>
						<td><?php echo $row['create_time'];?></td>
						<td>
							<button onclick="document.location.href='article.php?id=<?php echo $row['id'];?>'">進入</button>
						</td>
					</tr>
<?php
		$count++;
	}
?>
				</table>
			</td>
		</tr>
		<!-- content end -->
		<!-- footer start -->
		<tr bgcolor="#336699">
			<td align="center">
				<font color="#EFEFEF">Copyright 1899  by Fillano</font>
			</td>
		</tr>
		<!-- footer end -->
	</table>

從瀏覽器看:

可以看到,這支程式在套用MVC架構前,就跟原本的index.php一樣,資料、頁面的邏輯混雜在一起,要修改很困難。根據之前的步驟,第一步把view切開,不過我們一小步一小步走,先拆成兩個php:

把view拆出以後的forum.php:

<?php
session_start();
if(isset($_SESSION['user']['account'])) {
	$member = true;
	$name = $_SESSION['user']['name'];
} else $member = false;
$conn = mysql_connect('localhost', 'root', '');
if(!$conn)
	die('mysql connection error.');

mysql_select_db('myforum');

$forum_id = mysql_real_escape_string($_GET['id']);
if(isset($_SESSION['msg'])) {
	$message = mysql_real_escape_string($_SESSION['msg']);
	unset($_SESSION['msg']);
}
$sql = "SELECT * FROM forums WHERE id=$forum_id";
$result = mysql_query($sql, $conn);
$forum = mysql_fetch_array($result);
	$sql = "SELECT a.id, a.title, a.create_time, u.name FROM articles a LEFT JOIN users u ON u.id=a.users_id WHERE forums_id=$forum_id";
	$result = mysql_query($sql, $conn);
	$count = 0;
	$rows = array();
	while($row = mysql_fetch_array($result)) {
		if($count%2==0) {
			$style = 'bgcolor="#EEFFEE"';
		} else {
			$style = 'bgcolor="#FFFFFF"';
		}
		$row['style'] = $style;
		$rows[] = $row;
		$count++;
	}
include 'views/forum.php';

拆到views裡面的forum.php:

	<table width="800" border="1" cellpadding="0" cellspacing="0" align="center">
		<!-- header start -->
		<tr>
			<th align="left"><a href="index.php">我的論壇</a> - <?php echo $forum['name'];?></th>
		</tr>
		<tr>
			<td style="text-align:right" bgcolor="#336699">
				<table align="right">
					<tr>
<?php
if($member) {
?>
						<td>Welcome [<?php echo $name;?>] <button onclick='document.location.href="logout.php"'>登出</button></td>
<?php
} else {
?>
						<form method="post" action="login.php">
							<td>登入後可留言  </td>
							<td>帳號</td>
							<td><input type="text" name="account"></td>
							<td>密碼</td><td><input type="password" name="password"></td>
							<td><input type="submit" value="登入"></td>
						</form>
<?php
}
?>
					</tr>
				</table>
			</td>  
		</tr>
		<!-- header end -->
		<!-- content start-->
		<tr height="600">
			<td valign="top">
				<table width="100%" border="1" cellspacing="0" cellpadding="5">
					<tr bgcolor="#DDEEFF">
						<th>標題</th>
						<th>作者</th>
						<th>發表時間</th>
						<th>操作</th>
					</tr>
<?php
foreach($rows as $row) {
?>
					<tr <?php echo $row['style'];?>>
						<td><?php echo $row['title'];?></td>
						<td><?php echo $row['name'];?></td>
						<td><?php echo $row['create_time'];?></td>
						<td>
							<button onclick="document.location.href='article.php?id=<?php echo $row['id'];?>'">進入</button>
						</td>
					</tr>
<?php
}
?>
				</table>
			</td>
		</tr>
		<!-- content end -->
		<!-- footer start -->
		<tr bgcolor="#336699">
			<td align="center">
				<font color="#EFEFEF">Copyright 1899  by Fillano</font>
			</td>
		</tr>
		<!-- footer end -->
	</table>

拆完以後確定可以執行,畫面也沒問題,這時就可以把view從php+html改成Twig。根據index的經驗,實際上會用到的其實只有一部分,其他都已經放在base.html裡了。所以就把需要的部份抽出,參考views/index.html來修改一下:

{% extends "base.html" %}
{% block navbar %}<a href="index.php">我的論壇</a> - {{$forum_name}}{% endblock %}
{% block content %}
				<table width="100%" border="1" cellspacing="0" cellpadding="5">
					<tr bgcolor="#DDEEFF">
						<th>標題</th>
						<th>作者</th>
						<th>發表時間</th>
						<th>操作</th>
					</tr>
{% for row in data %}
{% if loop.index0%2==0 %}
{% set style="#EEFFEE" %}
{% else %}
{% set style="#FFFFFF" %}
{% endif %}
					<tr bgcolor="{{style}}">
						<td>{{$row.title}}</td>
						<td>{{$row.name}}</td>
						<td>{{$row.create_time}}</td>
						<td>
							<button onclick="document.location.href='article.php?id={{$row.id}}'">進入</button>
						</td>
					</tr>
{% endfor %}
				</table>
{% endblock %}

然後根據目前的調整,改寫一下forum.php:

<?php
include 'bootstrap.php';
if(isset($_SESSION['user']['account'])) {
	$member = true;
	$name = $_SESSION['user']['name'];
} else $member = false;
$conn = mysql_connect(DB_HOST, DB_USER, DB_PASSWORD);
if(!$conn)
	die('mysql connection error.');

mysql_select_db(DB_NAME);

$forum_id = mysql_real_escape_string($_GET['id']);
if(isset($_SESSION['msg'])) {
	$message = mysql_real_escape_string($_SESSION['msg']);
	unset($_SESSION['msg']);
}
$sql = "SELECT * FROM forums WHERE id=$forum_id";
$result = mysql_query($sql, $conn);
$forum = mysql_fetch_array($result);
	$sql = "SELECT a.id, a.title, a.create_time, u.name FROM articles a LEFT JOIN users u ON u.id=a.users_id WHERE forums_id=$forum_id";
	$result = mysql_query($sql, $conn);
	$count = 0;
	$rows = array();
	while($row = mysql_fetch_array($result)) {
		if($count%2==0) {
			$style = 'bgcolor="#EEFFEE"';
		} else {
			$style = 'bgcolor="#FFFFFF"';
		}
		$row['style'] = $style;
		$rows[] = $row;
		$count++;
	}
$view = Fillano\Core\View::getInstance(TEMPLATE_ENGINE, 'views');
$view->assign(array(
	'member'=>$member,
	'name'=>$name,
	'data'=>$rows,
	'forum_name'=>$forum['name'],
	'message'=>$message
));
$view->render('forum','html');

這是可以跑的中間版本,因為必須引入bootstrap.php,才會做class autoloading,不過mysql_connection()跟mysql_select_db()在之前的架構調整中已經從bootstrap.php中移除,所以還是要留著。同樣先用瀏覽器確認一下是否可以執行。沒問題了,就繼續小步前進。

接下來繼續拆Model,先定義好介面:

<?php
namespace Fillano\Models;

Interface IForum
{
	public function getForumById($forum_id);
	public function getArticleList($forum_id);
}

接著先用Mysql來實作,這樣就可以把之前的程式碼移過來使用:

<?php
namespace Fillano\Models;

class PDOForum implements IForum
{
	private $pdo;
	public function __construct($pdo)
	{
		$this->pdo = $pdo;
	}
	public function getForumById($forum_id)
	{
		$sql = "SELECT * FROM forums WHERE id=:forum_id";
		$stmt = $this->pdo->prepare($sql);
		if($stmt->execute(array(":forum_id"=>$forum_id))) {
			return $stmt->fetch();
		} else {
			return null;
		}
	}
	public function getArticleList($forum_id)
	{
		$sql = "SELECT a.id, a.title, a.create_time, u.name "
			."FROM articles a "
			."LEFT JOIN users u ON u.id=a.users_id "
			."WHERE forums_id=:forum_id";
		$stmt = $this->pdo->prepare($sql);
		if($stmt->execute(array(":forum_id"=>$forum_id))) {
			return $stmt->fetchAll();
		} else {
			return array();
		}
	}
}

確定沒問題以後,因為之後的Model架構會使用PDO,所以再把MysqlForum中的程式調整成PDO,程式就不貼了,其實跟Mysql版本差不是很多。

最後再來改Controller,這樣就需要改變原本forum.php的進入點,變成index.php?mod=forum&id=1這樣模式的URL。首先要把在forum.php中剩下的程式碼,移到Forum這個Controller中。加入Model之後,原本forum.php的程式碼會變成這樣:

<?php
include 'bootstrap.php';
if(isset($_SESSION['user']['account'])) {
	$member = true;
	$name = $_SESSION['user']['name'];
} else $member = false;
$forum_id = mysql_real_escape_string($_GET['id']);
if(isset($_SESSION['msg'])) {
	$message = mysql_real_escape_string($_SESSION['msg']);
	unset($_SESSION['msg']);
}
$model = Fillano\Core\ModelFactory::getInstance('Mysql', 'Forum');
$forum = $model->getForumById($forum_id);
$rows = $model->getArticleList($forum_id);
$view = Fillano\Core\View::getInstance(TEMPLATE_ENGINE, 'views');
$view->assign(array(
	'member'=>$member,
	'name'=>$name,
	'data'=>$rows,
	'forum_name'=>$forum['name'],
	'message'=>$message
));
$view->render('forum','html');

維持forum.php的程式碼不變(究竟他還是能正常運作),然後把這些程式碼先複製到Forum Controller中,然後再修改一下:

<?php
namespace Fillano\Controllers;

use Fillano\Core\Controller;

class Forum extends Controller
{
	public function index($req, $rep)
	{
		if(isset($_SESSION['user']['account'])) {
			$member = true;
			$name = $_SESSION['user']['name'];
		} else $member = false;
		if(isset($_SESSION['msg'])) {
			$message = mysql_real_escape_string($_SESSION['msg']);
			unset($_SESSION['msg']);
		}
		$forum_id = $req->get['id'];
		$model = $this->useModel('Forum');
		$forum = $model->getForumById($forum_id);
		$rows = $model->getArticleList($forum_id);
		$view = $rep->useView();
		$view->assign(array(
			'member'=>$member,
			'name'=>$name,
			'data'=>$rows,
			'forum_name'=>$forum['name'],
			'message'=>$message
		));
		$view->render('forum','html');
	}
}

最後修改一下index.html樣板中的網址,改變forum的進入點:

{% extends "base.html" %}
{% block navbar %}我的論壇{% endblock %}
{% block content %}
				<table width="100%" border="1" cellspacing="0" cellpadding="5">
					<tr bgcolor="#DDEEFF">
						<th>論壇名稱</th>
						<th>文章數</th>
						<th>最新文章</th>
						<th>操作</th>
					</tr>
{% for row in data %}
{% if loop.index0%2==0 %}
{% set style="#EEFFEE" %}
{% else %}
{% set style="#FFFFFF" %}
{% endif %}
					<tr bgcolor="{{style}}">
						<td>{{row.name}}</td>
						<td>{{row.count}}</td>
						<td>{{row.title}}</td>
						<td>
							<button onclick="document.location.href='?mod=forum&id={{row.id}}'">進入</button>
						</td>
					</tr>
{% endfor %}
				</table>
{% endblock %}

完成以後,先清一下樣板引擎的cache,然後回到首頁,進入個別的論壇,可以看到網址不太一樣,但是畫面沒變:

按照這個方式繼續下去,應該就可以一支一支程式地把架構調整好一點。不過這些範例程式很簡單,在實際的專案中,應該會有更複雜的狀況要解決就是了。

=謝幕=

我大概是最後交卷的吧?

每天這樣思考架構,嘗試解決方案,探索新知,很充實但是也很累(每天大概會花兩三個小時以上coding或測試)...不過還是希望能表達出一個看法:寫出有可維護架構的PHP程式不難,即使開發之初用最無法維護的方式開發PHP應用軟體,也還是有可能不花太多時間就把架構改善到比較容易維護的程度,而且修改過程中,所有的程式仍然可運行。有時候要跳一大步需要很大的勇氣,但是小步前進也會達到終點。

台灣寫PHP的廠商應該蠻多的,但是還真的看到不少這類的問題,也被拉進黑洞過(接手處理...),頗有一些感觸。這三十天,總算有稍微發發怨氣XD


上一篇
逐步提昇PHP技術能力 - 逐步改善軟體架構 - 寫一個簡單的Controller
系列文
逐步提昇PHP技術能力30
0
fillano
iT邦超人 1 級 ‧ 2013-10-30 23:01:35

code有些地方沒清乾淨...嘿嘿

不過我看來算乾淨,清楚很多的了 => 雖然我是 .net 派的 XD

0
月半車甫
iT邦研究生 3 級 ‧ 2013-10-30 23:41:29

讚拍手灑花恭喜完賽

fillano iT邦超人 1 級 ‧ 2013-10-30 23:42:50 檢舉

謝謝。哈哈

0
一級屠豬士
iT邦大師 1 級 ‧ 2013-10-31 07:58:07

灑花恭喜完賽
送上MM一枚

fillano iT邦超人 1 級 ‧ 2013-10-31 10:33:16 檢舉

謝謝哈哈

0
老鷹(eagle)
iT邦高手 1 級 ‧ 2013-10-31 08:28:25

恭喜費大公達陣~~!
拍手拍手拍手拍手
灑花灑花灑花灑花
rockrockrockrock
簽名簽名簽名簽名

fillano iT邦超人 1 級 ‧ 2013-10-31 10:33:36 檢舉

謝謝哈哈

0
卡斯
iT邦研究生 1 級 ‧ 2013-11-08 23:38:39

讚

fillano iT邦超人 1 級 ‧ 2013-11-09 07:40:31 檢舉

謝謝 哈哈

我要留言

立即登入留言