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);