dakujem/latter

PSR-7框架和堆栈的Latte视图层。

1.0.0-alpha2 2022-08-31 12:08 UTC

This package is auto-updated.

Last update: 2024-08-29 10:21:25 UTC


README

PHP from Packagist Test Suite Coverage Status

PSR-7框架和堆栈的Latte视图层。这不是一个打字错误。

💿 composer require dakujem/latter

要使用PSR-7兼容框架(如Slim)与出色的Latte模板语言一起使用,一个人可以选择自己设置所有内容或使用Latter
后者是一个更好的选择

  • Latter旨在减少代码重复(渲染管道、渲染例程)
  • Latter是一个灵活的薄层,可以根据需求进行调整和驯服
  • 一次设置,以后不用管

📖
查看Latte文档以熟练掌握Latte。

将Latte模板渲染为PSR-7响应

以下是一个非常基本的示例,用于渲染类似以下内容的Latte模板

{*} hello.latte {*}
Hello {$name}!

在Slim框架中使用Latter将会是

// app.php
$app = AppFactory::create();
// ...

$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $params = [
        'name' => $args['name'],
    ];
    return (new Dakujem\Latter\View)->render(
        $response,
        __DIR__ . '/templates/hello.latte',
        $params,
        new Latte\Engine
    );
});

// ...
$app->run();

当然,在上述情况下使用Latte会有些过度。
大多数情况下,一个人将使用Latte来处理更复杂的模板,其中包含多个变量、过滤器宏。

在这种情况下,在服务容器中定义一个Latter\View服务进行依赖注入是有意义的。

配置Latte\Engine工厂服务

首先,让我们创建一个服务容器。
我将使用Sleeve,这是Symfony Pimple容器的一个简单扩展。

$container = new Dakujem\Sleeve();

在大多数情况下,应该为每个模板渲染创建一个新的Latte\Engine实例。这就是为什么应该定义一个工厂服务。也就是说,每次从服务容器请求服务时,都会返回一个新的实例。

💡
请查看您所使用的服务容器或框架的文档,以正确配置此步骤。

$container->set('latte', $container->factory(function () use ($container) {
    $engine = new Latte\Engine();

    // Configure the file loader to search for templates in a dedicated directory.
    $loader = new Latte\Loaders\FileLoader(__DIR__ . '/templates');
    $engine->setLoader($loader);

    // Set a temporary directory, where compiled Latte templates will be stored.
    $engine->setTempDirectory($container->settings['view-temp-dir']);

    return $engine;
}));

定义应包含

  • 安装常用过滤器
  • 安装自定义标签(宏)
  • 配置合适的Latte loader
  • 任何其他需要的Latte\Engine设置调用

现在每次我们调用$container->get('latte')时,都会返回一个配置好的Latte\Engine的新实例

(new Dakujem\Latter\View)->render(
    $response,
    'hello.latte',
    $params,
    $container->get('latte')
);

请注意,由于FileLoader的配置,我们不再需要将模板名称的前缀设置为完整路径。

配置Latter\View服务

现在让我们定义一个Latter\View服务。

$container->set('view', function () use ($container) {
    $view = new Dakujem\Latter\View();

    // optionally set an engine factory (recommended)
    $view->setEngine(function () use ($container): Latte\Engine {
        return $container->get('latte');
    });

    return $view;
});

Latter\View实例现在将在服务容器中可用

$view = $container->get('view');

如果向View服务提供了引擎工厂,则可以省略为每个渲染的模板提供Engine实例。

// the render calls have gotten shorter:
$view->render($response, 'hello.latte', $params);

View服务定义可以包含以下可选定义

  • 模板别名
  • 渲染例程(模板渲染)
  • 渲染管道
  • 引擎工厂
  • 默认参数
  • 默认渲染例程

这些都是可选的。

模板别名

可以创建模板别名,以便可以使用不同的名称引用模板。

$view->alias('hello', 'hello.latte');
$view->alias('index', 'ClientModule/Index/default.latte');

使用别名渲染模板

$view->render($response, 'hello', $params);
$view->render($response, 'index', $params);

渲染例程

