前一篇文章說到了 Zephir 於是這篇就要來研究一下摟~
關於這篇文章,會做以下幾件事情:
- 安裝 & 設定
- 寫一個簡易的 Router
- 改寫成 Zephir 版本
- 安裝 Extension 以及測試
那麼,廢話不多說,馬上開始吧!
因為我習慣使用 Mac 所以是用 Mac 的方式安裝,不過 Zephir 並沒有表明支援 Mac 使用上需要多加小心。 如果你懶得安裝或者希望使用 Linux 環境,可以考慮使用 阿土伯 大大在 PHPConf 2013 時 Demo 用的 Vagrant Box - Phalcon Dev Box 裡面已經配置好 Zephir 可以直接使用。
安裝
首先,我們要安裝相依套件。
brew install re2c brew install json-c
不過 re2c 似乎在安裝 XCode 時已經有了,而且我系統內的版本還比 Homebrew 的還新(基本上跳出有安裝過之類的訊息就不用再次安裝了⋯⋯)
因為 Zephir 似乎沒有單一個執行檔的功能,因此我個人是習慣放到 ~/.tools
資料夾裡面,與一般工具區分開來。
cd ~/.tools git clone https://Github.com/phalcon/zephir cd zephir
因為 Mac 中沒有 /opt/local/lib 這個目錄,我們要編輯 install 這個檔案。
把 -L/opt/local/lib
這段刪除(讓編譯時不要在這個目錄找相關的 Library)
修改後的 install 裡面的 gcc 指令大概會長這樣
1gcc -Wl,-rpath /usr/local/lib -I/usr/local/lib -L/usr/local/lib -g3 parser.c scanner.c -ljson -ljson-c -o ../bin/zephir-parser
之後執行 install 即可
./install
接著可以修改 ~/.bashrc
或者相關檔案,加入以下這行
1export PATH=$PATH:~/.tools/zephir/bin
重開 Terminal 或者輸入 source ~/.bashrc
之後,就可以直接使用 zephir 指令了!
寫一個簡單的 Router
為了可以觀察到改變,我們先來用 PHP 做一個簡單的 Router 測試。
基本上就是會將 https://localhost/myApp/index
轉成 $controller = new MyApp(); $controller->index()
的語法。
這邊基本上就不多敘述實作,以下是這次範例用的 Router 原始碼。
1<?PHP
2
3namespace MyRouter;
4
5class Router {
6 protected $basePath = "";
7 protected $currentPath = "";
8 protected $defaultMethod = "";
9 public $notFound = null;
10
11 public function __construct($basePath = "") {
12 $this->basePath = $basePath;
13 $this->defaultMethod = "index";
14 }
15
16 /**
17 * Dispatch
18 *
19 * Create class instance and call method
20 */
21 public function dispatch()
22 {
23 $parseURI = $this->parseURI();
24 if(!empty($this->basePath) && $this->basePath == $parseURI[0]) {
25 $parseURI = array_slice($parseURI, 1);
26 }
27
28 $class = null;
29 $method = null;
30 if(isset($parseURI[0])) {
31 $class = $parseURI[0];
32 $class = ucwords($class);
33 }
34 if(isset($parseURI[1])) {
35 $method = $parseURI[1];
36 }
37
38 if(is_null($class) || !class_exists($class)) {
39 $this->error(404);
40 return;
41 }
42
43 $classInstance = new $class;
44
45 if(is_null($method)) {
46 $this->callMethod($classInstance, $this->defaultMethod);
47 return;
48 }
49
50 $this->callMethod($classInstance, $method);
51
52 }
53
54 /**
55 * Call Method
56 *
57 * @param object $class
58 * @param string $method
59 */
60
61 private function callMethod($class, $method) {
62 if(is_callable(array($class, $method))) {
63 call_user_func(array($class, $method));
64 } else {
65 $this->error(404);
66 }
67 }
68
69 /**
70 * Error
71 *
72 * @param int $code
73 */
74
75 public function error($code = 500) {
76 if($code == 404) {
77 if(is_callable($this->notFound)) {
78 call_user_func($this->notFound);
79 } else {
80 echo "404 Not Found";
81 }
82 } else {
83 echo "Error {$code}";
84 }
85 }
86
87 /**
88 * Parse URI
89 *
90 * Analytic URL and turn into class and method
91 *
92 * return array [$class, $method]
93 */
94
95 protected function parseURI()
96 {
97 $currentURI = $_SERVER['REQUEST_URI'];
98 $pattern = "/\/([a-z][a-z0-9-]*)/i";
99
100 $matches = array();
101 preg_match_all($pattern, $currentURI, $matches);
102
103 return array_slice($matches, 1)[0];
104 }
105}
使用方式(index.PHP 為例)
1<?PHP
2
3require("Router.PHP");
4// require some class for dispath
5
6$router = new MyRouter\Router();
7$router->dispatch();
另外還需要一個 Rewrite Rule 來轉換(這邊使用 Laravel 的 .htaccess)
<IFModule mod_rewrite.c>
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.PHP [L]
</IfModule>
基本上會運作後,就來改寫成 Zephir 的版本吧!
改寫為 Zephir 版本
雖然基本上都和 PHP 類似,不過仍有不少 Syntax 上的差異,建議大家先稍微讀過(不會很難,比新學語言簡單多了!)
一開始要初始化專案,我先建立一個 myExtension 放置我的 PHP Extension 專案,然後這邊建立一個 MyRouter 的專案。
mkdir -p ~/myExtension/MyRouter cd ~/myExtension/MyRouter zephir init
完成後,目錄下應該會多出一些檔案。
- config.json - 設定檔,裡面應該會寫著 namepsace 是什麼
- ext/ - 最後生成的 PHP Extension 會在這個資料夾找到
- myrouter/ - 放置 .zep 檔案的目錄(類似 PSR-0 的 Class 目錄規則,不過都是小寫)
接著把剛剛的 Router.PHP 複製進去,重新命名為 router.zep 然後著手改寫。
以下有不少地雷(我找 Syntax, Compile Error 超久,都用註解說明)
namespace MyRouter;
class Router {
// 單純去掉 $ 即可(雖然有 $ 似乎不會被當成錯誤)
protected basePath = "";
protected currentPath = "";
protected defaultMethod = "";
public notFound = null;
public function __construct(string basePath = null) {
let this->basePath = basePath; // 所有 Assign 的動作都要用 let
let this->defaultMethod = "index";
}
/**
* Dispatch
*
* Create class instance and call method
*/
public function dispatch()
{
var parseURI; // 像是 Array 之類的都分類在 Dynamic Variable 裡面,用 var 賦值(分不清楚用 var 比較保險)
let parseURI = []; // 做這個動作讓他判定為 Array (官方 Blog 有另一種做法,沒測試過)
let parseURI = this->parseURI(); // 從 parseURI 方法取得分析好的 Array
if !empty(this->basePath) && this->basePath == parseURI[0] {
let parseURI = array_slice(parseURI, 1); // 所有 PHP 的 Function / Class 基本上都可以直接取用
}
var className;
var method;
var value; // 下面會用 fetch 來替代 isset (官方部落格表示必須這樣用) 所以要先定義變數給他用
let className = null; // 給予初始值,如果沒有做這個動作後面的 is_null() 檢查都會讓變成 PHP Process 直接死掉
// --- 以下為推測,因為碰到問題點在這,但是詳細關係有待釐清 ---
// 關於這部分 阿土伯大大 也給出解釋,因為底層還是 C 所以離開 Scope 記憶體清除,就會 segfault
// 關於 Segmentation fault (segfault) 因為沒有碰過,所以暫時先搜集資料,代理解後在分享詳細的資訊
let method = null;
if fetch value, parseURI[0] { // 這個寫法跟 isset 效果相同,這邊比較不一樣
let className = value;
let className = ucwords(className);
}
if fetch value, parseURI[1] {
let method = parseURI[1];
}
if is_null(className) || !class_exists(className) {
this->error(404);
return;
}
/**
* 這邊是因為 PHP 可以用 new $someClass; 的方式產生新物件(物件名稱用變數代替)
* 但是 Zephir 會把你的變數當成 Class Name 所以無法正常產生,那麼就藉由 class_alias 方法處理
* class_alias 要傳入兩個參數(字串)第一個是原始類別,第二個是他的匿名類別名稱
* 所以透過這個方法,所有 Router 傳入的 Class Name 都被轉成統一的 ProxyClass 來產生實例使用
*/
// class_alias(className, "ProxyClass"); // 注意,字串一律使用雙引號,單引號被視為 char
var classInstance;
//let classInstance = new ProxyClass;
var classInstance = create_instance(className); // 阿土伯大大提供了正確的用法,也不會被編譯器警告了!
// create_instance_params(className, params) 是有參數的用法
if method == null {
this->callMethod(classInstance, this->defaultMethod);
return;
}
this->callMethod(classInstance, method);
}
/**
* Call Method
*
* @param object $class
* @param string $method
*/
private function callMethod(var instance, string method) {
if is_callable([instance, method]) { // 用 [] 直接產生 Array 是被接受的(也許是跟進 PHP 5.4 的新功能)
call_user_func([instance, method]);
} else {
this->error(404);
}
}
/**
* Error
*
* @param int $code
*/
public function error(int code = 500) {
if(code == 404) {
if(is_callable(this->notFound)) {
call_user_func(this->notFound);
} else {
echo "404 Not Found";
}
} else {
echo "Error {code}";
}
}
/**
* Parse URI
*
* Analytic URL and turn into class and method
*
* return array [$class, $method]
*/
protected function parseURI()
{
var currentURI;
let currentURI = _SERVER["REQUEST_URI"];
var pattern = "@/([a-z][a-z0-9-]*)@i"; // 這邊用 \/ 去脫跳不知道為什麼會出錯,只好改用其他界定符號
var matches;
let matches = [];
preg_match_all(pattern, currentURI, matches);
var schema;
let schema = array_slice(matches, 1);
return schema[0];
}
}
完成之後,在目錄下執行指令
zephir [compile]
compile 可以省略,但是我似乎找不到 help 的 command 來看支援什麼,目前已知 init 和 compile 兩個(遠望)
安裝與測試
因為我平常使用 PHPBrew 來建構 PHP 的測試環境,因此以下範例是複製到 PHPBrew 的 Extension 目錄。
sudo cp ext/modules/myrouter.so ~/path/to/your/PHP/extensions/ PHPbrew ext enable myrouter sudo apachectl graceful
PHPBrew 提供很方便的 command 來啓用 extension 基本上就是到 PHP.ini 加上 extension=myrouter.so
就能使用了!
最後,我們重新修改 index.PHP
來改用 Extension
1<?PHP
2$router = new MyRouter\Router();
3$router->dispatch();
重新打開後,如果正常運作就是成功了!
註:上文都沒有產生任何 Router 可以讀取到的 Class 大家可以自己嘗試加入,以下為範例程式碼
https://localhost/app/home
1<?PHP
2
3class App {
4 public function index() {
5 echo "Hello, this index";
6 }
7
8 public function home() {
9 echo "Hello, you should see this when open /app/home";
10 }
11}
12
13$router = new MyRouter\Router();
14$router->dispatch();
那麼,用 Zephir 可以做些什麼呢?
- 改善原本程式的效能瓶頸(可能是在無法改變語言的狀況下)
- 商業使用(加密程式碼)
- 在極端的環境下使用(記憶體不足之類的情況)
- 純粹好玩
- 開發一套大型 PHP 框架,嘗試改變些什麼 ( Phalcon )
- 其它也許你能想到的⋯⋯
至少我認為 PHP 藉此開拓了一條新道路,而 Phaclon 團隊的 Zephir 我想一定能夠改變很多東西。
目前我推薦他人學習 PHP 的理由,我會用這兩個:
- 容易取得運行環境、較少權限需求問題
- 容易入門學習,建構成就感
多了 Zephir 我想還可以加上一個「簡單寫超高速 PHP 網站」之類的吧 XDD (寫許用來寫 PHP Rebot 非常好用啊,高效的機器人,用來分析資料是非常方便的!)