【PHP开发那些事儿】1.3 MVC内核-View

1.3 MVC内核-View

视图层的实现,主要是一个HTML模板解析。

PHP模板有好些,可以百度一下,这里就不讲了。

为了讲好MVC这个事,我们实现一个简单的模板解析。

视图主要由布局Layout和模板TPL组成,在HtmlView类里面会创建Layout和Tpl的实例。

Layout 主要是实现布局功能,在模板中可以extend(继承)一个布局。布局主要由Block(区块)组成。

在模板文件中 调用 beginBlock\endBlock 这两个方法定义好一个区块。

Layout.php

<?php
namespace Lubed\MVCKernel\Views;

use Lubed\Http\Uri;
use Lubed\MVCKernel\Utils\URL;
use Lubed\Utils\Config;

class Layout
{
    protected $tpl           = null;
    protected $name       = '';
    private $blocks        = [];

    private $curLevel      = 0;
    private $blockQueue    = [];
    private $blockLevelMap = [];

    private $blockPre      = '%%BLOCK__';
    private $blockSuf      = '__BLOCK%%';

    public function __construct(Tpl $tpl,string $name)
    {
        //赋值模板
        $this->tpl=$tpl;
        //布局名称
        $this->name=$name;
        //区块解析
        $this->blocksParser=function ($tpl,$layout,array $blocks_level_map,array $all_blocks) {
            if (!$blocks_level_map||!$all_blocks) {
                return $tpl;
            }
            $len=count($blocks_level_map);
            //按区块层级 循环处理
            for ($i=0; $i < $len; $i++) {
                if (!isset($blocks_level_map[$i])) {
                    continue;
                }
                $level = $blocks_level_map[$i];
                if (!$level) {
                    continue;
                }
                $result=[];
                //循环处理 指定层级中的 区块,并替换更新模板解析结果
                foreach ($level as $blockName) {
                    if (!isset($all_blocks[$blockName])) {
                        continue;
                    }
                    //获取完整区块名称
                    $key=$layout->getFullBlockName($blockName);
                    //获取指定区块内容
                    $val=$all_blocks[$blockName];
                    $result[$key]=$val;
                    unset($all_blocks[$blockName]);
                }
                if (!empty($result)) {
                    $tpl= str_replace(array_keys($result), array_values($result), $tpl);
                }
            }
            return $tpl;
        };
    }

    //渲染视图模板
    public function render()
    {
        //开启缓存
        ob_start();
        //载入模板
        $this->tpl->load($this->name);
        //获取缓存内容
        $html = ob_get_clean();
        //解析区块并返回
        return ($this->blocksParser)($html,$this,$this->blockLevelMap,$this->blocks);
    }

    //继承布局
    public function extend(string $name)
    {
        //加载布局模板文件
        $this->tpl->load($name);
    }

    //加载模板
    public function load(string $name)
    {
        //加载布局模板文件
        $this->tpl->load($name);
    }

    //指定的区块开始定义
    public function beginBlock($blockName)
    {
        $blockName  = strtoupper($blockName);
        $parentBlock = $this->getCurrentBlock();
        if($parentBlock == $blockName){
            ViewExceptions::invalidBlock('子block与父block不允许重名,block名称:'. 
            $blockName,['method'=>__METHOD__]);
        }
        $this->curLevel++;
        //将区块写入区块队列
        array_push($this->blockQueue,$blockName);
        //开启缓存
        ob_start();
        return true;
    }

    //区块结束
    public function endBlock($blockName = null)
    {
        $this->curLevel--;
        //获取当前区块
        $curBlock = array_pop($this->blockQueue);
        if($blockName && $curBlock !== strtoupper($blockName)){
            ViewExceptions::invalidBlock(sprintf('block数量不匹配,(%s vs %s).',
            strtolower($curBlock),$blockName),['method'=>__METHOD__]);
        }
        //获取缓存内容,作为区块的内容,并清空缓存
        $content = ob_get_clean();
        $content = trim($content);
        //将区块内容写入数组
        if(isset($this->blocks[$curBlock])){
            $this->blocks[$curBlock] = $content;
            return true;
        }
        $this->blocks[$curBlock] = $content;
        //将区块层级写入区块层级Map
        $this->addBlockToLevelMap($curBlock,$this->curLevel);
        //输出区块名称
        echo $this->blockPre . $curBlock . $this->blockSuf;
    }

