Flake 模板引擎

相信使用过 Laravel 框架的都会知道 Laravel 提供的一个简单而又强大的模板引擎 —— Blade。Blade 有两个主要优点是:模板继承 和 区块(记得以前 Qeephp 框架就提出这个概念,奈何作者已经停开发)。虽然 Blade 并不限制在视图中使用 PHP 代码,但作为一个喜欢使用原生 PHP 代码的我,还是受不了,所以自己参考 Laravel 的代码,自己弄了一个 Flake 模板引擎。

项目地址:https://github.com/icodechef/Flake

例如视图被保存在 resources/views 文件夹内。

<!-- 视图被保存在 resources/views/index.php -->
<html>
    <body>
        <h1>Hello, <?php echo $name; ?></h1>
    </body>
</html>

这个视图可以使用以下的代码传递到用户的浏览器:

$flake = new Flake('index', 'resources/views', [
    'name' => 'World!',
]);

$flake->render();

如你所见,`Flake` 构造函数的第一个参数为视图文件;第二个参数为对应到视图文件的位置; 第三个参数是一个能够在视图内取用的数据数组。

向视图传递数据后,可以在视图文件中获取以 `键名` 作为变量名的值。
在下面的例子代码中,视图将可以使用 `$total`,`$fruits` 来取得数据,其值分别为 `100`,`array(‘apple’, ‘banana’)`。

$flake = new Flake('index.php', 'resources/views', [
    'total' => 100,
    'fruits' => ['apple', 'banana'],
]);

$flake->render();

在视图文件中:

<p><?php echo $total; ?></p>
<ul>
<?php foreach ($fruits as $fruit) { ?>
    <li><?php echo $fruit; ?></li>
<?php } ?>
</ul>

 在视图中,可访问 $this 指向 \Pure\Flake 对象来管理和渲染这个视图文件。

所以我们可以使用用视图对象的 `get()` 方法来获取视图数据,使用 `get()` 方法的好处是可以设置数据不存在时的默认值:

<p><?php echo $this->get('total'); ?></p>
<ul>
<?php foreach ($this->get('fruits', []) as $fruit) { ?>
    <li><?php echo $fruit; ?></li>
<?php } ?>
</ul>

视图继承和视图片段

大多数页面应用都有相同的页头和页尾,传统的方法是把相同的页头和页尾存在不同的文件或者函数,然后按顺序加载。

<!-- 页头 header.php -->
<html>
<head>
    <title>title</title>
</head>
<body>
<header>Header</header>

<!-- 页尾 footer.php -->
<footer>footer</footer>
</body>
</html>

<!-- 视图 -->
<?php require('header.php') ?>
<article>
    ......
</article>
<?php require('footer.php') ?>

我们可以看到这种方法将一个 HTML 整体分割为三部分,但这样做,当每一个页面的页头和页尾不同时,我们就需要添加判断或者新建不同的页头和页尾文件。

视图继承和视图片段将这些公共的部分(如页头和页尾)放到一个布局中,渲染内容视图后在合适的地方嵌入到布局中。

定义一个页面布局:

<!-- 视图被保存在 resources/views/layout/main.php -->
<html>
<head>
    <meta charset="utf-8">
    <title>Name: <?php $this->section('title', '视图标题'); ?></title>
</head>
<body>
    <?php $this->section('header') ?>
    <?php $this->content(); ?>
    <footer>我是页脚</footer>
</body>
</html>

在视图模板中使用页面布局:

<!-- 视图被保存在 resources/views/index.php -->
<?php $this->extend('main.php'); // 视图继承 ?>

<!-- 这区域是视图的内容 -->
Hello, <?php echo $name; ?>
<!-- 这区域是视图的内容 -->

<?php $this->def('title', '我是首页'); // 定义一个视图片段 ?>

<?php $this->def('header'); // 开始定义一个视图片段 ?>
<header>我是头部</header>
<?php $this->end(); // 结束定义一个视图片段 ?>

如你所见,在视图模板中,使用 `extend()` 方法继承一个页面布局,方法的参数为页面布局所在的文件。

使用 `section()` 方法在视图中定义一个视图片段,参数为 `片段的名称` 和 `片段的默认内容`,如 `$this->section(‘title’, ‘视图标题’)`、 `$this->section(‘header’)`。

视图片段的内容定义在 `def()` 和 `end()` 之间。如:

<?php $this->def('header'); // 开始定义一个视图片段, 片段的名称为 header ?>
<header>我是头部</header>
<?php $this->end(); // 结束定义一个视图片段 ?>