渲染例程应该用于在不重复代码的情况下应用特定模板的设置。

它们可以用于

  • 定义过滤器
  • 定义标签(宏)
  • 修改输入参数
  • 修改模板名称
  • 或者甚至使用完全不同的引擎实例或渲染自己的响应

渲染例程是一个 可调用的,它接收一个 Runtime 上下文对象,并返回一个 响应,其签名如下

function(Dakujem\Latter\Runtime $context): Psr\Http\Message\ResponseInterface | Dakujem\Latter\Runtime

示例

$view->register('shopping-cart', function (Runtime $context) {
    // This callable is the place to register filters,
    // variables and stuff for template named "shopping-cart"

    // Do any setup of the Engine that is needed for the template to render correctly
    $latte = $context->getEngine();
    $latte->addFilter('count', function(){
        // return the count of items in the shopping cart here
    });

    // Template name can be set or changed freely.
    // Note that if one only needs to set a nice name for the template to be rendered,
    // aliases are a simpler option to do so
    $template = 'ClientModule/Cart/list.latte';

    // The params can be modified at will, for example to provide defaults
    $params = array_merge(['default' => 'value'], $context->getParams());

    // the Runtime::toResponse helper method can be used for default rendering
    return $context->withTarget($template)->withParams($params);
});

可以像渲染别名一样渲染例程

$view->render($response, 'shopping-cart', $params);

默认渲染例程

可以可选地注册一个默认渲染例程,该例程将用于所有未注册的模板。

$view->registerDefault( function (Runtime $context) { ... } );

默认渲染例程的签名与命名例程完全相同。

在渲染未注册的模板时将使用它。

$view->render($response, 'a-template-with-no-registered-routine', $params);

默认参数

默认参数将与每个渲染调用提供的参数合并。

如果想要为每个模板定义默认参数,可以使用渲染例程。

$view->setParam('userName', 'Guest');      // a single parameter
$view->setParams([
    'userName' => 'Guest',
    'projectName' => 'My Awesome Project',
]); // all parameters at once

渲染管道

管道允许在最终渲染之前依次调用多个 预渲染 例程。
例程可以在具有共同布局、共同包含文件(include)、共同设置(过滤器、变量)或其他渲染逻辑的多个模板渲染调用之间共享。
最明显的例子是 布局 或常见的 文件 / 包含。

首先,必须注册适当的预渲染例程

$view->register('base-layout', function (Runtime $context) {
    // do setup needed for templates using the base layout
    $context->getEngine()->addFilter( ... );
    
    // return a context object (!)
    return $context;
});
$view->register('--withUser--', function (Runtime $context) {
    // do setup common for templates using a `$user` variable
    $defaults = [
        'user' => get_user( 'somehow' ),
    ];

    // return a context object (!)
    return $context->withParams($defaults);
});

对于在管道中使用的预渲染例程,返回一个 Runtime 上下文对象非常重要。如果返回了一个 Response,管道将提前结束(尽管在某些情况下这可能是有意的)。任何其他类型的返回值都将被忽略。

使用管道进行渲染的调用可能如下所示

// calling a pipeline with 2 _pre-render_ routines and a registered render routine
$view
    ->pipeline('base-layout', '--withUser--')
    ->render($response, 'shopping-cart', $params);

// rendering a file with a common _pre-render_ routine
$view
    ->pipeline('--withUser--')
    ->render($response, 'userProfile.latte', $params);

当处理包含的模板(页眉、页脚)或需要特定变量或过滤器进行渲染的布局模板时,管道尤其有用。
示例

