0%

[技能檢定]網頁乙級檢定-前置作業-程式功能整合測試-進階

在做完必要的檢查、整理和基本測試後,如果還有時間,可以針對程式運行做整合測試,基本上就是針對PHP+MySQL的運作做整合性的測試,避免發生資料庫連不上或是編碼設錯,時區沒設定等狀況

撰寫共用函式檔

共用函式的目的在於簡化CRUD的動作,同時減少撰寫SQL語法時的錯誤,最後則是為了讓除錯過程可以變得簡單一些。

這邊我們建議採用物件導向的方式來簡化自訂函式的撰寫,但是考量到檢定時間的限制,所以並不是全面的採用物件導向,只是把常用的自訂函式包裝成一個工具類別(Class)來使用而已,

宣告類別

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class DB{

/**
* $dsn 用來作為PDO的資料庫設定dbname為使用的資料庫名稱
* $table 使用的資料表名
* $pdo PDO的物件變數
* $links 用來存儲分頁功能的各項參數數值,此變數需執行過
*     類別內部的paginate()方法後才會有值
* */
protected $dsn='mysql:host=localhost;charset=utf8;dbname=db13';
protected $table;
protected $pdo;
protected $links;

/**
* 建立建構式,在建構時帶入table名稱會建立資料庫的連線
* 建構式為物件被實例化(new DB)時會先執行的方法
*/
function __construct($table)
{
//將物件內部的$table值設為帶入的$table
$this->table=$table;

//將物件內部的$pdo值設為PDO建立的資料庫連線物件
$this->pdo=new PDO($this->dsn,'root','');
}
}

撰寫內部共用方法

由於CRUD的SQL語法中有許多是類似的,比如where 後的句型;而我們希望能透過一些設計方式讓我們在建立SQL語法時可以更簡便,因此我們將一些接近的用法獨立出來做成一個可以被呼叫引用的方法,藉此來降低程式碼的重覆,提高可讀性、維護及擴充。
a2s()-內部保護函式,用來簡化陣列參數的字串轉換

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** 
* 此方法僅供類別內部使用,外部無法呼叫
* 帶入的參數必須為key-value型態的陣列
* 陣列透過foreach轉化為`key`='value'的字串存入陣列中
* 回傳此字串陣列供其他方法使用
* */
protected function a2s($array){
foreach($array as $key => $value){

//如果陣列的key名有id的,則跳過不處理
if($key!='id'){
$tmp[]="`$key`='$value'";
}
}
return $tmp;
}

sql_id()-用來組合有含id的陣列資料為sql語句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** 
* 此方法僅供類別內部使用,外部無法呼叫
* $sql 一個sql的字串,主要是where 前的語法
* $arg 可能是id值或一個陣列,陣列的話會是key-value的型式
* 此方法用來建立一個包含where中會有 `id`='$arg'型式的sql語句
* 如果$arg不是id則會把陣列做成where後的條件語句
**/
protected function sql_one($sql,$arg){
if(is_array($arg)){
$tmp=$this->a2s($arg);
$sql=$sql." where ". join(" && ",$tmp);
}else{
$sql=$sql. " where `id`='$arg'";
}
return $sql;
}

sql_all()-用來組合有特定條件並且為多筆結果的sql語句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/** 
* 此方法僅供類別內部使用,外部無法呼叫
* $sql 一個sql的字串,主要是where 前的語法
* ...$arg 使用不定參數,表示參數可能不只一個
* 根據不同的參數內容,會組合出適合的sql語句來回傳
**/
protected function sql_all($sql,...$arg){
if(!empty($arg)){

//如果第一個參數存在的話
if(isset($arg[0])){

//如果第一個參數是陣列,則依欄位條件組成sql語句
if(is_array($arg[0])){
$tmp=$this->a2s($arg[0]);

$sql=$sql . " where ".join(" && ",$tmp);
}else{
//如果第一個參數不是陣列,則視同字串和$sql串在一起
$sql=$sql .$arg[0];
}
}

//如果第二個參數存在的話
if(isset($arg[1])){
//將第二個參數則視同字串和$sql串在一起
$sql=$sql. $arg[1];
}
}
//回傳組合好的sql語句
return $sql;
}

math()-內部函式,用來做為聚合函式的sql語法轉換
雖然也可以直接使用,但是直接使用的話需要輸入較多的參數,所以我們利用類別內可以互相呼的方式來簡化參數的使用,對外則是以max(‘欄位’,’條件’)來使用,而不是較多參數的math(‘max’,’欄位’,’條件’)。