注意,一般 `def()` 和 `end()` 方法应该成对出现, 但传递第二个参数,则不需要 `end()` 了,否则输出并不是你想要的。

$this->def('title', '这里是一个标题');

// 或者
$this->def('title');
这里是一个标题。
$this->end();

在页面布局(也就是父模板)中使用 `content()` 方法返回继承自其的视图内容。

<div><?php $this->content(); ?></div>

源代码:

<?php

/**
 * Flake - a simple PHP templating engine
 *
 * @package     Pure
 * @author      Deng Man <dengman2010@163.com>
 * @copyright   2018 icodechef
 * @link        http://icodechef.com/
 * @license     http://opensource.org/licenses/MIT  MIT License
 * @version     1.0
 */

class Flake
{
    /**
     * 视图
     *
     * @var string
     */
    protected $view;

    /**
     * 布局视图实例
     *
     * @var \Flake
     */
    protected $layout;

    /**
     * 视图文件的路径
     *
     * @var string
     */
    protected $path;

    /**
     * 视图数据
     *
     * @var array
     */
    protected $data = [];

    /**
     * 已定位的视图数组
     *
     * @var array
     */
    protected $views = [];

    /**
     * 所有视图共享的数据
     *
     * @var array
     */
    protected static $shared = [];

    /**
     * 视图扩展名
     *
     * @var array
     */
    protected $extension = 'php';

    /**
     * 视图片段
     *
     * @var array
     */
    protected $sections = [];

    /**
     * 创建一个视图实例
     *
     * @param  string  $view
     * @param  string  $path
     * @param  array   $data
     * @return void
     */
    public function __construct($view, $path, array $data = [])
    {
        $this->view = $view;
        $this->setPath($path);
        $this->data = (array) $data;
    }

    /**
     * 创建并返回一个视图实例
     *
     * @param  string  $view
     * @param  array   $data
     * @return void
     */
    public function make($view, $mergeData = [])
    {
        $data = array_merge($this->data, $mergeData);
        return new self($view, $this->path, $data);
    }

    /**
     * 获取视图的内容
     *
     * @param  callable|null  $callback
     * @return string
     */
    public function render(callable $callback = null)
    {
        try {
            $contents = $this->renderContents();
            $response = isset($callback) ? call_user_func($callback, $this, $contents) : null;
            return ! is_null($response) ? $response : $contents;
        } catch (\Exception $e) {
            throw $e;
        }
    }

    /**
     * 获取视图实例的内容
     *
     * @return string
     */
    protected function renderContents()
    {
        $path = $this->find($this->view);
        $contents = $this->fetch($path, array_merge(static::$shared, $this->data));
        return $contents;
    }

    /**
     * 获取视图的内容
     *
     * @param  string  $path
     * @param  array   $data
     * @return string
     */
    public function fetch($path, array $data = [])
    {
        return $this->evaluatePath($path, $data);
    }

    /**
     * 获取给定路径中视图的内容
     *
     * @param  string  $__path
     * @param  array   $__data
     * @return string
     */
    protected function evaluatePath($__path, $__data)
    {
        ob_start();
        extract($__data, EXTR_SKIP);

        try {
            include $__path;
        } catch (\Exception $e) {
            throw $e;
        }

        $contents = trim(ob_get_clean());

        if ($this->layout) { // 视图布局
            $this->layout->sections = array_merge($this->sections, array('content' => $contents));
            $contents = $this->layout->render();
        }

        return $contents;
    }

    /**
     * 添加共享数据
     *
     * @param  array|string  $key
     * @param  mixed  $value
     * @return mixed
     */
    public static function share($key, $value = null)
    {
        $keys = is_array($key) ? $key : [$key => $value];

        foreach ($keys as $key => $value) {
            static::$shared[$key] = $value;
        }

        return $value;
    }

    /**
     * 向视图添加数据
     *
     * @param  string|array  $key
     * @param  mixed   $value
     * @return $this
     */
    public function set($key, $value = null)
    {
        if (is_array($key)) {
            $this->data = array_merge($this->data, $key);
        } else {
            $this->data[$key] = $value;
        }

        return $this;
    }

    /**
     * 获取视图数据
     *
     * @param  string $key
     * @param  mixed $default
     * @param  mixed $filters
     * @return mixed
     */
    public function get($key, $default = null)
    {
        if (isset($this->data[$key])) {
            $value = $this->data[$key];
        } else if (isset(static::$shared[$key])) {
            $value = static::$shared[$key];
        } else {
            $value = $default;
        }

        return $value;
    }