{*} home.latte {*}
{layout 'base.latte'}
{block #content}
<p>Greetings, stranger!</p>
{*} about.latte {*}
{layout 'base.latte'}
{block #content}
<p>Stay awhile and listen.</p>

上述两种情况都使用相同的布局,该布局需要在 'base-layout' 预渲染例程中进行特定的设置

$view->pipeline('base-layout')->render($response, 'home.latte');
$view->pipeline('base-layout')->render($response, 'about.latte');

这种渲染可以与在渲染前对模板进行标记或装饰进行比较。

或者,也可以将管道定义为渲染例程的一部分

$view->register('contacts.latte', $view->pipeline('base-layout', function (Runtime $context) {
    // ... do whatever setup needed for rendering the contacts page
    return $context->withParams(['foo' => 'bar']);
}));
{*} contacts.latte {*}
{layout 'base.latte'}
{block #content}
<p>Contact: {$foo}</p>
$view->render($response, 'contacts.latte');

显式链式调用

有时希望在另一个渲染例程中调用一个渲染例程。这可以通过使用 View::anotherView::execute 实现。

// register a routine named 'ahoy', that will render `hello.latte`
$view->register('ahoy', function (Runtime $context) {
    return $context->withTarget('hello.latte');
});
// register a routine that will internally invoke it
$view->register('foo', function (Runtime $context) use ($view) {
    return $view->another($context, $view->getRoutine('ahoy'));
});

// render 'hello.latte' using 'foo' routine that internally uses 'ahoy' routine
$view->render($response, 'foo');

请注意,这些方法不仅限于使用已注册的例程,它们可以执行任何签名匹配的可调用对象。

技巧 & 小贴士

性能

Latte 模板在第一次渲染时编译。所有后续渲染都将使用编译和优化的 PHP 代码。

为了在生产服务器上略微提高性能,可以在引擎工厂中关闭自动刷新
$engine->setAutoRefresh($container->settings['dev'] ?? true);
这有其局限性,请事先阅读 Latte 文档。

使用 {link} 和 n:href 宏与 Slim 框架配合使用

可以定义类似于 Nette 框架中宏的 {link}n:href 宏。这些宏将为 命名路由 生成 URL。

首先确保在 Latte 引擎中注册了一个生成 URL 的 过滤器,然后创建一个使用该过滤器的

$app = Slim\Factory\AppFactory::create();
$engine = new Latte\Engine();

// The section below configures the `{link}` and `n:href` macros and the `urlFor` filter.
$engine->addFilter('urlFor', function ($route, ...$args) use ($app) {
    // the filter will call the `urlFor` method of the route parser
    return $app->getRouteCollector()->getRouteParser()->urlFor($route, ...$args);
    // Note: if you are using slim v3, use `$container->get('router')->pathFor( ... )` instead.
});
$macroSet = new MacroSet($engine->getCompiler());
$linkMacro = function (MacroNode $node, PhpWriter $writer) {
    return $writer->using($node)->write('echo ($this->filters->urlFor)(%node.word, %node.args?);');
};
$macroSet->addMacro('link', $linkMacro);
$macroSet->addMacro('href', null, null, function (MacroNode $node, PhpWriter $writer) use ($linkMacro) {
    return ' ?> href="<?php ' . $linkMacro($node, $writer) . ' ?>"<?php ';
});

以上操作最好在 'latte' 服务定义期间完成。查看更多详情,请参见 测试

然后可以使用宏

{*} named routes without route parameters {*}
{link home}

{*} named routes wit route parameters {*}
{link hello [name => $name]}

{*} named routes with route parameters and query parameters {*}
{link rc [resource => apple, action => eat], [param1 => val1, param2 => val2]}

{*} n:href macro has the same syntax {*}
<a n:href='home'>go home</a>
<a n:href='hello [name => hugo], [a => b]'>polite hello</a>

{*} using the filter is of course possible too {*}
{='home'|urlFor}

💡
注意与 Nette 框架的区别 - 第一个宏参数必须是一个数组。
为了使查询参数生效,在宏中使用 %node.args?。可以通过将 %node.args? 替换为 %node.array? 来去除查询参数,转而使用精确的 Nette {link} 语法。

在渲染前设置引擎

渲染例程的意图是在渲染特定模板之前提供一个设置 Latte\Engine 实例的位置,然而,可以使用管道快速设置引擎。

$view->pipeline(function(Runtime $context) {
    $context->getEngine()->addFilter( ... );
})->render($response, 'myTemplate.latte', ['param' => 'value']);

贡献

... 是受欢迎的。🍵

请继续分叉仓库,做出你的更改,然后提交你的 PR。