jeydotc/nev

使用 PHP 作为模板系统!

v2.0.0.1 2020-08-25 22:00 UTC

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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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!',
]);