ingenerator / kohana-view
基于PHP类的Kohana框架视图
Requires
- php: ~8.0.0 || ~8.1.0 || ~8.2.0
- composer/installers: ~1.0
- ingenerator/kohana-core: ^4.9.0
Requires (Dev)
- ingenerator/kohana-dependencies: ^1.3.0
- kohana/koharness: dev-master
- mikey179/vfsstream: ^1.6.11
- phpunit/phpunit: ^9.5.5
Suggests
- ingenerator/kohana-dependencies: Dependency Injection container for Kohana 3.x
README
kohana-view
为PHP应用程序提供视图逻辑和模板分离。它设计用于与Kohana框架一起使用 - 但您应该能够通过一些工作在大多数PHP项目中使用它。
对于遗留项目,它可以与标准的Kohana View
类共存,但它完全不兼容 - 每个视图都需要使用库存的View
或更新为与kohana-view
一起使用。特别是,我们在页面布局视图方面的方法与标准的Kohana Controller_Template
有显著差异。
为什么你应该使用它
- 基于类的视图将逻辑从控制器和模板中分离出来
- 更容易找到和更新应用程序中所有的显示逻辑
- 更容易为模块、应用程序的可配置部分等自定义显示逻辑
- 使每个视图的依赖关系更明显,更容易维护
- 在输出为HTML时自动转义所有视图变量(默认情况下可以禁用)
- 干净、结构良好的代码没有全局状态,更容易测试,出错的风险更小
- 完全单元测试
安装
使用composer安装:$> composer require ingenerator/kohana-view
添加到您的application/bootstrap.php
Kohana::modules([ 'existing' => 'existing/modules/call/here', 'kohana-view' => __DIR__.'/../vendor/ingenerator/kohana-view' ]);
我们还建议使用依赖注入容器/服务容器来管理项目中所有依赖关系。Kohana-view不需要特定的容器,但它提供了zeelot/kohana-dependencies的配置。本readme中的示例假设您正在使用该容器,因此如果您使用其他容器(实际上,请勿尝试在PHP中全部内联执行),则从您的容器中获取依赖关系,如所需。
创建您的第一个视图
每个视图都以实现Ingenerator\KohanaView\ViewModel
接口的类开始。您可以自己创建一个,也可以从Ingenerator\KohanaView\ViewModel\AbstractViewModel
扩展,以获得一个具有一些有用公共功能的基本类。视图类可以命名为任何您喜欢的名称,有或没有命名空间,都可以。
<?php //application/classes/View/Hello/WorldView.php namespace View\Hello; /** * @property-read string $name automatically returned from the $variables array * @property-read boolean $is_morning automatically returned from the var_is_morning method */ class WorldView extends \Ingenerator\KohanaView\ViewModel\AbstractViewModel { protected $variables = [ 'name' => NULL, ]; protected function var_is_morning() { $date = new \DateTime; return ($date->format('H') < 12); } }
每个视图类都有一个对应的模板 - 默认情况下,模板名称映射自类名,但您可以自定义此映射。
<?php //application/views/hello/hello_world.php /** * @var \View\Hello\WorldView $view * @var \Ingenerator\KohanaView\Renderer\HTMLRenderer $renderer */ ?> <html> <head><title>Hello <?=$view->name;?></title></head> <body> <h1> <?php if ($view->is_morning): ?> <img src="sunrise.png" alt="sunrise">Good Morning, <?php else: ?> <img src="cup_of_tea.png" alt="teacup">Good Afternoon, <?php endif; ?> <?=$view->name;?> - notice how I HTML escaped your name? </h1> <div class="alert alert-danger"> Never render user-provided content unescaped like this: <?= raw($view->name);?>. Just do that if you're including other views or known-safe HTML content. </div> </body> </html>
要在控制器响应中渲染此视图,您可以这样做
<?php //application/classes/Controller/Welcome.php class Controller_Welcome extends Controller { public function action_index() { $view = new View\Hello\WorldView; $view->display(['name' => $this->request->query('name')]); $renderer = $this->dependencies->get('kohanaview.renderer.html'); /** @var \Ingenerator\KohanaView\Renderer\HTMLRenderer */ $this->response->body($renderer->render($view)); } }
模板映射
所有模板都从级联文件系统中的/views路径加载,使用与Kohana的其余部分相同的规则来选择适当的版本,当模板存在于一个或多个模块/应用程序目录中时。
默认情况下,根据视图类名称选择模板。命名空间分隔符和下划线变为目录分隔符,驼峰式单词变为下划线,从开头和结尾移除View/ViewModel。例如
如果您想全局自定义此,则可以提供用于TemplateManager
的ViewTemplateSelector
类的替代实现。
然而,有时您可能只想为单个视图自定义它 - 要么是因为默认映射在某些原因下不理想,要么是因为模板依赖于某些视图逻辑的结果。在这种情况下,您可以在您的 ViewModel
上实现 TemplateSpecifyingViewModel
接口,并明确告诉渲染引擎使用哪个模板。
页面布局/页面内容
Kohana-view 为常见情况提供了开箱即用的支持,即您有多个页面内容视图,想要在(或可能是一系列)包含页面布局视图内渲染。这与 Kohana 的股票 Controller_Template 类似,但它支持递归渲染模型,其中每个视图都可以包含在另一个视图内,直到最终的整个站点模板。
例如,这可以允许您有一组内容区域视图,一个渲染这些内容区域之一并在包含侧边栏和主区域布局中的视图,以及一个进一步的上层父视图,该视图渲染您的整体页面头部/页脚等。与旧的 Controller_Template 类似,对于 AJAX 请求,渲染器默认只渲染内容区域视图,而不渲染任何包含模板 - 这可以自定义。这也意味着您可以让控制器扩展任何任意的基本类。
要使用 PageLayoutRenderer
,您至少需要两个视图 - 一个实现 PageLayoutView
,另一个实现 PageContentView
。请注意,这些接口现在已过时,取而代之的是更灵活的 NestedChildView
和 NestedParentView
,将在未来的版本中删除。
尽管不是强制性的,您可能希望扩展提供的 AbstractIntermediateLayoutView
和 AbstractNestedChildView
。您的顶级页面视图应该是 PageLayoutView
的实例。
<?php namespace View\Layout; /** * @property-read string $body_html * @propery-read string $title */ class SitePageTemplateView extends Ingenerator\KohanaView\ViewModel\PageLayout\AbstractPageLayoutView { }
<?php namespace View\Layout; /** * @property-read ViewModel $sidebar */ class ContentWithSidebarLayoutView extends Ingenerator\KohanaView\ViewModel\PageLayout\AbstractIntermediateLayoutView { public function __construct(SitePageTemplateView $page, ViewModel $sidebar) { parent::__construct($page); $this->sidebar = $sidebar; } protected function var_sidebar() { return $this->sidebar; } }
<?php namespace View\Layout; class SidebarView extends AbstractViewModel { // Whatever you want it to show }
<?php namespace View\Pages; /** * @property-read View\Layout\SitePageTemplateView $page * @property-read string $name */ class HelloWorldView extends Ingenerator\KohanaView\ViewModel\PageLayout\AbstractNestedChildView { protected $variables = [ 'name' => NULL ]; protected function var_page() { // If you want to make this available to set things from the view : it's not required return $this->getUltimatePageView(); } }
<?php //application/views/site_page_template.php /** * @var \View\Layout\SitePageTemplateView $view * @var \Ingenerator\KohanaView\Renderer\HTMLRenderer $renderer */ ?> <html> <head><title><?=$view->title;?></title></head> <body><?=raw($view->body_html); // Good usecase for rendering unescaped content?></body> </html>
<?php //application/views/content_with_sidebar_layout.php /** * @var \View\Layout\ContentWithSidebarLayout $view * @var \Ingenerator\KohanaView\Renderer\HTMLRenderer $renderer */ ?> <div class="row"> <div class="sidebar"><?=raw($renderer->render($view->sidebar));?></div> <div class="content"><?=raw($view->child_html);?></div> </div>
<?php //application/views/pages/hello_world.php /** * @var \View\Pages\HelloWorldView $view * @var \Ingenerator\KohanaView\Renderer\HTMLRenderer $renderer */ // You can do this here if you want to keep templatey-type stuff together // Or in your view model at display time if it's a bit more involved $view->page->setTitle('Hello World'); ?> <h1>Hi <?=$view->name;?></h1>
<?php class Controller_Welcome extends Controller // Look, extend any controller! No more Controller_Template! { public function action_index() { // You probably want to put your views into the dependency container too $content = new HelloWorldView( new ContentWithSidebarLayoutView( new SitePageTemplateView(), new SidebarView() ) ); $content->display(['name' => $this->request->query('name')]); $renderer = $this->dependencies->get('kohanaview.renderer.page_layout'); /** @var \Ingenerator\KohanaView\Renderer\PageLayoutRenderer $renderer */ $this->response->body($renderer->render($content)); } }
高级示例
默认变量
按照标准,视图要求传递给 AbstractViewModel->display()
的数组包含所有定义的变量的值。这是为了确保即使在多次渲染(如部分和子视图常发生的那样)时,视图模型也始终处于正确的状态。
您可以通过在 ViewModel 中填充 $default_variables
数组来定义可选的视图变量。请注意,这些默认值将在每次调用 ->display()
时 重新分配 到 $variables
数组,以确保它们始终处于预期的状态。
class View_Something extends AbstractViewModel { protected $default_variables = [ 'title' => 'My page title', ]; protected $variables = [ 'caption' => NULL ]; } print $view->title; // 'My page title' print $view->caption; // '' $view->display(['caption' => 'Something', 'title' => 'A title']); print $view->title; // 'A title' print $view->caption; // 'Something' $view->display(['caption' => 'Something else']); print $view->title; // 'My page title' print $view->caption; // 'Something else'
缓存变量
扩展 AbstractViewModel
的视图公开其 $variables
数组中的所有变量,以及由 var_variable_name
方法提供的任何动态变量。数组中的 $variables
优先于动态方法,这意味着您还可以将其用作仅需要为每个视图渲染计算一次的计算变量的缓存。
<?php class View_That_Does_Work { protected $variables = [ 'user_email' => '' ]; protected function var_user_activity() { $activity = []; foreach ($this->database->loadActivityForUser($this->user_email) as $activity) { $activity[] = (string) $activity; } $this->variables['user_activity'] = $activity; // Future usage of $view->user_activity will now get the value cached in the variables array without calling // this method again. return $activity; } }
变量数组在每次调用 display
时都会被清除,因此以这种方式缓存的值将在您提供新的视图数据时(例如,在循环中渲染视图)被清除。
渲染嵌套视图(部分)
包含视图模型应该公开部分视图模型的引用,该引用可能作为构造函数依赖项传递,由动态变量方法创建,或以某种其他方式注入。
视图模型没有对渲染器的引用,因此它们不能直接渲染部分 - 而应该在模板中通过模板作用域内提供的当前渲染器来完成。
例如
<?php class View_Container { protected $variables = [ 'users' => [], ]; public function __construct(View_User_FaceWidget $face_widget) { $this->face_widget = $face_widget; } protected function var_face_widget() { return $this->face_widget; } }
<?php //application/views/container.php /** * @var \View_Container $view * @var \Ingenerator\KohanaView\Renderer\HTMLRenderer $renderer */ ?> <?php foreach($view->users as $user):?> <?php $view->face_widget->display(['user' => $user]);?> <?=raw($renderer->render($view)); // Note rendering unescaped HTML ?> <?php endforeach; ?>
配置是否编译模板
模板引擎自动将您的源模板编译以添加自动转义功能。编译后的模板在磁盘上缓存以供将来执行。默认情况下,它们被缓存在与您的自动加载器缓存等 Kohana::$cache
目录中
- 旁边 - 我们建议在每次部署时刷新。
模板管理器始终会在模板不存在于磁盘上时编译模板。但是,您也可以配置它在每个请求上编译 - 在开发中很有用。
如果您正在使用默认的依赖容器,则这些选项已为您配置,包括设置 recompile_always = (Kohana::$environemnt === Kohana::DEVELOPMENT)
。您可以通过在 application/config/kohanaview.php
中添加自定义配置来调整这些设置 - 默认值请参阅 config/kohanaview.php。
如果您正在使用自己的服务容器,应相应地配置 CFSTemplateManager
的 $options
参数。
致谢
本包深受 dyron/kohana-view 的启发,而后者又是 zombor/View-Model 的分支。但自 2.x 版本以来,已经完全重写,以实现更清洁和更分离的结构,并采用先测试的方法。感谢并感谢 @zombor, @dyron, @nanodocumet 和 @slacker 对原始包的各种贡献。
本包的 2.x 版本由 inGenerator Ltd 赞助。
贡献
我们非常欢迎贡献。请确保您遵循我们的编码风格,为每次更改添加测试,并避免引入全局状态或过度依赖。对于重大或 API 破坏性更改,请首先与我们讨论您在问题中的想法,这样我们可以与您一起了解问题,并找到适合当前和未来用户的方式来解决它。
错误修复应从相关最早的 (>=2.0) 版本分支,我们将根据需要合并它们。新功能应从当前版本的开发分支分支。
许可证
本软件受 BSD-3-Clause 许可证 许可。