1
2
3
4
5
6
7
protected function math($math,$col,...$arg){
$sql="select $math($col) from $this->table ";
$sql=$this->sql_all($sql,...$arg);

//因為這類方法大多是只會回傳一個值,所以使用fetchColumn()的方式來回傳
return $this->pdo->query($sql)->fetchColumn();
}

撰寫外部公開方法(資料庫相關功能)

外部公開方法是在類別實例化成物件後,提供外部來呼叫用的,可以根據自己的專案需要來加上各式便利的功能

$table->all()-查詢符合條件的全部資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 
* 此方法主要用來取得符合條件的所有資料
* $arg 可能是一個key-value的陣列,代表要撈取的資料欄位條件是什麼
* 可能是一個sql字串
* 可能是一個陣列加一個字串
* 可能是兩個字串
**/
function all(...$arg){
//建立一個基礎語法字串
$sql="select * from $this->table ";

//將語法字串及參數帶入到類別內部的sql_all()方法中,結果會得到一個完整的SQL句子
$sql=$this->sql_all($sql,...$arg);

//將sql句子帶進pdo的query方法中,並以fetchAll的方式回傳所有的結果
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}

$table->find($id)-查詢符合條件的單筆資料

1
2
3
4
5
6
7
8
9
10
function find($arg){
//建立一個基礎語法字串
$sql="select * from $this->table ";

//將語法字串及參數帶入到類別內部的sql_id()方法中,結果會得到一個完整的SQL句子
$sql=$this->sql_id($sql,$arg);

//將sql句子帶進pdo的query方法中,並以fetch的方式回傳一筆資料結果
return $this->pdo->query($sql)->fetch(PDO::FETCH_ASSOC);
}

$table->del($id)-刪除資料

1
2
3
4
5
6
7
8
9
10
function del($arg){
//建立一個基礎語法字串
$sql="delete from $this->table ";

//將語法字串及參數帶入到類別內部的sql_id()方法中,結果會得到一個完整的SQL句子
$sql=$this->sql_id($sql,$arg);

//將sql句子帶進pdo的exec方法中,回傳的結果是影響了幾筆資料
return $this->pdo->exec($sql);
}

$table->save($array)-新增/更新資料

利用新增和更新語法的特點整合兩個動作為一個,簡化函式的數量並提高函式的通用性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* $arg 必須是個陣列,但考量速度,所以程式中沒有特別檢查是否為陣列,
* 依據$arg是否帶有'id'這個key名,來決定是更新(有id)還是新增(沒id)
*/
function save($arg){
if(isset($arg['id'])){
//建立更新的sql語法
$sql="update $this->table set ";

//透過a2s()方法把陣列轉成sql字串陣列
$tmp=$this->a2s($arg);

//根據sql語法需要,把字串陣列組合成以 **,** 分隔的字串
$sql=$sql . join(",",$tmp);

//加上id值做為條件
$sql=$sql . " where `id`='{$arg['id']}'";
}else{
//利用php內建的array_keys()函式把陣列的key名獨立成為一個陣列
$key=array_keys($arg);

//建立新增用的sql語句
$sql="insert into $this->table (`".join("`,`",$key)."`) value('".join("','",$arg)."')";
}

//將sql句子帶進pdo的exec方法中,回傳的結果是影響了幾筆資料
return $this->pdo->exec($sql);
}

sum()/max()/min()…-使用聚合函式來計算某個欄位的結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 直接呼叫內部的方法math(),帶入需要的參數即可
* 這樣設計的目的是為了讓外部呼叫時方法名稱比較直覺,
* 同時也減少需要帶入的參數
*/
function max($col,...$arg){
return $this->math('max',$col,...$arg);
}

function min($col,...$arg){
return $this->math('min',$col,...$arg);
}

function sum($col,...$arg){
return $this->math('sum',$col,...$arg);
}

撰寫外部公開方法(畫面相關功能)

由於乙級的題目中有不少的功能是需要把資料呈現在畫面上,為了簡化頁面的程式撰寫,所以我們把這一類的功能抽像出來放到類別中。
view($url,$arg=[])-引入頁面模版

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 此方法是簡化include()函式,但在include前會先執行一次extract($array)函式
* extract()函式可以把key-value的陣列解開為$key=$value,解開後的變數可以直
* 接在頁面模版中使用,藉此可以簡化參數的傳遞及使用。
*/
function view($path,$arg=[]){
//將陣列內容解開為數個變數
extract($arg);

//引入指定路徑的檔案
include($path);
}

