jeydotc / nev
使用 PHP 作为模板系统!
Requires
- php: >=7.2.0
Requires (Dev)
- phpunit/phpunit: >=6.2.3
This package is auto-updated.
Last update: 2024-09-26 07:26:37 UTC
README
你知道为什么有模板引擎吗?因为我们都是不守纪律的人!
安装
composer require jeydotc/nev
创建视图
<?php namespace MyNamespace\Views; use Nev\View; final class MyView extends View { protected $name = ''; protected function render() { ?> <!DOCTYPE html> <html> <head> <title>Hello Nev</title> </head> <body> <h1>Hello <?=$this->name?>.</h1> </body> </html> <? } }
渲染它
<?= MyView::show(['name' => 'World']) ?>
这究竟是什么?
Nev 的理念是,如果你足够自律,就可以只用普通的 PHP 类来表示视图。你所需要的是一个简单的基类,从它派生出来,隐藏掉大部分丑陋的东西。
那么,我能用这个做什么呢?让我带你看看
首先,让我们看看一个简单的视图
<?php namespace MyNamespace\Views; use Nev\View; // Just create a regular PHP class and inherit from Nev\View final class BasicView extends View { // Optionally, use the Html trait to have a few helper methods use \Nev\Html; // Implement the abstract render method. (And yes, I stealed the syntax from react). protected function render() { ?> <!-- As a recommendation, do as much HTML as you can so your view remains clear --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- This method from the HTML trait will render a link tag. --> <?= $this->css('https://stackpath.bootstrap.ac.cn/bootstrap/4.3.1/css/bootstrap.min.css') ?> <title>Hello Nev</title> </head> <body> <h1>Hello world.</h1> <!-- This method from the HTML trait will render as many script tags as arguments provided. --> <? $this->js( "https://code.jqueryjs.cn/jquery-3.3.1.slim.min.js", "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", "https://stackpath.bootstrap.ac.cn/bootstrap/4.3.1/js/bootstrap.min.js" ) ?> </body> </html> <? } }
好的,正如你所见,创建视图只需要创建一个类,并给它渲染 HTML 的责任。这里有一些智慧之语,这也适用于你只使用纯 PHP 文件的情况
- 尽可能多地使用 HTML!不要这样做
echo "<a href='$someVariable'>$someOtherVariable</a>"
,或者这样:echo "$someCrazyStringContainingHTMLYouBuiltWithcomplexOperations
。我可以发誓,你会后悔的。 - 视图类应该只用作 视图!不要让它们调用数据库或处理复杂操作,这些应该在业务层完成,并为视图提供你想要渲染的信息。
- 这些是普通的类,这意味着你可以组合它们,做所有那些优雅的事情。只要记住上面的内容。
现在,正如我们上面提到的,这些是普通的类,所以,你可以
组合视图。
即使你可以创建一个基类并从中继承,但普遍认为,组合事物(组合优于继承)更好,因为这会导致更灵活、更易于维护的代码。
所以,作为一个例子,让我们创建一个 Page 组件,它接收页面部分作为参数
<?php namespace MyNamespace\Views; use Nev\Html; use Nev\View; final class Page extends View { use Html; /** * The page's title. * * @var string */ protected $title = ''; /** * The page's body. It can be a string, callable or View instance. * * @var string|callable|View */ protected $body = ''; /** * The list of script urls. * @var string[] */ protected $scripts = []; /** * The list of css urls. * @var array */ protected $cssFiles = []; public function render() { ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/> <!-- Render the CSS files --> <?= $this->css( 'https://stackpath.bootstrap.ac.cn/bootstrap/4.3.1/css/bootstrap.min.css', // In case you din't know, here we're using the spread operator, this allows to // take an array and send its values as parameters, this feature is available since PHP 7.2 ...$this->cssFiles ) ?> <!-- Render the page title. --> <title><?= $this->title ?></title> </head> <body> <!-- Render the Page's body. The draw method will check if the value is a string, a callable or a View instance and act accordinly, more info in a few moments. --> <?= self::draw($this->body) ?> <!-- Render the javascript files --> <?= $this->js( "https://code.jqueryjs.cn/jquery-3.3.1.slim.min.js", "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js", "https://stackpath.bootstrap.ac.cn/bootstrap/4.3.1/js/bootstrap.min.js", ...$this->scripts ) ?> </body> </html> <? } }
我们将所有样板代码移动到了 Page 类中。现在让我们创建一个特定的视图
<?php namespace MyNamespace\Views; use Nev\View; final class HelloWorldView extends View { protected $name; protected function render() { ?> <h1>Hello, <?=$this->name?>!</h1> <p> It feels good to do things without dealing with boilerplate things. </p> <? } }
然后我们可以这样组合我们的页面
<?= Page::show([ 'title' => 'My first page', 'body' => new HelloWorldView(['name' => 'World']), ]);
专业提示:由于 show
方法将返回一个字符串,你可以将其用作响应体
<?php // Example as you'd see in slim framework and similar: $app->get('/hello/{name}', function (Request $request, Response $response, array $args) { $name = $args['name']; $html = Page::show([ 'body' => new HelloWorldView(['name' => $name]), ]); $response->getBody()->write($html); return $response; });
甚至更进一步,由于 View 实现了 __toString
,你甚至可以直接发送视图实例
<?php // Example as you'd see in slim framework and similar: $app->get('/hello/{name}', function (Request $request, Response $response, array $args) { $name = $args['name']; $response->getBody()->write(new Page([ 'body' => new HelloWorldView(['name' => $name]), ])); return $response; });
向视图传递数据
正如你可能已经从之前的示例中注意到的那样,你发送给构造函数(或 show 方法)的数据被映射到你的视图中作为属性,这允许你从你的 IDE 获得自动完成支持,这在处理复杂对象或集合时特别有用。
<?php namespace Nev\Tests\SampleViews; final class ModelDependentView extends View { /** * Your IDE will surely have support for auto-completing this :D * * @var SomeViewModel */ protected $model; protected function render() { ?> <h1>Hello, user Nº <?=$this->model->id?></h1> <p> Sorry to treat you in such a cold manner Mr <?=$this->model->name?>, my programmer just made me that way. </p> <? } }
在你的控制器中
<?php // This can be anything, from scalar values to arrays, objects, whatever. $someViewModel = new SomeViewModel(); $renderedResult = ChildView::show([ 'model' => $someViewModel ]);
布尔值
布尔值是一个特殊情况,当然你可以通过提供一个键和值的数组来设置它们,但你也可以通过只添加没有键的名称来将布尔字段设置为 true
。
让我们用一个例子来澄清这一点
<?php namespace MyNamespace\Views; use Nev\View; final class HelloWorldView extends View { /** * Let's have a boolean property declared. * @var bool */ protected $isAdmin = false; protected $name; protected function render() { ?> <h1>Hello, <?=$this->name?>!</h1> <? if($this->isAdmin): ?> <a href="....">Go to some special place reserved for admin guys</a> <? endif; ?> <p> It feels good to do things without dealing with boilerplate things. </p> <? } }
现在我们在视图中有了布尔属性,我们可以以多种方式发送一个值
<!--We can send the value as a key as usual --> <?= HelloWorldView::show(['isAdmin' => true, 'name' => 'World']) ?> <!--Or We can send just the name without a key, which will set the value to true --> <?= HelloWorldView::show(['isAdmin', 'name' => 'World']) ?>
注意:省略值不会将其设置为 false,而是将其设置为类中声明的默认值(通常是 false)。
向视图传递额外的属性
你可以向视图发送未声明的值。它们将被映射到属性中,就像其他属性一样,但由于你没有声明它们,所以它们不会很有用。
因此,有一个名为 extraProperties
的方法,它将提供一个包含未声明值的关联数组,你可以使用它来添加额外的属性到某些元素
<?php namespace MyNamespace\Views; use Nev\View; use Nev\Html; final class Div extends View { use Html; protected $contents; protected function render() { // Get all the non-declared properties $attrs = $this->extraProperties(); ?> <!-- Render the associative array as html attributes (more on attrs method at the 'Crating a Component' section) --> <div <?= $this->attrs($attrs) ?> > <!-- Draw the contents, (more on draw methods at the 'Crating a Component' section) --> <?= self::draw($this->contents) ?> </div> <? } }
创建组件
使用Nev创建可以被其他视图使用的组件,这是一种非常实用的功能。
组件并没有什么特殊之处,它们只是具有不同目的的常规视图。
假设我们想要创建一个Bootstrap警报组件,为了实现这一点,我们只需要创建一个视图
<?php namespace Nev\Tests\SampleViews; use Nev\Html; use Nev\View; final class AlertComponent extends View { use Html; protected function render() { ?> <div class="alert alert-info alert-dismissible fade show" role="alert"> <h4 class="alert-heading">Hello!</h4> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> This is a cool alert! </div> <? } }
在你的视图中
<?php namespace Nev\Tests\SampleViews; use Nev\Tests\SampleViews\AlertComponent; final class HelloWorldView extends View { protected function render() { ?> <!-- Let's display the alert. --> <?=AlertComponent::show()?> <p>Lots of content!.</p> <? } }
到目前为止,一切顺利,但这个警报几乎是静态的,几乎没有什么用处,所以,让我们添加至少设置内容和标题的能力
<?php namespace Nev\Tests\SampleViews; use Nev\Html; use Nev\View; final class AlertComponent extends View { use Html; protected $title; protected $body; protected function render() { ?> <div class="alert alert-info alert-dismissible fade show" role="alert"> <h4 class="alert-heading"><?=$this->title?></h4> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> <?=$this->body?> </div> <? } }
在你的视图中
<?php namespace Nev\Tests\SampleViews; use Nev\Tests\SampleViews\AlertComponent; final class HelloWorldView extends View { protected function render() { ?> <!-- Let's display the alert. --> <?= AlertComponent::show(['title' => 'Notice', 'body' => 'This is a notice!']) ?> <p>Lots of content!.</p> <? } }
好的,这样更好,但,如果我想添加更复杂的东西呢?让我来介绍一下draw
方法
draw
方法
有时你的组件需要接收复杂标记作为属性,draw
静态方法允许你通过处理不同场景来渲染属性
<?php namespace Nev\Tests\SampleViews; use Nev\Html; use Nev\View; final class AlertComponent extends View { use Html; protected $title; protected $body; protected function render() { ?> <div class="alert alert-info alert-dismissible fade show" role="alert"> <h4 class="alert-heading"><?=self::draw($this->title)?></h4> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> <?=self::draw($this->title)?> </div> <? } }
注意,现在内容和标题都是通过draw
方法渲染的,这使我们能够处理这些场景
<?= AlertComponent::show([ // Sending a string: 'body' => 'This string will be echoed!' ]) ?> <?= AlertComponent::show([ // Sending a function: 'body' => function(){ ?> <p>This function will be called upon component rendering</p> <blockquote>The sky is the limit!</blockquote> <? }, ])?> <?= AlertComponent::show([ // Sending a view Instance: 'body' => new SomeOtherComponent([/*...*/]), ])?>
draw
方法将负责检查组件值的类型,并根据值类型执行正确操作
- 字符串或任何标量值:该值将被返回,以便可以进行回显。
- 可调用:它将被调用,并且输出将被捕获并返回。
- 视图实例:它将通过调用其
display
方法进行渲染。
处理CSS类
配置CSS类可能有点繁琐。幸运的是,Html特性提供了一种舒适的方式来动态添加它们。
继续我们的例子,首先改变我们添加类的方式
<?php namespace Nev\Tests\SampleViews; use Nev\Html; use Nev\View; final class AlertComponent extends View { use Html; // Some content omitted... protected function render() { $classes = $this->classes("alert", "alert-info", "alert-dismissible", "fade", "show"); ?> <div class="<?=$classes?>" role="alert"> <h4 class="alert-heading"><?= self::draw($this->title)?></h4> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> <?= self::draw($this->title)?> </div> <? } }
如你所见,我所做的只是调用$this->classes(...)
方法,并将每个类作为单独的字符串发送。这并没有太大的改进,但请耐心,事情会变得更好。
现在,让我们添加一个状态属性
<?php namespace Nev\Tests\SampleViews; use Nev\Html; use Nev\View; final class AlertComponent extends View { use Html; // Some content omitted... protected $status = 'info'; protected function render() { $classes = $this->classes("alert", "alert-{$this->status}", "alert-dismissible", "fade", "show"); ?> <div class="<?=$classes?>" role="alert"> <h4 class="alert-heading"><?= self::draw($this->title)?></h4> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> <?= self::draw($this->title)?> </div> <? } }
好的,现在我们可以将警报的状态设置为Bootstrap支持的任何选项(信息、警告、危险...)。但这样做仍然不能体现这个方法的需要,那么,添加一个决定组件是否可关闭的能力怎么样
<?php namespace Nev\Tests\SampleViews; use Nev\Html; use Nev\View; final class AlertComponent extends View { use Html; // Some content omitted... protected $status = 'info'; protected $dismissible = false; protected function render() { $classes = $this->classes( "alert", "alert-{$this->status}", // Look, a conditional class! These classes will only display if // $this->dismissible is true. [ "alert-dismissible fade show" => $this->dismissible ] ); ?> <div class="<?=$classes?>" role="alert"> <h4 class="alert-heading"><?= self::draw($this->title)?></h4> <!-- Only show this button if dismissible. --> <?if($this->dismissible):?> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> <?endif;?> <?= self::draw($this->title)?> </div> <? } }
请注意[ "alert-dismissible fade show" => $this->dismissible ]
数组参数,这是由->class()
方法支持的特殊情况,它基本上将键作为类添加,如果值评估为真。数组可以有任意多的键值对。
现在,为了完成这部分,让我们让组件用户能够添加他自己的类
<?php namespace Nev\Tests\SampleViews; use Nev\Html; use Nev\View; final class AlertComponent extends View { use Html; // Some content omitted... protected $status = 'info'; protected $dismissible = true; /** * @var array|string */ protected $className = []; protected function render() { $classes = $this->classes( // Add the user provided classes. $this->className, "alert", "alert-{$this->status}", // Look, a conditional class! These classes will only display if // $this->dismissible is true. [ "alert-dismissible fade show" => $this->dismissible ] ); ?> <div class="<?=$classes?>" role="alert"> <h4 class="alert-heading"><?= self::draw($this->title)?></h4> <!-- Only show this button if dismissible. --> <?if($this->dismissible):?> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> <?endif;?> <?= self::draw($this->title)?> </div> <? } }
这将允许组件用户添加他自己的类,请看示例用法
<?= AlertComponent::show([ 'dismissible' => true, 'status' => 'warning', // You can still send a string. 'className' => 'my-custom-class', 'body' => 'This string will be echoed!' ]) ?> <?= AlertComponent::show([ 'dismissible', 'status' => 'warning', // Or provide an array for more fun! 'className' => [ // Numerical index are just appended. 'my-custom-class', 'my-other-custom-class', // String keys are appended if the value evaluates to true. 'this-class-will-be-added' => $someTruthyValue, 'this-class-will-be-ignored' => $someFalsyValue, ], 'body' => 'This string will be echoed!' ]) ?>
条件绘图
如上所示,PHP模板中的if
语句可能会很繁琐,对于这些情况,你可以选择调用drawIf
静态方法
<!-- Instead of using if, you can do this. --> <?= self::drawIf($this->dismissible, function() {?> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> <?}) ?> <!-- Or send a string --> <?= self::drawIf($this->dismissible, "Yay, I'm dismissible!") ?> <!-- Or send a View instance! --> <?= self::drawIf($this->dismissible, new DismissButton()) ?>
drawIf
方法接受两个参数:一个布尔值指示渲染是否会发生,以及一个字符串|可调用|视图,如果第一个参数评估为真,将使用draw
方法进行渲染。
额外属性
有时你想要允许用户向你的组件添加自定义HTML属性。正如前文所述,你只需要使用extraProperties
方法获取未声明的属性,并使用attr
方法进行渲染
<?php namespace Nev\Tests\SampleViews; use Nev\Html; use Nev\View; final class AlertComponent extends View { use Html; // Some content omitted... protected $status = 'info'; protected $dismissible = true; /** * @var array|string */ protected $className = []; protected function render() { // Get the extra attributes as an associative array $attributes = $this->extraProperties(); $classes = $this->classes( // Add the user provided classes. $this->className, "alert", "alert-{$this->status}", // Look, a conditional class! These classes will only display if // $this->dismissible is true. [ "alert-dismissible fade show" => $this->dismissible ] ); ?> <div <?= $this->attrs($attributes) /*<-- Render the attributes */?> class="<?= $classes ?>" role="alert"> <h4 class="alert-heading"><?= self::draw($this->title)?></h4> <!-- Only show this button if dismissible. --> <?if($this->dismissible):?> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> <?endif;?> <?= self::draw($this->title)?> </div> <? } }
extraProperties()
方法将返回发送给构造函数的所有未在组件类中声明的属性。这将允许你执行像这样的事情
<?php AlertComponent::show([ // These will be rendered as attributes. 'id' => 'my-id', 'data-some-custom-attr' => "Some cool value", 'title' => "Some cool title for this element!", // Since this attribute is declared in the class, extraProperties() won't return it. 'body' => 'This string will be echoed!', ]);
注意,我们需要使用Html
特质的attrs
方法来将关联数组显示为HTML属性集。
用样式结束一切
最后,为了完成个性化选项,有一个辅助方法允许你将键值对数组渲染为CSS字符串。
<?php AlertComponent::show([ // These will be rendered as attributes. 'id' => 'my-id', 'data-some-custom-attr' => "Some cool value", 'title' => "Some cool title for this element!", // The `style` method from `Html` trait will convert an associative array into a CSS string. 'style' => $this->style([ 'float' => 'right' ]), // Since this attribute is declared in the class, extraProperties() won't return it. 'body' => 'This string will be echoed!', ]);