Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"phpunit/phpunit": "^12.0"
},
"suggest": {
"liquid/liquid": "Required for the Liquid template driver (set view.driver to 'liquid').",
"workerman/workerman": "Required for the WebSocket module (^5.0). Install when config/websocket.php is present."
},
"autoload": {
Expand Down
112 changes: 109 additions & 3 deletions docs/view.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

```php
return [
// 模板驱动:twig(默认)或 liquid(需 composer require liquid/liquid)
'driver' => 'twig',

// 模板根目录
'view_path' => app_path('view'),

Expand All @@ -29,11 +32,12 @@ return [

| 配置项 | 类型 | 默认值 | 说明 |
| ------------ | -------- | ----------------------- | -------------------------------------- |
| `driver` | string | `'twig'` | 模板引擎驱动,可选 `twig` 或 `liquid` |
| `view_path` | string | `app_path('view')` | 模板文件所在目录 |
| `cache_path` | string | `runtime_path('twig')` | Twig 编译缓存目录 |
| `debug` | bool | `false` | 是否开启 Twig 调试模式 |
| `cache_path` | string | `runtime_path('twig')` | 模板编译缓存目录 |
| `debug` | bool | `false` | 是否开启调试模式 |
| `base_url` | string | `''` | 静态资源 URL 前缀,用于 `asset()`/`url()` |
| `extensions` | string[] | `['.twig', '.html']` | 允许的模板后缀,按查找优先级排序 |
| `extensions` | string[] | `['.twig', '.html']` | 允许的模板后缀,按查找优先级排序(liquid 驱动默认为 `['.liquid']`) |

## 模板后缀

Expand Down Expand Up @@ -181,3 +185,105 @@ $twig->addFilter(new \Twig\TwigFilter('money', fn ($value) => number_format((flo
```bash
php lee run --host=127.0.0.1 --port=8000
```

## 切换到 Liquid 驱动

框架内置了基于 [liquid/liquid](https://github.com/kalimatas/php-liquid) 的 Liquid 驱动,可通过配置一键切换,无需编写适配代码。

### 1. 安装依赖

```bash
composer require liquid/liquid
```

### 2. 修改配置

在 `config/view.php` 中将 `driver` 设为 `liquid`:

```php
return [
'driver' => 'liquid',
'view_path' => app_path('view'),
'cache_path' => runtime_path('liquid'),
'extensions' => ['.liquid'],
// ...
];
```

切换后,`view()`、`asset()` 辅助函数及 `app('view')` 自动使用 Liquid 驱动,控制器代码无需改动:

```php
public function index()
{
return view('user/profile', ['name' => 'Lychee']);
}
```

### Liquid 模板示例

```liquid
{# app/view/user/profile.liquid #}
<h1>Hello, {{ name }}!</h1>

{% if user %}
<p>Email: {{ user.email }}</p>
{% endif %}

{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
```

> **注意**:框架内置的异常页面(调试模式的堆栈页、生产环境的通用错误页)始终使用 Twig 渲染,不受驱动切换影响,确保异常情况下也能正常展示错误信息。

## 自定义模板驱动

如需使用 Twig、Liquid 之外的其他模板引擎(如 Blade、Smarty),可实现 `Lychee\view\ViewInterface` 接口并通过容器绑定覆盖。

### 1. 实现 ViewInterface

```php
<?php
// app/view/CustomDriver.php

namespace App\view;

use Lychee\view\ViewInterface;

class CustomDriver implements ViewInterface
{
public function __construct(
protected string $viewPath,
protected string $baseUrl = '',
) {}

public function render(string $template, array $data = []): string
{
// 你的渲染逻辑
}

public function exists(string $template): bool
{
return is_file($this->viewPath . '/' . $template);
}

public function asset(string $path): string
{
return $this->baseUrl . '/' . ltrim($path, '/');
}
}
```

### 2. 注册到容器

在 `app/common.php` 中绑定(加载时机早于视图模块初始化):

```php
app()->instance(\Lychee\view\ViewInterface::class, new \App\view\CustomDriver(
viewPath: app_path('view'),
baseUrl: config('view.base_url', ''),
));
```

移除绑定即可恢复框架默认的 Twig 驱动。

10 changes: 9 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ parameters:
- tests
checkExplicitMixed: false
bootstrapFiles:
- tests/stub/bootstrap.php
- tests/stub/bootstrap.php
ignoreErrors:
# Liquid 驱动依赖可选的 liquid/liquid 包,未安装时静态分析无法解析其符号
-
identifier: class.notFound
path: src/view/driver/Liquid.php
-
identifier: constructor.unusedParameter
path: src/view/driver/Liquid.php
54 changes: 35 additions & 19 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Lychee\session\Session;
use Lychee\view\ExceptionRenderer;
use Lychee\view\View;
use Lychee\view\ViewInterface;
use Lychee\websocket\command\ServerCommand;
use Lychee\websocket\WebSocketServer;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -555,28 +556,43 @@ private function resolveMigrationPdo(): ?PDO

private function bootView(): void
{
/** @var Config $config */
$config = $this->container->get('config');
$viewConfig = $config->get('view', []);

$viewPath = (string) ($viewConfig['view_path'] ?? ($this->basePath . '/app/view'));
$cachePath = (string) ($viewConfig['cache_path'] ?? ($this->container->runtimePath . 'twig'));
$debug = (bool) ($viewConfig['debug'] ?? false);
$baseUrl = (string) ($viewConfig['base_url'] ?? '');
$extensions = (array) ($viewConfig['extensions'] ?? ['.twig', '.html']);
// 若应用侧已通过容器绑定自定义模板驱动,则优先使用
if ($this->container->has(ViewInterface::class)) {
$view = $this->container->get(ViewInterface::class);
} else {
/** @var Config $config */
$config = $this->container->get('config');
$viewConfig = $config->get('view', []);

$driver = (string) ($viewConfig['driver'] ?? 'twig');
$viewPath = (string) ($viewConfig['view_path'] ?? ($this->basePath . '/app/view'));
$cachePath = (string) ($viewConfig['cache_path'] ?? ($this->container->runtimePath . 'twig'));
$debug = (bool) ($viewConfig['debug'] ?? false);
$baseUrl = (string) ($viewConfig['base_url'] ?? '');

if (!is_dir($viewPath)) {
@mkdir($viewPath, 0777, true);
}

if (!is_dir($viewPath)) {
@mkdir($viewPath, 0777, true);
$view = match ($driver) {
'liquid' => new \Lychee\view\driver\Liquid(
viewPath: $viewPath,
cachePath: $cachePath,
debug: $debug,
baseUrl: $baseUrl,
extensions: (array) ($viewConfig['extensions'] ?? ['.liquid']),
),
default => new View(
viewPath: $viewPath,
cachePath: $cachePath,
debug: $debug,
baseUrl: $baseUrl,
extensions: (array) ($viewConfig['extensions'] ?? ['.twig', '.html']),
),
};
}

$view = new View(
viewPath: $viewPath,
cachePath: $cachePath,
debug: $debug,
baseUrl: $baseUrl,
extensions: $extensions,
);

$this->container->instance(ViewInterface::class, $view);
$this->container->instance(View::class, $view);
$this->container->instance('view', $view);
}
Expand Down
5 changes: 4 additions & 1 deletion src/console/stubs/config/view.stub
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
declare(strict_types=1);

return [
// 模板驱动:twig(默认)或 liquid(需 composer require liquid/liquid)
'driver' => 'twig',

// 模板根目录
'view_path' => app_path('view'),

Expand All @@ -15,6 +18,6 @@ return [
// 静态资源基础 URL,如配置 CDN 可填 'https://cdn.example.com'
'base_url' => '',

// 允许的模板后缀,按查找优先级排序
// 允许的模板后缀,按查找优先级排序(twig 驱动默认 ['.twig', '.html'],liquid 驱动默认 ['.liquid'])
'extensions' => ['.twig', '.html'],
];
4 changes: 2 additions & 2 deletions src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ function error_message(): string
*/
function view(string $template, array $data = []): string
{
/** @var \Lychee\view\View $view */
/** @var \Lychee\view\ViewInterface $view */
$view = app('view');

return $view->render($template, $data);
Expand All @@ -396,7 +396,7 @@ function view(string $template, array $data = []): string
*/
function asset(string $path): string
{
/** @var \Lychee\view\View $view */
/** @var \Lychee\view\ViewInterface $view */
$view = app('view');

return $view->asset($path);
Expand Down
5 changes: 4 additions & 1 deletion src/view/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
* 基于 Twig 的模板引擎。
*
* 模板文件默认从 app/view 目录读取,编译缓存写入 runtime/twig。
*
* 这是 {@see ViewInterface} 的默认实现;如需切换到 Liquid 等其他模板引擎,
* 实现 ViewInterface 后通过容器覆盖绑定即可。
*/
class View
class View implements ViewInterface
{
protected Environment $twig;

Expand Down
34 changes: 34 additions & 0 deletions src/view/ViewInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Lychee\view;

/**
* 模板引擎驱动接口。
*
* 框架默认实现为基于 Twig 的 {@see View}。如需使用 Liquid、Blade 等其他模板引擎,
* 实现本接口并通过容器绑定覆盖默认驱动即可(无需修改框架核心代码)。
*
* @see View 默认的 Twig 实现
*/
interface ViewInterface
{
/**
* 渲染模板并返回 HTML 字符串。
*
* @param string $template 模板路径(相对模板根目录)
* @param array<string, mixed> $data 传递给模板的数据
*/
public function render(string $template, array $data = []): string;

/**
* 判断模板是否存在。
*/
public function exists(string $template): bool;

/**
* 生成 public 目录下静态资源的 URL。
*/
public function asset(string $path): string;
}
Loading
Loading