paginate($num,$arg=null)-根據分頁的資料筆數來回傳一頁中的資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* $num 單一頁面中的最大筆數
* $arg 如果有此參數,則視為是SQL語法中的條件式,可能是陣列也可能是字串
*/
function paginate($num,$arg=null){
$total=$this->count($arg); //計算符合條件的資料總筆數
$pages=ceil($total/$num); //根據每頁的資料筆數來計算總頁數
$now=$_GET['p']??1; //根據網址是否帶有p參數來決定目前的頁碼
$start=($now-1)*$num; //計算需要的資料起點

//利用內部的all方法來取得資料,其中最後的參數為sql 中的limit語法
$rows=$this->all($arg," limit $start,$num");

//完成資料取得後,將分頁資訊寫入到類別中的links成員,方便換頁連結使用
$this->links=[
'total'=>$total,
'pages'=>$pages,
'now'=>$now,
'start'=>$start,
'table'=>$this->table
];

//回傳分頁需要的資料陣列
return $rows;
}

links()-根據分頁方法的資料,回傳分頁連結的html程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 此方法一定要在paginate()執行後才會有效,
* 因為paginate()執行後$this->links才會有值
* 回傳的內容為html的字串
*/
function links(){
$html=''; //先宣告一個空字串;

//判斷是否有上一頁,有的話則建立起上一頁的連結html碼
if($this->links['now']-1>=1){
$prev=$this->links['now']-1;
$html.="<a href='?do=$this->table&p=$prev'> &lt; </a>";
}

//使用回圈印出每一頁的連結html碼
for($i=1;$i<=$this->links['pages'];$i++){

//判斷是否為當前頁,當前頁則字型放大
$size=($this->links['now']==$i)?'24px':'18px';
$html.="<a href='?do=$this->table&p=$i' style='font-size:$size'> $i </a>";
}

//判斷是否有下一頁,有的話則建立起下一頁的連結html碼
if($this->links['now']+1<=$this->links['pages']){
$next=$this->links['now']+1;
$html.="<a href='?do=$this->table&p=$next'> &gt; </a>";
}

return $html;
}

撰寫輔助用的全域函式

有些功能不必然都要放到類別中,我們可以宣告在共用的引入檔中,做為全域隨時可以呼叫的工具函式

q($sql)-複雜SQL語法的簡化函式

1
2
3
4
5
6
//用來解決聯表查詢或是子查詢之類較為複雜的語法
public function q($sql){
$dsn="mysql:host=localhost;charset=utf8;dbname=db01";
$pdo=new PDO($dsn,'root','');
return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}

也可以在DB class中建一個q()函式來使用
DB.php

1
2
3
4
5
6
7
class DB{
.....

function q($sql){
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
}

$to($url)-頁面導向輔助函式

此函式會獨立在 DB 這個類別外,但是會和共用檔放在一起,然後include到所有的頁面去使用,主要目的是簡化header指令的語法,避免拚字錯誤之類的事發生。

1
2
3
function to($url){
header("location:".$url);
}

dd()-PHP陣列查看

1
2
3
4
5
6
//方便在網頁查上以比較美觀的方式來查看陣列的內容
function dd($array){
echo "<pre>";
print_r($array);
echo "</pre>";
}

時區設定

有些題組會使用到時間,直接修改apache設定檔或php.ini都可以,但如果是一般坊間的server,可能沒有提供使用者去更改全域設定的功能,此時可以簡單的加上一個動態時區設定的語法,讓我們的程式在執行期間可以使用我們自行設定的時區:

1
date_default_timezone_set("Asia/Taipei");

啟用session

有很多功能需要透過session來暫存狀態,因此我們可以在共用檔中先啟月session,方便在各個頁面都可以操作session。

1
session_start();

預宣告資料表變數

由於共用函式檔會include到所有的頁面去使用,如果每次要使用時才去做 new DB('table') 會有點囉嗦,因此可以考慮把常用或會用到的資料表都先建立一個 DB 的實體出來,當成全域變數先宣告,之後在各個頁面就可以直接套用。

1
2
3
4
//建議使用首字母大寫來代表這是資料表的變數,方便和全小寫的變數做出區隔
$User=new DB('user');
$Menu=new DB('menu');
//etc......

也可以使用繼承的方式來建立一個特定的資料表類別,並預先建立相關的成員或函式,如此也可以簡化宣告物件和後續的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
include_once "DB.php";

class User extends DB{

function __construct()
{
//要求執行父類別中的建構式,並帶入資料表名
parent::__construct('user');
}
}

class Menu extends DB{

function __construct()
{
parent::__construct('menu');
}
}

//宣告物件變數時,因為繼承的類別中已經有執行了父類別的建構式了,
//所以不用再帶入資料表的名稱
$User=new User;
$Menu=new Menu;

測試CRUD正常運作

  • 建一個資料表
  • 寫一個簡單的SQl語法來測試CRUD都正常

測試前端JS及jQuery運作正常

  • 檢查jQuery的引入是否正常運作