    //定义区块位置,将模板定义的区块内容 写入指定位置
    public function place($blockName, $content = '')
    {
        $blockName  = strtoupper($blockName);
        $parentBlock = $this->getCurrentBlock();
        if($parentBlock == $blockName){
            ViewExceptions::invalidBlock(sprintf('block(%s)不允许重名',$blockName)
            ,['method'=>__METHOD__]);
        }
        if(isset($this->blocks[$blockName])){
            $this->blocks[$blockName] = $content;
            return true;
        }
        $this->blocks[$blockName] = $content;
        $this->addBlockToLevelMap($blockName,$this->curLevel);
        //输出区块完整名称,占据位置,方便解析区块时,将内容替换写入此处
        echo $this->getFullBlockName($blockName);
    }

    //从区块队列中 获取当前区块
    public function getCurrentBlock()
    {
        if(!$this->blockQueue) return null;
        $lastIdx = count($this->blockQueue) - 1;
        return $this->blockQueue[$lastIdx];
    }

    //将区块名称及对应的层级,写入区块层级映射数组
    private function addBlockToLevelMap($blockName, $level = 0)
    {
        if(!isset($this->blockLevelMap[$level])){
            $this->blockLevelMap[$level] = [];
        }
        array_push($this->blockLevelMap[$level],$blockName);
    }

    //获取完整区块名称
    private function getFullBlockName(string $name) : string {
        return sprintf('%s%s%s', $this->blockPre, $name, $this->blockSuf);
    }
}

模板文件加载及解析实现

Tpl.php

namespace Lubed\MVCKernel\Views;

final class Tpl
{
    private $name;
    private $suffix;
    private $data = [];

    //$path:接收模板路径配置信息,模板源文件路径及解析后存放的路径
    //$suffix:模板文件后缀名
    public function __construct( $path,string $suffix='.html')
    {
        $this->path = $path;
        $this->suffix=$suffix;
    }

    //载入视图模板
    public function load(string $name, array $data = [])
    {
        $path = $this->getTplFilePath($name);
        //将PHP数组中的key作为变量名,value变量值 进行解压
        extract(array_merge($this->data, $data));
        //加载视图模板PHP文件
        require $path;
    }

    //给模板设置数据
    public function setData(array $data)
    {
        $this->data = array_merge($this->data, $data);
    }

    //获取模板文件的绝对路径
    public function getTplFilePath(string $tpl_name)
    {
        return vsprintf('%s/%s%s',[
            $this->path->get('source'),
            $tpl_name,
            $this->suffix
        ]);
    }
}

接下来,我们来看一下视图模板文件如何制作

1.布局模板 layout.html.php

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="robots" content="noindex,nofollow"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
    <title><?php echo isset($title)?$title:'';?></title>
    <link rel="stylesheet" href="/assets/css/whoops.base.css" />
    <link rel="stylesheet" href="/assets/css/prism.css" />
  </head>
  <body>

    <div class="Whoops container">
        <?php /*设定一个区块名称为content的位置,解析时将区块的解析结果写入这个位置中*/ ?>
        <?php $view->place('content');?>
    </div>

    <script src="/assets/js/prism.js"></script>
    <script src="/assets/js/zepto.min.js"></script>
    <script src="/assets/js/clipboard.min.js"></script>
    <script><?php echo isset($javascript)?$javascript:''; ?></script>
  </body>
</html>

2.异常模板 failed.html.php

<?php $view->extend('whoops/layout');?>
<?php /*开始定义区块content*/ ?>
<?php $view->beginBlock('content');?>
    <div class="stack-container">
        <div class="panel left-panel cf <?php echo (isset($has_frames)&&$has_frames ? 'empty' : ''); ?>">
            <?php /*载入whoops下的panel_left模板*/ ?>
            <?php $view->load('whoops/panel_left'); ?>
        </div>
        <div class="panel details-container cf">
            <?php $view->load('whoops/frame_code'); ?>
            <?php $view->load('whoops/details');?>
        </div>
    </div>
<?php $view->endBlock();?>
<?php /*区块content结束*/ ?>

最后,在控制器中选择模板并返回解析结果,实现如下:

$data=[
//.......
];

return $this->view->display('whoops/failed',$data);