一直以來都在用 HTML + Bootstrap 來做 Prototype , 之前是自己用 Native PHP 寫了簡易的 Routing 與 Helper Proxy,直接用檔案結構當作網址階層,作為一個靜態頁面又能簡單支持PHP的系統來說很足夠了,也就這樣用了很久。
單純覺得想玩點新玩具,所以研究了一下有沒有合用的 Micro Framework,Silex 稍嫌厚重了點,所以就選了 Slim 來用。
Slim 教學
萬事第一步
現在多數 Framework 已經支援 Composer 了,我們用 Composer 來載入 Slim。以下是專案根目錄下的 composer.json
{
"require": {
"slim/slim": "2.*"
},
"autoload": {
"psr-0": { "": "src/"}
}
}
然後執行:
$ composer install
Slim 就會被自動載入到 vendor
目錄下面,超簡潔的,沒有任何相依,就是一個 slim
目錄而已。
專案目錄規劃
我希望這個 Prototype 專案可以直接包裹起來,放到任何地方皆可執行,所以我的 .gitignore
不排除 vendor
目錄,且 index.php
直接放在根目錄下。
Project
|
|-- assets // CSS, JS
|
|-- src // 自己寫的物件與 helper 函式庫
|
|-- templates // View 的 pages
|
|-- vendor // 相依函式庫與 Slim 本身
|
|-- index.php
|
|-- composer.json
如果你想用在正式專案,可以將 index.php
與 assets
放在 /www
下面,在用這個目錄當作 Web DocumentRoot,隱藏所有 source。
Project
|
|-- src // 自己寫的物件與 helper 函式庫
|
|-- templates // View 的 pages
|
|-- vendor // 相依函式庫與 Slim 本身
|
|-- www
| |
| |-- css
| |
| |-- js
| |
| |-- index.php
|
|-- composer.json
不過我們現在先以第一種寫法為主。
萬源之源 index.php
現在的 Framework 大多以 index.php 為主要入口頁面,我們依照官方手冊這樣寫:
<?php
// index.php
require 'vendor/autoload.php';
$app = new \Slim\Slim();
$app->get('/', function()
{
echo '<h1>Hello World</h1>';
});
$app->run();
打開網址進到首頁就能印出 Hello World 了。
增加新的 Routes 在 $app->run();
前面:
<?php
// index.php
// ...
// 一般頁面
$app->get('/blog', function()
{
echo '<h1>Here is Blog</h1>';
});
// 動態頁面
$app->get('/blog/:alias', function($alias)
{
echo '<h1>Here is Blog Article: ' . $alias . '</h1>';
});
$app->run();
如此,輸入 index.php/blog
就會出現 Here is Blog
:
輸入 index.php/blog/i-am-foo
就會出現 Here is Blog Article: i-am-foo
, :alias
會自動轉成第一個參數送進函式中。
一切就是這麼的簡單。
使用 Config
依照官方手冊,直接將 array 丟進 Slim
建構子即可當作 config:
$app = new \Slim\Slim(array('debug' => true, 'mode' => 'development'));
我改用另一個 config.php
檔案來儲存,這是啟動除錯模式的設定:
<?php
// config.php
return array(
'debug' => true,
'mode' => 'development'
);
回傳成陣列變數,塞進 \Slim\Slim()
中:
<?php
// index.php
require 'vendor/autoload.php';
$config = require 'config.php';
$app = new \Slim\Slim($config);
// ...
現在隨便 throw 個 Exception,就會出現Debugger:
使用 View 與 Templates
我們在 Config 中加上 templates 的參數,另外建立一個 SLIM_ROOT
常數為根目錄以備不時之需:
<?php
// config.php
define('SLIM_ROOT', __DIR__);
return array(
'debug' => true,
'mode' => 'development',
'templates.path' => __DIR__ . '/templates'
);
然後我們在 templates
下面新增模板檔案:
<!-- templates/html.php -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Slim DEMO</title>
</head>
<body>
<h1>Nick Fury:</h1>
<code>You think you're the only hero in the world?</code>
</body>
</html>
回到 index.php
修改首頁的匿名函式,加上 use($app)
讓我們可以在函式中使用 render:
$app->get('/', function() use($app)
{
$app->render('html.php');
});
成果:
巢狀 Templates
我們可以在 template 中使用 $this->render('xxx')
來呼叫更多子模板區塊:
新增一個 index.php
檔案,並把內文複製過來:
// templates/index.php
<h1>Nick Fury:</h1>
<code>You think you're the only hero in the world?</code>
原本的 html.php
改成這樣:
<!-- templates/html.php -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Slim DEMO</title>
</head>
<body>
<?php echo $this->render('index.php'); ?>
</body>
</html>
結果一模一樣,我們只是重構程式碼而已:
Twig 教學
Twig 是 Symfony 作者參考許多模板引擎所創造出來的新一代模板引擎(其實也不新了),優點是功能非常強大,有完整的資料處理與優良的物件導向結構讓使用者能夠自由覆蓋其功能。傳統 PHP Template 是以父代模板 include 子代模板的方式載入 block,而 Twig 則改以子代繼承(extends)父代模板的方式達到更高的彈性化。
範例
剛剛的 template 改以 Twig 寫會變成這樣,index.twig
會變成入口:
{# templates/index.twig #}
{% extends 'html.twig' %}
{% block body %}
<h1>Nick Fury:</h1>
<code>You think you're the only hero in the world?</code>
{% endblock %}
而 html.twig
是被繼承的:
{# templates/html.twig #}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Slim DEMO</title>
</head>
<body>
{% block body %}
Here will be overrided.
{% endblock %}
</body>
</html>
於是 html.twig
內的 block body 會被 index.twig
覆蓋掉。
載入
詳細的 Twig 標籤教學不在這裡贅述,我們只說明正常情況下,Twig 如何載入,一樣藉由 Composer 來設定:
{
"require": {
"twig/twig": "1.*"
}
}
安裝完成後,直接在專案中呼叫:
<?php
$loader = new Twig_Loader_String();
$twig = new Twig_Environment($loader);
echo $twig->render('Hello {{ name }}!', array('name' => 'Asika'));
即可用字串作為模板輸出。
若要以檔案為模板,則我們改用 Twig_Loader_Filesystem
來當作 loader:
<?php
$loader = new Twig_Loader_Filesystem('/path/to/templates');
$twig = new Twig_Environment($loader);
echo $twig->render('index.html', array('name' => 'Asika'));
由此可知我們其實可以藉由覆蓋或繼承 Loader class 來改變 Twig 的行為,只有在 Twig_Environment
物件建立時,才把 Loader 物件塞進去。這就是相依性注入(Dependency Injection)模式。
簡單的 Twig 用法參考這裡: 新一代 Drupal 樣板引擎 Twig
將 Twig 整合進 Slim 使用
載入 Twig
更改剛剛 Slim 專案下的 composer.json:
{
"require": {
"slim/slim": "2.*",
"twig/twig": "1.*"
},
"autoload": {
"psr-0": { "": "src/"}
}
}
命令列執行
$ composer update
建立我們自己的 View::render()
接著,我們要覆蓋 Slim 內建 View 的行為,由於 PHP Autoload 已經註冊好 /src
下面的目錄了,我們在這裡建立一個 /View/Twig.php
的類別就能快速使用:
<?php
// src/View/Twig.php
namespace View;
use Slim\View as SlimView;
class Twig extends SlimView
{
protected $twig;
/**
* Get Twig Engine.
*/
public function getTwig()
{
if($this->twig)
{
return $this->twig;
}
$loader = new \Twig_Loader_Filesystem($this->getTemplatesDirectory());
$twig = new \Twig_Environment($loader);
return $this->twig = $twig;
}
/**
* Render a template file by Twig
*
* @param string $template The template pathname, relative to the template base directory
*
* @return string The rendered template
*/
public function render($template)
{
$twig = $this->getTwig();
return $twig->render($template, $this->data->all());
}
}
我們完全覆蓋預設的 render()
方法,改用 Twig 來 render 頁面,並且加上 getTwig()
來 Lazyloading。
接著我們要讓 Slim\Slim
這個主要的核心物件改用我們自己的 Twig View,直接在 config.php
設定 view 的物件即可:
<?php
// config.php
define('SLIM_ROOT', __DIR__);
return array(
'debug' => true,
'mode' => 'development',
'view' => new View\Twig(),
'templates.path' => __DIR__ . '/templates'
);
改用 Twig 模板檔案
記得重新建立 index.twig
與 html.twig
兩個檔案,在上面可以找到。
最後我們更改一下 index.php
:
<?php
// index.php
require 'vendor/autoload.php';
$config = require 'config.php';
$app = new \Slim\Slim($config);
$app->get('/', function() use($app)
{
// 改用 index.twig 入口
$app->render('index.twig');
});
$app->run();
最後的成果,理應沒變,我們只是重構程式碼而已:
將專案轉成一個 Prototype 系統
我希望的 Prototype 系統很簡單,只要一個 View ,根據網址來動態抓取不同的 body 的模板檔案,每個模板檔案幾乎都是靜態的,只有重複項目可能用到迴圈,或模板繼承自共用的 HTML 框架。
假設網址是: index.php/blog/articles
,就應該抓取 templates/blog/articles.twig
這個檔案出來。作為一個 Prototype 已經很夠用了。
設置一個全域的 HTML 框架
我們把它放在 templates/ 下面,叫做 _html.twig
,之後所有頁面都繼承它:
{# templates/_html.twig #}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Slim DEMO{% endblock %}</title>
<!-- Bootstrap core CSS -->
<link href='{{ uri.base }}/assets/css/bootstrap.css' rel="stylesheet">
<!-- Custom styles for this template -->
<link href='{{ uri.base }}/assets/css/prototype.css' rel="stylesheet">
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Slim DEMO</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</div>
</div>
</div>
<div id="main-body">
{% block body %}{% endblock %}
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src='{{ uri.base }}/assets/js/jquery.js'></script>
<script src='{{ uri.base }}/assets/js/bootstrap.min.js'></script>
</body>
</html>
變更 index.php 並插入 uri.base 變數
我們會看到 {{ uri.base }}
這樣的標籤,在 Twig 中等同 <?php echo $this->data->uri->base; ?>
。
所以我們要先把這個 uri
建立好,可惜 Slim 沒有全域的 URI 物件讓我們方便處理網址子目錄,所以我們自己寫一下,更改 index.php :
<?php
// index.php
require 'vendor/autoload.php';
$config = require 'config.php';
$app = new \Slim\Slim($config);
// Main Intro
$app->get('/:path+', function($path = array()) use($app)
{
// Get assets path
$baseUri = str_replace($_SERVER['DOCUMENT_ROOT'], '', dirname($_SERVER['SCRIPT_FILENAME']));
$template = implode('/', $path);
$data = array(
'path' => $path,
// 子資料夾的路徑
'uri' => array('base' => $baseUri)
);
$app->render($template . '.twig', $data);
});
$app->run();
這個 uri.base
的目的是為了讓 css, js 的路徑前,可以加上子目錄名稱,假設我把專案放在 http://localhost/slim
下,則 css 的 link 會是:
<link href='{{ uri.base }}/assets/css/bootstrap.css' rel="stylesheet">
<!-- 經過 render 變成 -->
<link href='/slim/assets/css/bootstrap.css' rel="stylesheet">
由 /
開頭可確保網址路徑的 base 被設為根目錄,否則相對路徑就會出錯。
動態 Router
有沒有注意到我們的 Router 是用 /:path+
,+
在 Slim 中代表後面的每個網址區段都會被存成陣列,例如: index.php/blog/articles/12
,則送進匿名函式的變數 $path
會是:
Array
(
[0] => blog
[1] => articles
[2] => 12
)
這也是我選用 Slim 的原因,畢竟 Silex 我找老伴天沒看到這個 Pattern。
加入 CSS, JS
我們採用 Bootstrap 3 ,可以先去官網下載。
然後把他們放進 assets
目錄:
別忘了還有 jQuery 要下載回來。
內頁模板
終於可以開始寫內頁模板了,我們依照網址 index.php/blog/articles
所以寫在 templates/blog/articles.twig
中吧:
{# templates/blog/articles.twig #}
{% extends '_html.twig' %}
{% block title %}Articles{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2">
<div class="row">
<h2>Nick Fury</h2>
<div class="col-xs-4">
<img src="http://placehold.it/200x200" alt="img">
</div>
<div class="col-xs-8">
You think you're the only hero in the world?
</div>
</div>
<div class="row">
<h2>Nick Fury</h2>
<div class="col-xs-4">
<img src="http://placehold.it/200x200" alt="img">
</div>
<div class="col-xs-8">
You think you're the only hero in the world?
</div>
</div>
<div class="row">
<h2>Nick Fury</h2>
<div class="col-xs-4">
<img src="http://placehold.it/200x200" alt="img">
</div>
<div class="col-xs-8">
You think you're the only hero in the world?
</div>
</div>
</div>
</div>
</div>
{% endblock %}
如果以上狀況都沒錯,應該可以看到畫面了:
加上首頁
細心一點的會發現首頁回報 404 ,因為我們現在的 router 不支援 /
,所以再把 index.php 更改一下吧:
<?php
// index.php
require 'vendor/autoload.php';
$config = require 'config.php';
$app = new \Slim\Slim($config);
// Main Intro Closure
$execute = function($path = array()) use($app)
{
// Get assets path
$baseUri = str_replace($_SERVER['DOCUMENT_ROOT'], '', dirname($_SERVER['SCRIPT_FILENAME']));
$template = implode('/', $path);
$template = $template ?: 'index';
$data = array(
'path' => $path,
'uri' => array('base' => $baseUri)
);
$app->render($template . '.twig', $data);
};
// For Home
$app->get('/', $execute);
// For inner pages
$app->get('/:path+', $execute);
$app->run();
善用 Helper\Set 增加自己的函式庫
Slim 的 Helper\Set
其實是 DI Container ,不過我們可以自己延伸出來讓它變得更方便,在 src/DI/Helper
加入一個 class:
<?php
// src/DI/Helper.php
namespace DI;
use Slim\Helper\Set as SlimHelper;
/**
* A helper container
*/
class Helper extends SlimHelper
{
/**
* Get data value with key
*
* @param string $key The data key
* @param mixed $default The value to return if data key does not exist
*
* @return mixed The data value, or the default value
*/
public function get($key, $default = null)
{
if (!$this->has($key) && !$default)
{
$class = 'Helper\\' . ucfirst($key);
$this->singleton($key, function($this) use($class)
{
return new $class;
});
}
return parent::get($key, $default);
}
public function __isset($key)
{
return true;
}
}
現在,這個 Helper
等同一個 Container 物件,只是我們不再需要手動 set 物件進去,他會根據命名規範,自動搜尋 src/Helper
下面的物件,自動抓取出來使用。
現在我們新增一個 helper 在 src/Helper
下面:
<?php
// src/Helper/Lorem.php
namespace Helper;
class Lorem
{
public $lorem = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
public function getLorem($num = 0)
{
if($num)
{
return substr($this->lorem, 0, $num);
}
return $this->lorem;
}
}
然後我們從 index.php
註冊這個 DI\Helper 到 $data
中,這樣就能在 View 中調用:
<?php
// index.php
// ...
$execute = function($path = array()) use($app)
{
// ...
$data = array(
'path' => $path,
'uri' => array('base' => $baseUri),
// Add helper to view
'helper' => new DI\Helper()
);
$app->render($template . '.twig', $data);
};
// ...
接著,更改 articles.twig
:
{# templates/blog/articles.twig #}
{% extends '_html.twig' %}
{% block title %}Articles{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-lg-8 col-lg-offset-2">
<div class="row">
<h2>Nick Fury</h2>
<div class="col-xs-4">
<img src="http://placehold.it/200x200" alt="img">
</div>
<div class="col-xs-8">
{{ helper.lorem.getLorem(250) }}
</div>
</div>
<div class="row">
<h2>Nick Fury</h2>
<div class="col-xs-4">
<img src="http://placehold.it/200x200" alt="img">
</div>
<div class="col-xs-8">
{{ helper.lorem.getLorem(250) }}
</div>
</div>
<div class="row">
<h2>Nick Fury</h2>
<div class="col-xs-4">
<img src="http://placehold.it/200x200" alt="img">
</div>
<div class="col-xs-8">
{{ helper.lorem.getLorem(250) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
結果:
我們完全沒有事先載入或註冊 Lorem
這個產生假文用的 class,但我們的 Helper Container 卻可以自動抓取他,這樣小組成員就能在一個統一的命名規範下,以超高效率進行簡單的 Prototype 開發,並能夠快速平行延伸 Helper 類別。
結語
說真的,這樣搞下去,Slim 已經被我玩的不像 Slim 了,不過這也是它的優點,除了少數的規範外,他非常接近 Native PHP,真正熟悉 PHP 的高手們應該可以玩出更多不同的花樣來。
我把以上的專案放在 Github 上: Slim-Prototype-Tutorial-Demo ,有興趣的人可以自己 clone 下來玩。
後記
過了一陣子之後,我又把整個系統打包成可以直接使用的 composer 專案了,叫做 Vaseman。
直接用 Composer 安裝即可:
$ php composer.phar create-project asika/vaseman [project-dir]
Github: https://github.com/asika32764/vaseman
Packagist: https://packagist.org/packages/asika/vaseman