    /**
     * 转义字符串
     *
     * @param  string $value
     * @param  boolean $doubleEncode
     * @return string
     */
    public function escape($value, $doubleEncode = true)
    {
        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', $doubleEncode);
    }

    /**
     * 过滤
     *
     * @param  mixed $string
     * @param  null|string|callback $rules
     * @return mixed
     */
    public function sanitize($value, $rules = null)
    {
        if (is_string($rules)) {
            foreach (explode('|', $rules) as $rule) {
                $parameters = [];
                if (strpos($rule, ':') !== false) {
                    // substr:0, 10
                    list($callback, $parameter) = explode(':', $rule, 2);
                    $parameters = str_getcsv($parameter);
                } else {
                    $callback = $rule;
                }
                array_unshift($parameters, $value);
                $value = call_user_func_array($callback, $parameters);
            }
        } else if (is_callable($rules)) {
            $value = call_user_func($rules, $value);
        }

        return $value;
    }

    /**
     * 将视图实例添加到视图数据
     *
     * @param  string  $key
     * @param  string  $view
     * @param  array   $data
     * @return $this
     */
    public function nest($key, $view, array $data = [])
    {
        return $this->set($key, $this->make($view, $data));
    }

    /**
     * 设置视图的路径
     *
     * @param  string  $path
     * @return void
     */
    public function setPath($path)
    {
        if (! is_dir($path)) {
            throw new \Exception(sprintf('The "%s" directory does not exist.', $path));
        }

        $this->path = rtrim($path, '/\\');
    }

    /**
     * 获取视图文件的路径
     *
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * 注册扩展名
     *
     * @param  string  $extension
     * @return void
     */
    public function setExtension($extension)
    {
        $this->extension = (string) $extension;
    }

    /**
     * 获取扩展名
     *
     * @param  string  $extension
     * @return void
     */
    public function getExtension()
    {
        return $this->extension;
    }

    /**
     * 获取视图的位置
     *
     * @param  string  $name
     * @return string
     */
    public function find($name)
    {
        if (isset($this->views[$name])) {
            return $this->views[$name];
        }

        return $this->views[$name] = $this->findInPath($name);
    }

    /**
     * 查找视图的完整位置
     *
     * @param  string  $name
     * @return string
     */
    protected function findInPath($name)
    {
        $viewPath = $this->path.'/'.$name.'.'.$this->extension;

        if (file_exists($viewPath)) {
            return $viewPath;
        }

        throw new \Exception("View [{$name}] not found.");
    }

    /**
     * 确定给定视图是否存在
     *
     * @param  string  $view
     * @return bool
     */
    public function exists($view)
    {
        try {
            $this->find($view);
        } catch (\Exception $e) {
            return false;
        }

        return true;
    }

    /**
     * 设置视图布局
     *
     * @param  string $layout
     * @param  array  $data
     * @return void
     */
    protected function extend($layout, array $data = [])
    {
        $this->layout = $this->make($layout, $data);
    }

    /**
     * 返回视图布局的内容片段
     *
     * @return string
     */
    public function content()
    {
        return isset($this->sections['content']) ? $this->sections['content'] : '';
    }

    /**
     * 开始定义一个视图片段
     *
     * * 一般 def() 与 end() 成对出现, 但传递第二个参数, 则不需要 end()
     *
     * @param  string $name
     * @param  string $content
     * @return null
     */
    protected function def($name, $content = '')
    {
        if ($name === 'content') {
            throw new \Exception('The section name "content" is reserved.');
        }

        $this->sections[$name] = $content;

        if (! $content) {
            ob_start();
        }
    }

    /**
     * 结束定义一个视图片段
     *
     * @return void
     */
    protected function end()
    {
        if (empty($this->sections)) {
            throw new \Exception('You must start a section before you can stop it.');
        }

        end($this->sections);

        $this->sections[key($this->sections)] = ob_get_clean();
    }

    /**
     * 返回视图片断的内容
     *
     * @param  string      $name    视图片断名
     * @param  string      $default 默认的内容
     * @return string|null
     */
    protected function section($name, $default = null)
    {
        if (! isset($this->sections[$name])) {
            return $default;
        }

        return $this->sections[$name];
    }

    /**
     * 检查视图片段是否存在
     *
     * @param  string  $name
     * @return bool
     */
    public function hasSection($name)
    {
        return array_key_exists($name, $this->sections);
    }

    /**
     * 获取视图的字符串内容
     *
     * @return string
     */
    public function __toString()
    {
        return $this->render();
    }
}

使用说明:

待续

发表评论

电子邮件地址不会被公开。 必填项已用*标注