PHP Slim 教學 - 用 Slim 整合 Twig 打造 Prototype 系統

Written by Simon Asika on

img

一直以來都在用 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 目錄而已。

p2013-10-22-1.jpg

專案目錄規劃

我希望這個 Prototype 專案可以直接包裹起來,放到任何地方皆可執行,所以我的 .gitignore 不排除 vendor 目錄,且 index.php 直接放在根目錄下。

Project
 |
 |-- assets // CSS, JS
 |
 |-- src // 自己寫的物件與 helper 函式庫
 |
 |-- templates // View 的 pages
 |
 |-- vendor // 相依函式庫與 Slim 本身
 |
 |-- index.php
 |
 |-- composer.json

如果你想用在正式專案,可以將 index.phpassets 放在 /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 了。

p2013-10-22-2.jpg

增加新的 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 :

p2013-10-22-3.jpg

輸入 index.php/blog/i-am-foo 就會出現 Here is Blog Article: i-am-foo:alias 會自動轉成第一個參數送進函式中。

p2013-10-22-4.jpg

一切就是這麼的簡單。

使用 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:

p2013-10-22-5.jpg

使用 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');
});

成果:

p2013-10-22-6.jpg

巢狀 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>

結果一模一樣,我們只是重構程式碼而已:

p2013-10-22-6.jpg

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.twightml.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();

最後的成果,理應沒變,我們只是重構程式碼而已:

p2013-10-22-6.jpg

將專案轉成一個 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 被設為根目錄,否則相對路徑就會出錯。

注意,因為這個部落格系統會自動解析 URL 的關係,我的 HTML 中載入 CSS, JS 都用單引號來避開,其實你可以用雙引號。

動態 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 目錄:

p2013-10-22-7.jpg

別忘了還有 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 %}

如果以上狀況都沒錯,應該可以看到畫面了:

p2013-10-22-8.jpg

加上首頁

細心一點的會發現首頁回報 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 %}

結果:

p2013-10-22-10.jpg

我們完全沒有事先載入或註冊 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

Control Tools

WS-logo