Added view-engine component. (#2737)

Co-authored-by: nfangxu <nfangxu@gmail.com>
This commit is contained in:
李铭昕 2020-10-30 16:33:16 +08:00 committed by GitHub
parent 85b2ad4356
commit 199a6e2324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 9189 additions and 3 deletions

View File

@ -139,6 +139,7 @@
"hyperf/utils": "self.version",
"hyperf/validation": "self.version",
"hyperf/view": "self.version",
"hyperf/view-engine": "self.version",
"hyperf/websocket-client": "self.version",
"hyperf/websocket-server": "self.version"
},
@ -153,7 +154,8 @@
"src/filesystem/src/Adapter/AliyunOssHook.php",
"src/nats/src/Functions.php",
"src/translation/src/Functions.php",
"src/utils/src/Functions.php"
"src/utils/src/Functions.php",
"src/view-engine/src/Functions.php"
],
"psr-4": {
"Hyperf\\Amqp\\": "src/amqp/src/",
@ -229,6 +231,7 @@
"Hyperf\\Translation\\": "src/translation/src/",
"Hyperf\\Utils\\": "src/utils/src/",
"Hyperf\\Validation\\": "src/validation/src/",
"Hyperf\\ViewEngine\\": "src/view-engine/src/",
"Hyperf\\View\\": "src/view/src/",
"Hyperf\\Watcher\\": "src/watcher/src/",
"Hyperf\\WebSocketClient\\": "src/websocket-client/src/",
@ -302,6 +305,7 @@
"HyperfTest\\Translation\\": "src/translation/tests/",
"HyperfTest\\Utils\\": "src/utils/tests/",
"HyperfTest\\Validation\\": "src/validation/tests/",
"HyperfTest\\ViewEngine\\": "src/view-engine/tests/",
"HyperfTest\\View\\": "src/view/tests/",
"HyperfTest\\Watcher\\": "src/watcher/tests/",
"HyperfTest\\WebSocketClient\\": "src/websocket-client/tests/",
@ -382,6 +386,7 @@
"Hyperf\\Translation\\ConfigProvider",
"Hyperf\\Utils\\ConfigProvider",
"Hyperf\\Validation\\ConfigProvider",
"Hyperf\\ViewEngine\\ConfigProvider",
"Hyperf\\View\\ConfigProvider",
"Hyperf\\Watcher\\ConfigProvider",
"Hyperf\\WebSocketClient\\ConfigProvider",

View File

@ -43,6 +43,7 @@
* [命令行](zh-cn/command.md)
* [自动化测试](zh-cn/testing.md)
* [视图](zh-cn/view.md)
* [视图引擎](zh-cn/view-engine.md)
* [国际化](zh-cn/translation.md)
* [验证器](zh-cn/validation.md)
* [Session 会话管理](zh-cn/session.md)

528
docs/zh-cn/view-engine.md Normal file
View File

@ -0,0 +1,528 @@
# 视图引擎
> 基于 laravel blade 模板引擎改写, 支持原始 blade 模板引擎的语法.
```
composer require hyperf/view-engine
```
## 生成配置
```
php bin/hyperf.php vendor:publish hyperf/view-engine
```
默认配置如下
> 本组件推荐使用 SYNC 的渲染模式,可以有效减少进程间通信的损耗
```php
return [
'engine' => Hyperf\ViewEngine\HyperfViewEngine::class,
'mode' => Hyperf\View\Mode::SYNC,
'config' => [
'view_path' => BASE_PATH . '/storage/view/',
'cache_path' => BASE_PATH . '/runtime/view/',
],
# 自定义组件注册
'components' => [
// 'alert' => \App\View\Components\Alert::class
],
# 视图命名空间 (主要用于扩展包中)
'namespaces' => [
// 'admin' => BASE_PATH . '/storage/view/vendor/admin',
],
];
```
## 使用
> 本使用教程大量借鉴于 [LearnKu](https://learnku.com),十分感谢 LearnKu 对 PHP 社区做出的贡献。
### 简介
`Blade``Laravel` 提供的一个简单而又强大的模板引擎。和其他流行的 `PHP` 模板引擎不同,`Blade` 并不限制你在视图中使用原生 `PHP` 代码。
所有 `Blade` 视图文件都将被编译成原生的 `PHP` 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 `Blade` 基本上不会给你的应用增加任何负担。
`Blade` 视图文件使用 `.blade.php` 作为文件扩展名,默认被存放在 `storage/view` 目录。
### 模板继承
#### 定义布局
首先,我们来研究一个「主」页面布局。因为大多数 `web` 应用会在不同的页面中使用相同的布局方式,因此可以很方便地定义单个 `Blade` 布局视图:
```blade
<!-- Stored in storage/view/layouts/app.blade.php -->
<html>
<head>
<title>App Name - @yield('title')</title>
</head>
<body>
@section('sidebar')
This is the master sidebar.
@show
<div class="container">
@yield('content')
</div>
</body>
</html>
```
你可以看见,这段程序里包含常见的 `HTML`。但请注意 `@section``@yield` 和指令。如同 `section` 的意思,一个片段,`@section` 指令定义了片段的内容,而 `@yield` 指令则用来显示片段的内容。
现在,我们已经定义好了这个应用程序的布局,接下来,我们定义一个继承此布局的子页面。
#### 布局继承
在定义一个子视图时,使用 `Blade``@extends` 指令指定子视图要「继承」的视图。扩展自 `Blade` 布局的视图可以使用 `@section` 指令向布局片段注入内容。
就如前面的示例中所示,这些片段的内容将由布局中的 `@yield` 指令控制显示:
```blade
<!-- Stored in resources/views/child.blade.php -->
@extends('layouts.app')
@section('title', 'Page Title')
@section('sidebar')
@parent
<p>This is appended to the master sidebar.</p>
@endsection
@section('content')
<p>This is my body content.</p>
@endsection
```
在这个示例中,`sidebar` 片段利用 `@parent` 指令向布局的 `sidebar` 追加(而非覆盖)内容。 在渲染视图时,`@parent` 指令将被布局中的内容替换。
> 和上一个示例相反,这里的 sidebar 片段使用 @endsection 代替 @show 来结尾。 @endsection 指令仅定义了一个片段, @show 则在定义的同时 立即 yield 这个片段。
`@yield` 指令还接受一个默认值作为第二个参数。如果被「yield」的片段未定义则该默认值被渲染
```blade
@yield('content', 'Hyperf')
```
`Blade` 视图可以用 `Hyperf\\ViewEngine\\view` 辅助函数返回:
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use function Hyperf\ViewEngine\view;
/**
* @AutoController(prefix="view")
*/
class ViewController extends Controller
{
public function child()
{
return (string) view('child');
}
}
```
### 显示数据
你可以把变量置于花括号中以在视图中显示数据。 例如,给定下方的路由:
```php
use Hyperf\HttpServer\Router\Router;
use function Hyperf\ViewEngine\view;
Router::get('greeting', function () {
return view('welcome', ['name' => 'Samantha']);
});
```
您可以像如下这样显示 `name` 变量的内容:
```blade
Hello, {{ $name }}.
```
> Blade 的 {{ }} 语句将被 PHP 的 htmlspecialchars 函数自动转义以防范 XSS 攻击。
不仅仅可以显示传递给视图的变量的内容,您亦可输出任何 `PHP` 函数的结果。事实上,您可以在 `Blade` 模板的回显语句放置任何 `PHP` 代码:
```blade
The current UNIX timestamp is {{ time() }}.
```
#### 显示非转义字符
默认情况下,`Blade {{ }}` 语句将被 `PHP``htmlspecialchars` 函数自动转义以防范 `XSS` 攻击。如果您不想您的数据被转义,那么您可使用如下的语法:
```blade
Hello, {!! $name !!}.
```
> 在应用中显示用户提供的数据时请格外小心,请尽可能的使用转义和双引号语法来防范 XSS 攻击。
#### 渲染 JSON
有时,为了初始化一个 `JavaScript` 变量,您可能会向视图传递一个数组并将其渲染成 `JSON`。例如:
```
<script>
var app = <?php echo json_encode($array); ?>;
</script>
```
当然,您亦可使用 `@json` `Blade` 指令来代替手动调用 `json_encode` 方法。`@json` 指令的参数和 `PHP``json_encode` 函数一致:
```html
<script>
var app = @json($array);
var app = @json($array, JSON_PRETTY_PRINT);
</script>
```
> 使用 @json 指令时,您应该只渲染已经存在的变量为 JSON。Blade 模板是基于正则表达式的,如果尝试将一个复杂表达式传递给 @json 指令可能会导致无法预测的错误。
#### HTML 实体编码
默认情况下,`Blade` 将会对 `HTML` 实体进行双重编码。如果您想要禁用此举,您可以监听 `BootApplication` 事件,并调用 `Blade::withoutDoubleEncoding` 方法:
```php
<?php
declare(strict_types=1);
namespace App\Listener;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
use Hyperf\ViewEngine\Blade;
use Psr\Container\ContainerInterface;
/**
* @Listener
*/
class BladeWithoutDoubleEncodingListener implements ListenerInterface
{
/**
* @var ContainerInterface
*/
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function listen(): array
{
return [
BootApplication::class
];
}
public function process(object $event)
{
Blade::withoutDoubleEncoding();
}
}
```
#### Blade & JavaScript 框架
由于许多 `JavaScript` 框架也使用「花括号」来标识将显示在浏览器中的表达式,因此,您可以使用 `@` 符号来表示 `Blade` 渲染引擎应当保持不便。例如:
```blade
<h1>Laravel</h1>
Hello, @{{ name }}.
```
在这个例子中,`@` 符号将被 `Blade` 移除;当然,`Blade` 将不会修改 `{{ name }}` 表达式,取而代之的是 `JavaScript` 模板来对其进行渲染。
`@` 符号也用于转义 `Blade` 指令:
```
{{-- Blade --}}
@@json()
<!-- HTML 输出 -->
@json()
```
如果您在模板中显示很大一部分 `JavaScript` 变量,您可以将 `HTML` 嵌入到 `@verbatim` 指令中,这样,您就不需要在每一个 `Blade` 回显语句前添加 `@` 符号:
```blade
@verbatim
<div class="container">
Hello, {{ name }}.
</div>
@endverbatim
```
### 流程控制
除了模板继承和显示数据以外,`Blade` 还为常见的 `PHP` 控制结构提供了便捷的快捷方式,例如条件语句和循环。这些快捷方式为 `PHP` 控制结构提供了一个非常清晰、简洁的书写方式,同时,还与 `PHP` 中的控制结构保持了相似的语法特性。
#### If 语句
您可以使用 `@if``@elseif``@else` 和 `@endif` 指令构造 `if` 语句。这些指令功能与它们所对应的 `PHP` 语句完全一致:
```blade
@if (count($records) === 1)
I have one record!
@elseif (count($records) > 1)
I have multiple records!
@else
I don't have any records!
@endif
```
为了方便,`Blade` 还提供了一个 `@unless` 指令:
```blade
@unless (is_signed_in())
You are not signed in.
@endunless
```
除了已经讨论过了的条件指令外,`@isset` 和 `@empty` 指令亦可作为它们所对应的 `PHP` 函数的快捷方式:
```blade
@isset($records)
// $records 已经定义但不为空
@endisset
@empty($records)
// $records 为空……
@endempty
```
#### 区块指令
您可以使用 `@hasSection` 指令来判断区块是否有内容:
```blade
@hasSection('navigation')
<div class="pull-right">
@yield('navigation')
</div>
<div class="clearfix"></div>
@endif
```
您可以使用 `@sectionMissing` 指令来判断区块是否没有内容:
```blade
@sectionMissing('navigation')
<div class="pull-right">
@include('default-navigation')
</div>
@endif
```
#### 环境指令
您可以使用 `@production` 指令来判断应用是否处于生产环境:
```blade
@production
// 生产环境特定内容……
@endproduction
```
或者,您可以使用 `@env` 指令来判断应用是否运行于指定的环境:
```blade
@env('staging')
// 应用运行于「staging」环境……
@endenv
@env(['staging', 'production'])
// 应用运行于 「staging」环境或生产环境……
@endenv
```
#### Switch 语句
您可使用 `@switch``@case``@break``@default` 和 `@endswitch` 语句来构造 `Switch` 语句:
```blade
@switch($i)
@case(1)
First case...
@break
@case(2)
Second case...
@break
@default
Default case...
@endswitch
```
#### 循环
除了条件语句,`Blade` 还提供了与 `PHP` 循环结构功能相同的指令。同样,这些语句的功能和它们所对应的 `PHP` 语法一致:
```blade
@for ($i = 0; $i < 10; $i++)
The current value is {{ $i }}
@endfor
@foreach ($users as $user)
<p>This is user {{ $user->id }}</p>
@endforeach
@forelse ($users as $user)
<li>{{ $user->name }}</li>
@empty
<p>No users</p>
@endforelse
@while (true)
<p>I'm looping forever.</p>
@endwhile
```
> 循环时,您可以使用 循环变量 去获取有关循环的有价值的信息,例如,您处于循环的第一个迭代亦或是处于最后一个迭代。
在使用循环的时候,您可以终止循环或跳过当前迭代:
```blade
@foreach ($users as $user)
@if ($user->type == 1)
@continue
@endif
<li>{{ $user->name }}</li>
@if ($user->number == 5)
@break
@endif
@endforeach
```
您可以在指令的单独一行中声明一个条件语句:
```blade
@foreach ($users as $user)
@continue($user->type == 1)
<li>{{ $user->name }}</li>
@break($user->number == 5)
@endforeach
```
#### Loop 变量
循环时,循环内部可以使用 `$loop` 变量。该变量提供了访问一些诸如当前的循环索引和此次迭代是首次或是末次这样的信息的方式:
```blade
@foreach ($users as $user)
@if ($loop->first)
This is the first iteration.
@endif
@if ($loop->last)
This is the last iteration.
@endif
<p>This is user {{ $user->id }}</p>
@endforeach
```
如果您在嵌套循环中,您可以使用循环的 `$loop` 的变量的 `parent` 属性访问父级循环:
```blade
@foreach ($users as $user)
@foreach ($user->posts as $post)
@if ($loop->parent->first)
This is first iteration of the parent loop.
@endif
@endforeach
@endforeach
```
`$loop` 变量还包含各种各样有用的属性:
| 属性 | 备注 |
|:--:|:--:|
| `$loop->index` | 当前迭代的索引(从 0 开始)。|
| `$loop->iteration` | 当前循环的迭代次数(从 1 开始)。|
| `$loop->remaining` | 循环剩余的迭代次数。|
| `$loop->count` | 被迭代的数组的元素个数。|
| `$loop->first` | 当前迭代是否是循环的首次迭代。|
| `$loop->last` | 当前迭代是否是循环的末次迭代。|
| `$loop->even` | 当前循环的迭代次数是否是偶数。|
| `$loop->odd` | 当前循环的迭代次数是否是奇数。|
| `$loop->depth` | 当前循环的嵌套深度。|
| `$loop->parent` | 嵌套循环中的父级循环。|
#### 注释
`Blade` 也允许您在视图中定义注释。但是和 `HTML` 注释不同, `Blade` 注释不会被包含在应用返回的 `HTML` 中:
```blade
{{-- This comment will not be present in the rendered HTML --}}
```
#### PHP
在许多情况下,嵌入 `PHP` 代码到您的视图中是很有用的。您可以在模板中使用 `Blade``@php` 指令执行原生的 `PHP` 代码块:
```blade
@php
//
@endphp
```
> 尽管 Blade 提供了这个功能,频繁使用它可能使得您的模板中嵌入过多的逻辑。
#### @once 指令
`@once` 指令允许您定义模板的一部分内容,这部分内容在每一个渲染周期中只会被计算一次。
该指令在使用 `堆栈` 推送一段特定的 `JavaScript` 代码到页面的头部环境下是很有用的。
例如,如果您想要在循环中渲染一个特定的 `组件`,您可能希望仅在组件渲染的首次推送 `JavaScript` 代码到头部:
```blade
@once
@push('scripts')
<script>
// 您自定义的 JavaScript 代码
</script>
@endpush
@endonce
```
## 可选中间件
- Hyperf\ViewEngine\Http\Middleware\ShareErrorsFromSession::class
自动将 `session` 中的 `errors` 共享给视图,依赖 `hyperf/session` 组件
- Hyperf\ViewEngine\Http\Middleware\ValidationExceptionHandle::class
自动捕捉 `validation` 中的异常加入到 `session` 中,依赖 `hyperf/session``hyperf/validation` 组件
## 其他命令
自动安装 `view-engine`、`translation` 和 `validation` 组件相关配置
```
php bin/hyperf.php view:publish
```

View File

@ -192,4 +192,3 @@ class ViewController
```
Hello, Hyperf. You are using blade template now.
```

2
src/view-engine/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/tests export-ignore
/.github export-ignore

View File

@ -0,0 +1,25 @@
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Release
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false

22
src/view-engine/LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Copyright (c) Hyperf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,64 @@
{
"name": "hyperf/view-engine",
"type": "library",
"license": "MIT",
"keywords": [
"php",
"hyperf",
"view",
"engine"
],
"description": "",
"require": {
"php": ">=7.2",
"ext-json": "*",
"hyperf/config": "~2.0.0",
"hyperf/di": "~2.0.0",
"hyperf/event": "~2.0.0",
"hyperf/utils": "~2.0.0",
"hyperf/view": "~2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"hyperf/session": "~2.0.0",
"hyperf/validation": "~2.0.0",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "*",
"psr/http-message": "^1.0"
},
"suggest": {
"hyperf/session": "Required to use ShareErrorsFromSession.",
"hyperf/validation": "Required to use ShareErrorsFromSession and ValidationExceptionHandle."
},
"autoload": {
"psr-4": {
"Hyperf\\ViewEngine\\": "src/"
},
"files": [
"src/Functions.php"
]
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\ViewEngine\\": "tests/"
}
},
"bin": [],
"config": {
"sort-packages": true
},
"scripts": {
"cs-fix": "php-cs-fixer fix $1",
"analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./src ./tests",
"test": "./vendor/bin/phpunit -c phpunit.xml --color=always",
"test-coverage": "php -d zend_extension=xdebug ./vendor/bin/phpunit -c phpunit.xml --color=always --coverage-html .codeCoverage"
},
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
},
"hyperf": {
"config": "Hyperf\\ViewEngine\\ConfigProvider"
}
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
use Hyperf\View\Mode;
use Hyperf\ViewEngine\HyperfViewEngine;
return [
'engine' => HyperfViewEngine::class,
'mode' => Mode::SYNC,
'config' => [
'view_path' => BASE_PATH . '/storage/view/',
'cache_path' => BASE_PATH . '/runtime/view/',
'charset' => 'UTF-8',
],
# 自定义组件
'components' => [
// 'alert' => \App\View\Components\Alert::class
],
# 视图命名空间 (用于扩展包中)
'namespaces' => [
// 'admin' => BASE_PATH . '/storage/view/vendor/admin',
],
];

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Utils\ApplicationContext;
use Hyperf\ViewEngine\Compiler\CompilerInterface;
use Psr\Container\ContainerInterface;
/**
* Class Blade.
*
* @method static array getClassComponentAliases()
* @method static array getCustomDirectives()
* @method static array getExtensions()
* @method static bool check(string $name, array ...$parameters)
* @method static string compileString(string $value)
* @method static string getPath()
* @method static string stripParentheses(string $expression)
* @method static void aliasComponent(string $path, null|string $alias = null)
* @method static void aliasInclude(string $path, null|string $alias = null)
* @method static void compile(null|string $path = null)
* @method static void component(string $class, null|string $alias = null, string $prefix = '')
* @method static void components(array $components, string $prefix = '')
* @method static void directive(string $name, callable $handler)
* @method static void extend(callable $compiler)
* @method static void if (string $name, callable $callback)
* @method static void include (string $path, string|null $alias = null)
* @method static void precompiler(callable $precompiler)
* @method static void setEchoFormat(string $format)
* @method static void setPath(string $path)
* @method static void withDoubleEncoding()
* @method static void withoutComponentTags()
* @method static void withoutDoubleEncoding()
*/
class Blade
{
/**
* @var null|ContainerInterface
*/
protected static $container;
public static function __callStatic($method, $args)
{
return static::resolve()->{$method}(...$args);
}
public static function resolve()
{
return static::container()
->get(CompilerInterface::class);
}
public static function container()
{
return static::$container ?: static::$container = ApplicationContext::getContainer();
}
public static function config($key, $default = '')
{
$key = 'view.' . $key;
return static::container()
->get(ConfigInterface::class)
->get($key, $default);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Command;
use Hyperf\Command\Command as HyperfCommand;
class ViewPublishCommand extends HyperfCommand
{
protected $signature = 'view:publish {--f|force}';
protected $packages = [
'hyperf/session',
'hyperf/validation',
'hyperf/translation',
];
public function handle()
{
$this->call('vendor:publish', [
'package' => 'hyperf/view-engine',
'--force' => true,
]);
foreach ($this->packages as $package) {
$this->call('vendor:publish', [
'package' => $package,
'--force' => $this->input->getOption('force') === false ? false : true,
]);
}
}
}

View File

@ -0,0 +1,735 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Collection;
use Hyperf\Utils\Str;
use InvalidArgumentException;
class BladeCompiler extends Compiler implements CompilerInterface
{
use Concern\CompilesComments;
use Concern\CompilesComponents;
use Concern\CompilesConditionals;
use Concern\CompilesEchos;
use Concern\CompilesErrors;
use Concern\CompilesIncludes;
use Concern\CompilesInjections;
use Concern\CompilesJson;
use Concern\CompilesLayouts;
use Concern\CompilesLoops;
use Concern\CompilesRawPhp;
use Concern\CompilesStacks;
use Concern\CompilesTranslations;
/**
* All of the registered extensions.
*
* @var array
*/
protected $extensions = [];
/**
* All custom "directive" handlers.
*
* @var array
*/
protected $customDirectives = [];
/**
* All custom "condition" handlers.
*
* @var array
*/
protected $conditions = [];
/**
* All of the registered precompilers.
*
* @var array
*/
protected $precompilers = [];
/**
* The file currently being compiled.
*
* @var string
*/
protected $path;
/**
* All of the available compiler functions.
*
* @var array
*/
protected $compilers = [
// 'Comments',
'Extensions',
'Statements',
'Echos',
];
/**
* Array of opening and closing tags for raw echos.
*
* @var array
*/
protected $rawTags = ['{!!', '!!}'];
/**
* Array of opening and closing tags for regular echos.
*
* @var array
*/
protected $contentTags = ['{{', '}}'];
/**
* Array of opening and closing tags for escaped echos.
*
* @var array
*/
protected $escapedTags = ['{{{', '}}}'];
/**
* The "regular" / legacy echo string format.
*
* @var string
*/
protected $echoFormat = '\Hyperf\ViewEngine\T::e(%s)';
/**
* Array of footer lines to be added to template.
*
* @var array
*/
protected $footer = [];
/**
* Array to temporary store the raw blocks found in the template.
*
* @var array
*/
protected $rawBlocks = [];
/**
* The array of class component aliases and their class names.
*
* @var array
*/
protected $classComponentAliases = [];
/**
* The array of class component namespaces to autoload from.
*
* @var array
*/
protected $classComponentNamespaces = [];
/**
* Indicates if component tags should be compiled.
*
* @var bool
*/
protected $compilesComponentTags = true;
/**
* Compile the view at the given path.
*
* @param null|string $path
*/
public function compile($path = null)
{
if ($path) {
$this->setPath($path);
}
if (! is_null($this->cachePath)) {
$contents = $this->compileString($this->files->get($this->getPath()));
if (! empty($this->getPath())) {
$contents = $this->appendFilePath($contents);
}
$this->files->put(
$this->getCompiledPath($this->getPath()),
$contents
);
}
}
/**
* Get the path currently being compiled.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* Set the path currently being compiled.
*
* @param string $path
*/
public function setPath($path)
{
$this->path = $path;
}
/**
* Compile the given Blade template contents.
*
* @param string $value
* @return string
*/
public function compileString($value)
{
[$this->footer, $result] = [[], ''];
// First we will compile the Blade component tags. This is a precompile style
// step which compiles the component Blade tags into @component directives
// that may be used by Blade. Then we should call any other precompilers.
$value = $this->compileComponentTags(
$this->compileComments($this->storeUncompiledBlocks($value))
);
foreach ($this->precompilers as $precompiler) {
$value = call_user_func($precompiler, $value);
}
// Here we will loop through all of the tokens returned by the Zend lexer and
// parse each one into the corresponding valid PHP. We will then have this
// template as the correctly rendered PHP that can be rendered natively.
foreach (token_get_all($value) as $token) {
$result .= is_array($token) ? $this->parseToken($token) : $token;
}
if (! empty($this->rawBlocks)) {
$result = $this->restoreRawContent($result);
}
// If there are any footer lines that need to get added to a template we will
// add them here at the end of the template. This gets used mainly for the
// template inheritance via the extends keyword that should be appended.
if (count($this->footer) > 0) {
$result = $this->addFooters($result);
}
return $result;
}
/**
* Strip the parentheses from the given expression.
*
* @param string $expression
* @return string
*/
public function stripParentheses($expression)
{
if (Str::startsWith($expression, '(')) {
$expression = substr($expression, 1, -1);
}
return $expression;
}
/**
* Register a custom Blade compiler.
*/
public function extend(callable $compiler)
{
$this->extensions[] = $compiler;
}
/**
* Get the extensions used by the compiler.
*
* @return array
*/
public function getExtensions()
{
return $this->extensions;
}
/**
* Register an "if" statement directive.
*
* @param string $name
*/
public function if($name, callable $callback)
{
$this->conditions[$name] = $callback;
$this->directive($name, function ($expression) use ($name) {
return $expression !== ''
? "<?php if (\\Hyperf\\ViewEngine\\Blade::check('{$name}', {$expression})): ?>"
: "<?php if (\\Hyperf\\ViewEngine\\Blade::check('{$name}')): ?>";
});
$this->directive('unless' . $name, function ($expression) use ($name) {
return $expression !== ''
? "<?php if (! \\Hyperf\\ViewEngine\\Blade::check('{$name}', {$expression})): ?>"
: "<?php if (! \\Hyperf\\ViewEngine\\Blade::check('{$name}')): ?>";
});
$this->directive('else' . $name, function ($expression) use ($name) {
return $expression !== ''
? "<?php elseif (\\Hyperf\\ViewEngine\\Blade::check('{$name}', {$expression})): ?>"
: "<?php elseif (\\Hyperf\\ViewEngine\\Blade::check('{$name}')): ?>";
});
$this->directive('end' . $name, function () {
return '<?php endif; ?>';
});
}
/**
* Check the result of a condition.
*
* @param string $name
* @param array $parameters
* @return bool
*/
public function check($name, ...$parameters)
{
return call_user_func($this->conditions[$name], ...$parameters);
}
/**
* Register a class-based component alias directive.
*
* @param string $class
* @param null|string $alias
* @param string $prefix
*/
public function component($class, $alias = null, $prefix = '')
{
if (! is_null($alias) && Str::contains($alias, '\\')) {
[$class, $alias] = [$alias, $class];
}
if (is_null($alias)) {
$alias = Str::contains($class, '\\View\\Components\\')
? collect(explode('\\', Str::after($class, '\\View\\Components\\')))->map(function ($segment) {
return Str::kebab($segment);
})->implode(':')
: Str::kebab(class_basename($class));
}
if (! empty($prefix)) {
$alias = $prefix . '-' . $alias;
}
$this->classComponentAliases[$alias] = $class;
}
/**
* Register an array of class-based components.
*
* @param string $prefix
*/
public function components(array $components, $prefix = '')
{
foreach ($components as $key => $value) {
if (is_numeric($key)) {
static::component($value, null, $prefix);
} else {
static::component($key, $value, $prefix);
}
}
}
/**
* Get the registered class component aliases.
*
* @return array
*/
public function getClassComponentAliases()
{
return $this->classComponentAliases;
}
/**
* Register a class-based component namespace.
*
* @param string $namespace
* @param string $prefix
*/
public function componentNamespace($namespace, $prefix)
{
$this->classComponentNamespaces[$prefix] = $namespace;
}
/**
* Get the registered class component namespaces.
*
* @return array
*/
public function getClassComponentNamespaces()
{
return $this->classComponentNamespaces;
}
/**
* Register a component alias directive.
*
* @param string $path
* @param null|string $alias
*/
public function aliasComponent($path, $alias = null)
{
$alias = $alias ?: Arr::last(explode('.', $path));
$this->directive($alias, function ($expression) use ($path) {
return $expression
? "<?php \$__env->startComponent('{$path}', {$expression}); ?>"
: "<?php \$__env->startComponent('{$path}'); ?>";
});
$this->directive('end' . $alias, function ($expression) {
return '<?php echo $__env->renderComponent(); ?>';
});
}
/**
* Register an include alias directive.
*
* @param string $path
* @param null|string $alias
*/
public function include($path, $alias = null)
{
return $this->aliasInclude($path, $alias);
}
/**
* Register an include alias directive.
*
* @param string $path
* @param null|string $alias
*/
public function aliasInclude($path, $alias = null)
{
$alias = $alias ?: Arr::last(explode('.', $path));
$this->directive($alias, function ($expression) use ($path) {
$expression = $this->stripParentheses($expression) ?: '[]';
return "<?php echo \$__env->make('{$path}', {$expression}, \\Hyperf\\Utils\\Arr::except(get_defined_vars(), ['__data', '__path']))->render(); ?>";
});
}
/**
* Register a handler for custom directives.
*
* @param string $name
*
* @throws \InvalidArgumentException
*/
public function directive($name, callable $handler)
{
if (! preg_match('/^\w+(?:::\w+)?$/x', $name)) {
throw new InvalidArgumentException("The directive name [{$name}] is not valid. Directive names must only contain alphanumeric characters and underscores.");
}
$this->customDirectives[$name] = $handler;
}
/**
* Get the list of custom directives.
*
* @return array
*/
public function getCustomDirectives()
{
return $this->customDirectives;
}
/**
* Register a new precompiler.
*/
public function precompiler(callable $precompiler)
{
$this->precompilers[] = $precompiler;
}
/**
* Set the echo format to be used by the compiler.
*
* @param string $format
*/
public function setEchoFormat($format)
{
$this->echoFormat = $format;
}
/**
* Set the "echo" format to double encode entities.
*/
public function withDoubleEncoding()
{
$this->setEchoFormat('\Hyperf\ViewEngine\T::e(%s, true)');
}
/**
* Set the "echo" format to not double encode entities.
*/
public function withoutDoubleEncoding()
{
$this->setEchoFormat('\Hyperf\ViewEngine\T::e(%s, false)');
}
/**
* Indicate that component tags should not be compiled.
*/
public function withoutComponentTags()
{
$this->compilesComponentTags = false;
}
/**
* Append the file path to the compiled string.
*
* @param string $contents
* @return string
*/
protected function appendFilePath($contents)
{
$tokens = $this->getOpenAndClosingPhpTokens($contents);
if ($tokens->isNotEmpty() && $tokens->last() !== T_CLOSE_TAG) {
$contents .= ' ?>';
}
return $contents . "<?php /**PATH {$this->getPath()} ENDPATH**/ ?>";
}
/**
* Get the open and closing PHP tag tokens from the given string.
*
* @param string $contents
* @return Collection
*/
protected function getOpenAndClosingPhpTokens($contents)
{
return collect(token_get_all($contents))
->pluck('0')
->filter(function ($token) {
return in_array($token, [T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG]);
});
}
/**
* Store the blocks that do not receive compilation.
*
* @param string $value
* @return string
*/
protected function storeUncompiledBlocks($value)
{
if (strpos($value, '@verbatim') !== false) {
$value = $this->storeVerbatimBlocks($value);
}
if (strpos($value, '@php') !== false) {
$value = $this->storePhpBlocks($value);
}
return $value;
}
/**
* Store the verbatim blocks and replace them with a temporary placeholder.
*
* @param string $value
* @return string
*/
protected function storeVerbatimBlocks($value)
{
return preg_replace_callback('/(?<!@)@verbatim(.*?)@endverbatim/s', function ($matches) {
return $this->storeRawBlock($matches[1]);
}, $value);
}
/**
* Store the PHP blocks and replace them with a temporary placeholder.
*
* @param string $value
* @return string
*/
protected function storePhpBlocks($value)
{
return preg_replace_callback('/(?<!@)@php(.*?)@endphp/s', function ($matches) {
return $this->storeRawBlock("<?php{$matches[1]}?>");
}, $value);
}
/**
* Store a raw block and return a unique raw placeholder.
*
* @param string $value
* @return string
*/
protected function storeRawBlock($value)
{
return $this->getRawPlaceholder(
array_push($this->rawBlocks, $value) - 1
);
}
/**
* Compile the component tags.
*
* @param string $value
* @return string
*/
protected function compileComponentTags($value)
{
if (! $this->compilesComponentTags) {
return $value;
}
return (new ComponentTagCompiler(
$this->classComponentAliases,
$this->classComponentNamespaces,
$this
))->compile($value);
}
/**
* Replace the raw placeholders with the original code stored in the raw blocks.
*
* @param string $result
* @return string
*/
protected function restoreRawContent($result)
{
$result = preg_replace_callback('/' . $this->getRawPlaceholder('(\d+)') . '/', function ($matches) {
return $this->rawBlocks[$matches[1]];
}, $result);
$this->rawBlocks = [];
return $result;
}
/**
* Get a placeholder to temporary mark the position of raw blocks.
*
* @param int|string $replace
* @return string
*/
protected function getRawPlaceholder($replace)
{
return str_replace('#', $replace, '@__raw_block_#__@');
}
/**
* Add the stored footers onto the given content.
*
* @param string $result
* @return string
*/
protected function addFooters($result)
{
return ltrim($result, "\n")
. "\n" . implode("\n", array_reverse($this->footer));
}
/**
* Parse the tokens from the template.
*
* @param array $token
* @return string
*/
protected function parseToken($token)
{
[$id, $content] = $token;
if ($id == T_INLINE_HTML) {
foreach ($this->compilers as $type) {
$content = $this->{"compile{$type}"}($content);
}
}
return $content;
}
/**
* Execute the user defined extensions.
*
* @param string $value
* @return string
*/
protected function compileExtensions($value)
{
foreach ($this->extensions as $compiler) {
$value = $compiler($value, $this);
}
return $value;
}
/**
* Compile Blade statements that start with "@".
*
* @param string $value
* @return string
*/
protected function compileStatements($value)
{
return preg_replace_callback(
'/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x',
function ($match) {
return $this->compileStatement($match);
},
$value
);
}
/**
* Compile a single Blade @ statement.
*
* @param array $match
* @return string
*/
protected function compileStatement($match)
{
if (Str::contains($match[1], '@')) {
$match[0] = isset($match[3]) ? $match[1] . $match[3] : $match[1];
} elseif (isset($this->customDirectives[$match[1]])) {
$match[0] = $this->callCustomDirective($match[1], Arr::get($match, 3));
} elseif (method_exists($this, $method = 'compile' . ucfirst($match[1]))) {
$match[0] = $this->{$method}(Arr::get($match, 3));
}
return isset($match[3]) ? $match[0] : $match[0] . $match[2];
}
/**
* Call the given directive with the given value.
*
* @param string $name
* @param null|string $value
* @return string
*/
protected function callCustomDirective($name, $value)
{
if (Str::startsWith($value, '(') && Str::endsWith($value, ')')) {
$value = Str::substr($value, 1, -1);
}
return call_user_func($this->customDirectives[$name], trim($value));
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler;
use Hyperf\Utils\Filesystem\Filesystem;
use InvalidArgumentException;
abstract class Compiler
{
/**
* The Filesystem instance.
*
* @var Filesystem
*/
protected $files;
/**
* Get the cache path for the compiled views.
*
* @var null|string
*/
protected $cachePath;
/**
* Create a new compiler instance.
*
* @param string $cachePath
*
* @throws InvalidArgumentException
*/
public function __construct(Filesystem $files, $cachePath)
{
if (! $cachePath) {
throw new InvalidArgumentException('Please provide a valid cache path.');
}
$this->files = $files;
if (! $this->files->exists($cachePath)) {
$this->files->makeDirectory($cachePath);
}
$this->cachePath = $cachePath;
}
/**
* Get the path to the compiled version of a view.
*
* @param string $path
* @return string
*/
public function getCompiledPath($path)
{
return $this->cachePath . '/' . sha1($path) . '.php';
}
/**
* Determine if the view at the given path is expired.
*
* @param string $path
* @return bool
*/
public function isExpired($path)
{
$compiled = $this->getCompiledPath($path);
// If the compiled file doesn't exist we will indicate that the view is expired
// so that it can be re-compiled. Else, we will verify the last modification
// of the views is less than the modification times of the compiled views.
if (! $this->files->exists($compiled)) {
return true;
}
return $this->files->lastModified($path) >=
$this->files->lastModified($compiled);
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler;
interface CompilerInterface
{
/**
* Get the path to the compiled version of a view.
*
* @param string $path
* @return string
*/
public function getCompiledPath($path);
/**
* Determine if the given view is expired.
*
* @param string $path
* @return bool
*/
public function isExpired($path);
/**
* Compile the view at the given path.
*
* @param string $path
*/
public function compile($path);
}

View File

@ -0,0 +1,537 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler;
use Hyperf\Utils\Filesystem\Filesystem;
use Hyperf\Utils\Str;
use Hyperf\ViewEngine\Blade;
use Hyperf\ViewEngine\Component\AnonymousComponent;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\Contract\FinderInterface;
use InvalidArgumentException;
use ReflectionClass;
class ComponentTagCompiler
{
/**
* The Blade compiler instance.
*
* @var BladeCompiler
*/
protected $blade;
/**
* The component class aliases.
*
* @var array
*/
protected $aliases = [];
/**
* The component class namespaces.
*
* @var array
*/
protected $namespaces = [];
/**
* The "bind:" attributes that have been compiled for the current component.
*
* @var array
*/
protected $boundAttributes = [];
/**
* Create new component tag compiler.
*
* @param ?BladeCompiler $blade
*/
public function __construct(array $aliases = [], array $namespaces = [], ?BladeCompiler $blade = null)
{
$this->aliases = $aliases;
$this->namespaces = $namespaces;
$this->blade = $blade ?: new BladeCompiler(new Filesystem(), sys_get_temp_dir());
}
/**
* Compile the component and slot tags within the given string.
*
* @return string
*/
public function compile(string $value)
{
$value = $this->compileSlots($value);
return $this->compileTags($value);
}
/**
* Compile the tags within the given string.
*
* @throws InvalidArgumentException
* @return string
*/
public function compileTags(string $value)
{
$value = $this->compileSelfClosingTags($value);
$value = $this->compileOpeningTags($value);
return $this->compileClosingTags($value);
}
/**
* Get the component class for a given component alias.
*
* @throws InvalidArgumentException
* @return string
*/
public function componentClass(string $component)
{
$viewFactory = Blade::container()->get(FactoryInterface::class);
if (isset($this->aliases[$component])) {
if (class_exists($alias = $this->aliases[$component])) {
return $alias;
}
if ($viewFactory->exists($alias)) {
return $alias;
}
throw new InvalidArgumentException(
"Unable to locate class or view [{$alias}] for component [{$component}]."
);
}
if ($class = $this->findClassByComponent($component)) {
return $class;
}
if (class_exists($class = $this->guessClassName($component))) {
return $class;
}
if ($viewFactory->exists($view = $this->guessViewName($component))) {
return $view;
}
throw new InvalidArgumentException(
"Unable to locate a class or view for component [{$component}]."
);
}
/**
* Find the class for the given component using the registered namespaces.
*
* @return null|string|void
*/
public function findClassByComponent(string $component)
{
$segments = explode('::', $component);
$prefix = $segments[0];
if (! isset($this->namespaces[$prefix]) || ! isset($segments[1])) {
return;
}
if (class_exists($class = $this->namespaces[$prefix] . '\\' . $this->formatClassName($segments[1]))) {
return $class;
}
}
/**
* Guess the class name for the given component.
*
* @return string
*/
public function guessClassName(string $component)
{
$class = $this->formatClassName($component);
return 'App\\View\\Components\\' . $class;
}
/**
* Format the class name for the given component.
*
* @return string
*/
public function formatClassName(string $component)
{
$componentPieces = array_map(function ($componentPiece) {
return ucfirst(Str::camel($componentPiece));
}, explode('.', $component));
return implode('\\', $componentPieces);
}
/**
* Guess the view name for the given component.
*
* @param string $name
* @return string
*/
public function guessViewName($name)
{
$prefix = 'components.';
$delimiter = FinderInterface::HINT_PATH_DELIMITER;
if (Str::contains($name, $delimiter)) {
return Str::replaceFirst($delimiter, $delimiter . $prefix, $name);
}
return $prefix . $name;
}
/**
* Partition the data and extra attributes from the given array of attributes.
*
* @param string $class
* @return array
*/
public function partitionDataAndAttributes($class, array $attributes)
{
// If the class doesn't exists, we'll assume it's a class-less component and
// return all of the attributes as both data and attributes since we have
// now way to partition them. The user can exclude attributes manually.
if (! class_exists($class)) {
return [collect($attributes), collect($attributes)];
}
$constructor = (new ReflectionClass($class))->getConstructor();
$parameterNames = $constructor
? collect($constructor->getParameters())->map->getName()->all()
: [];
return collect($attributes)->partition(function ($value, $key) use ($parameterNames) {
return in_array(Str::camel($key), $parameterNames);
})->all();
}
/**
* Compile the slot tags within the given string.
*
* @return string
*/
public function compileSlots(string $value)
{
$value = preg_replace_callback('/<\s*x[\-\:]slot\s+(:?)name=(?<name>(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+))\s*>/', function ($matches) {
$name = $this->stripQuotes($matches['name']);
if ($matches[1] !== ':') {
$name = "'{$name}'";
}
return " @slot({$name}) ";
}, $value);
return preg_replace('/<\/\s*x[\-\:]slot[^>]*>/', ' @endslot', $value);
}
/**
* Strip any quotes from the given string.
*
* @return string
*/
public function stripQuotes(string $value)
{
return Str::startsWith($value, ['"', '\''])
? substr($value, 1, -1)
: $value;
}
/**
* Compile the opening tags within the given string.
*
* @throws InvalidArgumentException
* @return string
*/
protected function compileOpeningTags(string $value)
{
$pattern = "/
<
\\s*
x[-\\:]([\\w\\-\\:\\.]*)
(?<attributes>
(?:
\\s+
(?:
(?:
\\{\\{\\s*\\\$attributes(?:[^}]+?)?\\s*\\}\\}
)
|
(?:
[\\w\\-:.@]+
(
=
(?:
\\\"[^\\\"]*\\\"
|
\\'[^\\']*\\'
|
[^\\'\\\"=<>]+
)
)?
)
)
)*
\\s*
)
(?<![\\/=\\-])
>
/x";
return preg_replace_callback($pattern, function (array $matches) {
$this->boundAttributes = [];
$attributes = $this->getAttributesFromAttributeString($matches['attributes']);
return $this->componentString($matches[1], $attributes);
}, $value);
}
/**
* Compile the self-closing tags within the given string.
*
* @throws InvalidArgumentException
* @return string
*/
protected function compileSelfClosingTags(string $value)
{
$pattern = "/
<
\\s*
x[-\\:]([\\w\\-\\:\\.]*)
\\s*
(?<attributes>
(?:
\\s+
(?:
(?:
\\{\\{\\s*\\\$attributes(?:[^}]+?)?\\s*\\}\\}
)
|
(?:
[\\w\\-:.@]+
(
=
(?:
\\\"[^\\\"]*\\\"
|
\\'[^\\']*\\'
|
[^\\'\\\"=<>]+
)
)?
)
)
)*
\\s*
)
\\/>
/x";
return preg_replace_callback($pattern, function (array $matches) {
$this->boundAttributes = [];
$attributes = $this->getAttributesFromAttributeString($matches['attributes']);
return $this->componentString($matches[1], $attributes) . "\n@endcomponentClass ";
}, $value);
}
/**
* Compile the Blade component string for the given component and attributes.
*
* @throws InvalidArgumentException
* @return string
*/
protected function componentString(string $component, array $attributes)
{
$class = $this->componentClass($component);
[$data, $attributes] = $this->partitionDataAndAttributes($class, $attributes);
$data = $data->mapWithKeys(function ($value, $key) {
return [Str::camel($key) => $value];
});
// If the component doesn't exists as a class we'll assume it's a class-less
// component and pass the component as a view parameter to the data so it
// can be accessed within the component and we can render out the view.
if (! class_exists($class)) {
$parameters = [
'view' => "'{$class}'",
'data' => '[' . $this->attributesToString($data->all(), $escapeBound = false) . ']',
];
$class = AnonymousComponent::class;
} else {
$parameters = $data->all();
}
return " @component('{$class}', '{$component}', [" . $this->attributesToString($parameters, $escapeBound = false) . '])
<?php $component->withAttributes([' . $this->attributesToString($attributes->all()) . ']); ?>';
}
/**
* Compile the closing tags within the given string.
*
* @return string
*/
protected function compileClosingTags(string $value)
{
return preg_replace('/<\\/\\s*x[-\\:][\\w\\-\\:\\.]*\\s*>/', ' @endcomponentClass ', $value);
}
/**
* Get an array of attributes from the given attribute string.
*
* @return array
*/
protected function getAttributesFromAttributeString(string $attributeString)
{
$attributeString = $this->parseAttributeBag($attributeString);
$attributeString = $this->parseBindAttributes($attributeString);
$pattern = '/
(?<attribute>[\w\-:.@]+)
(
=
(?<value>
(
\"[^\"]+\"
|
\\\'[^\\\']+\\\'
|
[^\s>]+
)
)
)?
/x';
if (! preg_match_all($pattern, $attributeString, $matches, PREG_SET_ORDER)) {
return [];
}
return collect($matches)->mapWithKeys(function ($match) {
$attribute = $match['attribute'];
$value = $match['value'] ?? null;
if (is_null($value)) {
$value = 'true';
$attribute = Str::start($attribute, 'bind:');
}
$value = $this->stripQuotes($value);
if (Str::startsWith($attribute, 'bind:')) {
$attribute = Str::after($attribute, 'bind:');
$this->boundAttributes[$attribute] = true;
} else {
$value = "'" . $this->compileAttributeEchos($value) . "'";
}
return [$attribute => $value];
})->toArray();
}
/**
* Parse the attribute bag in a given attribute string into it's fully-qualified syntax.
*
* @return string
*/
protected function parseAttributeBag(string $attributeString)
{
$pattern = '/
(?:^|\\s+) # start of the string or whitespace between attributes
\\{\\{\\s*(\\$attributes(?:[^}]+?(?<!\\s))?)\\s*\\}\\} # exact match of attributes variable being echoed
/x';
return preg_replace($pattern, ' :attributes="$1"', $attributeString);
}
/**
* Parse the "bind" attributes in a given attribute string into their fully-qualified syntax.
*
* @return string
*/
protected function parseBindAttributes(string $attributeString)
{
$pattern = '/
(?:^|\\s+) # start of the string or whitespace between attributes
: # attribute needs to start with a semicolon
([\\w\\-:.@]+) # match the actual attribute name
= # only match attributes that have a value
/xm';
return preg_replace($pattern, ' bind:$1=', $attributeString);
}
/**
* Compile any Blade echo statements that are present in the attribute string.
*
* These echo statements need to be converted to string concatenation statements.
*
* @return string
*/
protected function compileAttributeEchos(string $attributeString)
{
$value = $this->blade->compileEchos($attributeString);
$value = $this->escapeSingleQuotesOutsideOfPhpBlocks($value);
$value = str_replace('<?php echo ', '\'.', $value);
return str_replace('; ?>', '.\'', $value);
}
/**
* Escape the single quotes in the given string that are outside of PHP blocks.
*
* @return string
*/
protected function escapeSingleQuotesOutsideOfPhpBlocks(string $value)
{
return collect(token_get_all($value))->map(function ($token) {
if (! is_array($token)) {
return $token;
}
return $token[0] === T_INLINE_HTML
? str_replace("'", "\\'", $token[1])
: $token[1];
})->implode('');
}
/**
* Convert an array of attributes to a string.
*
* @param bool $escapeBound
* @return string
*/
protected function attributesToString(array $attributes, $escapeBound = true)
{
return collect($attributes)
->map(function (string $value, string $attribute) use ($escapeBound) {
return $escapeBound && isset($this->boundAttributes[$attribute]) && $value !== 'true' && ! is_numeric($value)
? "'{$attribute}' => \\Hyperf\\ViewEngine\\Compiler\\BladeCompiler::sanitizeComponentAttribute({$value})"
: "'{$attribute}' => {$value}";
})
->implode(',');
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesComments
{
/**
* Compile Blade comments into an empty string.
*
* @param string $value
* @return string
*/
protected function compileComments($value)
{
$pattern = sprintf('/%s--(.*?)--%s/s', $this->contentTags[0], $this->contentTags[1]);
return preg_replace($pattern, '', $value);
}
}

View File

@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
use Hyperf\Utils\Str;
use Hyperf\ViewEngine\Component\ComponentAttributeBag;
use Hyperf\ViewEngine\T;
trait CompilesComponents
{
/**
* The component name hash stack.
*
* @var array
*/
protected static $componentHashStack = [];
/**
* Get a new component hash for a component name.
*
* @return string
*/
public static function newComponentHash(string $component)
{
static::$componentHashStack[] = $hash = sha1($component);
return $hash;
}
/**
* Compile a class component opening.
*
* @return string
*/
public static function compileClassComponentOpening(string $component, string $alias, string $data, string $hash)
{
return implode("\n", [
'<?php if (isset($component)) { $__componentOriginal' . $hash . ' = $component; } ?>',
'<?php $component = $__env->getContainer()->make(' . Str::finish($component, '::class') . ', ' . ($data ?: '[]') . '); ?>',
'<?php $component->withName(' . $alias . '); ?>',
'<?php if ($component->shouldRender()): ?>',
'<?php $__env->startComponent($component->resolveView(), $component->data()); ?>',
]);
}
/**
* Compile the end-component statements into valid PHP.
*
* @return string
*/
public function compileEndComponentClass()
{
return static::compileEndComponent() . "\n" . implode("\n", [
'<?php endif; ?>',
]);
}
/**
* Sanitize the given component attribute value.
*
* @param mixed $value
* @return mixed
*/
public static function sanitizeComponentAttribute($value)
{
return is_string($value) ||
(is_object($value) && ! $value instanceof ComponentAttributeBag && method_exists($value, '__toString'))
? T::e($value)
: $value;
}
/**
* Compile the component statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileComponent($expression)
{
[$component, $alias, $data] = strpos($expression, ',') !== false
? array_map('trim', explode(',', trim($expression, '()'), 3)) + ['', '', '']
: [trim($expression, '()'), '', ''];
$component = trim($component, '\'"');
$hash = static::newComponentHash($component);
if (Str::contains($component, ['::class', '\\'])) {
return static::compileClassComponentOpening($component, $alias, $data, $hash);
}
return "<?php \$__env->startComponent{$expression}; ?>";
}
/**
* Compile the end-component statements into valid PHP.
*
* @return string
*/
protected function compileEndComponent()
{
$hash = array_pop(static::$componentHashStack);
return implode("\n", [
'<?php if (isset($__componentOriginal' . $hash . ')): ?>',
'<?php $component = $__componentOriginal' . $hash . '; ?>',
'<?php unset($__componentOriginal' . $hash . '); ?>',
'<?php endif; ?>',
'<?php echo $__env->renderComponent(); ?>',
]);
}
/**
* Compile the slot statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileSlot($expression)
{
return "<?php \$__env->slot{$expression}; ?>";
}
/**
* Compile the end-slot statements into valid PHP.
*
* @return string
*/
protected function compileEndSlot()
{
return '<?php $__env->endSlot(); ?>';
}
/**
* Compile the component-first statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileComponentFirst($expression)
{
return "<?php \$__env->startComponentFirst{$expression}; ?>";
}
/**
* Compile the end-component-first statements into valid PHP.
*
* @return string
*/
protected function compileEndComponentFirst()
{
return $this->compileEndComponent();
}
/**
* Compile the prop statement into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileProps($expression)
{
return "<?php \$attributes = \$attributes->exceptProps{$expression}; ?>
<?php foreach (array_filter({$expression}, 'is_string', ARRAY_FILTER_USE_KEY) as \$__key => \$__value) {
\$\$__key = \$\$__key ?? \$__value;
} ?>
<?php \$__defined_vars = get_defined_vars(); ?>
<?php foreach (\$attributes as \$__key => \$__value) {
if (array_key_exists(\$__key, \$__defined_vars)) unset(\$\$__key);
} ?>
<?php unset(\$__defined_vars); ?>";
}
}

View File

@ -0,0 +1,317 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
use Hyperf\Contract\ConfigInterface;
trait CompilesConditionals
{
/**
* Identifier for the first case in switch statement.
*
* @var bool
*/
protected $firstCaseInSwitch = true;
/**
* Compile an end-once block into valid PHP.
*
* @return string
*/
public function compileEndOnce()
{
return '<?php endif; ?>';
}
/**
* Compile the if-auth statements into valid PHP.
*
* @param null|string $guard
* @return string
*/
protected function compileAuth($guard = null)
{
$guard = is_null($guard) ? '()' : $guard;
return "<?php if(auth()->guard{$guard}->check()): ?>";
}
/**
* Compile the else-auth statements into valid PHP.
*
* @param null|string $guard
* @return string
*/
protected function compileElseAuth($guard = null)
{
$guard = is_null($guard) ? '()' : $guard;
return "<?php elseif(auth()->guard{$guard}->check()): ?>";
}
/**
* Compile the end-auth statements into valid PHP.
*
* @return string
*/
protected function compileEndAuth()
{
return '<?php endif; ?>';
}
/**
* Compile the `@env` statements into valid PHP.
*
* @param string $environments
* @return string
*/
protected function compileEnv($environments)
{
$config = ConfigInterface::class;
return "<?php if(\\in_array(\$__env->getContainer()->get({$config}::class)->get('app_env'), {$environments})): ?>";
}
/**
* Compile the end-env statements into valid PHP.
*
* @return string
*/
protected function compileEndEnv()
{
return '<?php endif; ?>';
}
/**
* Compile the `@production` statements into valid PHP.
*
* @return string
*/
protected function compileProduction()
{
return $this->compileEnv("['prod', 'production']");
}
/**
* Compile the end-production statements into valid PHP.
*
* @return string
*/
protected function compileEndProduction()
{
return '<?php endif; ?>';
}
/**
* Compile the if-guest statements into valid PHP.
*
* @param null|string $guard
* @return string
*/
protected function compileGuest($guard = null)
{
$guard = is_null($guard) ? '()' : $guard;
return "<?php if(auth()->guard{$guard}->guest()): ?>";
}
/**
* Compile the else-guest statements into valid PHP.
*
* @param null|string $guard
* @return string
*/
protected function compileElseGuest($guard = null)
{
$guard = is_null($guard) ? '()' : $guard;
return "<?php elseif(auth()->guard{$guard}->guest()): ?>";
}
/**
* Compile the end-guest statements into valid PHP.
*
* @return string
*/
protected function compileEndGuest()
{
return '<?php endif; ?>';
}
/**
* Compile the has-section statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileHasSection($expression)
{
return "<?php if (! empty(trim(\$__env->yieldContent{$expression}))): ?>";
}
/**
* Compile the section-missing statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileSectionMissing($expression)
{
return "<?php if (empty(trim(\$__env->yieldContent{$expression}))): ?>";
}
/**
* Compile the if statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileIf($expression)
{
return "<?php if{$expression}: ?>";
}
/**
* Compile the unless statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileUnless($expression)
{
return "<?php if (! {$expression}): ?>";
}
/**
* Compile the else-if statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileElseif($expression)
{
return "<?php elseif{$expression}: ?>";
}
/**
* Compile the else statements into valid PHP.
*
* @return string
*/
protected function compileElse()
{
return '<?php else: ?>';
}
/**
* Compile the end-if statements into valid PHP.
*
* @return string
*/
protected function compileEndif()
{
return '<?php endif; ?>';
}
/**
* Compile the end-unless statements into valid PHP.
*
* @return string
*/
protected function compileEndunless()
{
return '<?php endif; ?>';
}
/**
* Compile the if-isset statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileIsset($expression)
{
return "<?php if(isset{$expression}): ?>";
}
/**
* Compile the end-isset statements into valid PHP.
*
* @return string
*/
protected function compileEndIsset()
{
return '<?php endif; ?>';
}
/**
* Compile the switch statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileSwitch($expression)
{
$this->firstCaseInSwitch = true;
return "<?php switch{$expression}:";
}
/**
* Compile the case statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileCase($expression)
{
if ($this->firstCaseInSwitch) {
$this->firstCaseInSwitch = false;
return "case {$expression}: ?>";
}
return "<?php case {$expression}: ?>";
}
/**
* Compile the default statements in switch case into valid PHP.
*
* @return string
*/
protected function compileDefault()
{
return '<?php default: ?>';
}
/**
* Compile the end switch statements into valid PHP.
*
* @return string
*/
protected function compileEndSwitch()
{
return '<?php endswitch; ?>';
}
/**
* Compile an once block into valid PHP.
*
* @param null|mixed $id
* @return string
*/
protected function compileOnce($id = null)
{
$id = $id ? $this->stripParentheses($id) : "'" . md5((string) microtime(true)) . "'";
return '<?php if (! $__env->hasRenderedOnce(' . $id . ')): $__env->markAsRenderedOnce(' . $id . '); ?>';
}
}

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesEchos
{
/**
* Compile Blade echos into valid PHP.
*
* @param string $value
* @return string
*/
public function compileEchos($value)
{
foreach ($this->getEchoMethods() as $method) {
$value = $this->{$method}($value);
}
return $value;
}
/**
* Get the echo methods in the proper order for compilation.
*
* @return array
*/
protected function getEchoMethods()
{
return [
'compileRawEchos',
'compileEscapedEchos',
'compileRegularEchos',
];
}
/**
* Compile the "raw" echo statements.
*
* @param string $value
* @return string
*/
protected function compileRawEchos($value)
{
$pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->rawTags[0], $this->rawTags[1]);
$callback = function ($matches) {
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
return $matches[1] ? substr($matches[0], 1) : "<?php echo {$matches[2]}; ?>{$whitespace}";
};
return preg_replace_callback($pattern, $callback, $value);
}
/**
* Compile the "regular" echo statements.
*
* @param string $value
* @return string
*/
protected function compileRegularEchos($value)
{
$pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->contentTags[0], $this->contentTags[1]);
$callback = function ($matches) {
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
$wrapped = sprintf($this->echoFormat, $matches[2]);
return $matches[1] ? substr($matches[0], 1) : "<?php echo {$wrapped}; ?>{$whitespace}";
};
return preg_replace_callback($pattern, $callback, $value);
}
/**
* Compile the escaped echo statements.
*
* @param string $value
* @return string
*/
protected function compileEscapedEchos($value)
{
$pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->escapedTags[0], $this->escapedTags[1]);
$callback = function ($matches) {
$whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
return $matches[1] ? $matches[0] : "<?php echo \\Hyperf\\ViewEngine\\T::e({$matches[2]}); ?>{$whitespace}";
};
return preg_replace_callback($pattern, $callback, $value);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesErrors
{
/**
* Compile the error statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileError($expression)
{
$expression = $this->stripParentheses($expression);
return '<?php $__errorArgs = [' . $expression . '];
$__bag = $errors->getBag($__errorArgs[1] ?? \'default\');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>';
}
/**
* Compile the enderror statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileEnderror($expression)
{
return '<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?>';
}
}

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesIncludes
{
/**
* Compile the each statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileEach($expression)
{
return "<?php echo \$__env->renderEach{$expression}; ?>";
}
/**
* Compile the include statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileInclude($expression)
{
$expression = $this->stripParentheses($expression);
return "<?php echo \$__env->make({$expression}, \\Hyperf\\Utils\\Arr::except(get_defined_vars(), ['__data', '__path']))->render(); ?>";
}
/**
* Compile the include-if statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileIncludeIf($expression)
{
$expression = $this->stripParentheses($expression);
return "<?php if (\$__env->exists({$expression})) echo \$__env->make({$expression}, \\Hyperf\\Utils\\Arr::except(get_defined_vars(), ['__data', '__path']))->render(); ?>";
}
/**
* Compile the include-when statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileIncludeWhen($expression)
{
$expression = $this->stripParentheses($expression);
return "<?php echo \$__env->renderWhen({$expression}, \\Hyperf\\Utils\\Arr::except(get_defined_vars(), ['__data', '__path'])); ?>";
}
/**
* Compile the include-unless statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileIncludeUnless($expression)
{
$expression = $this->stripParentheses($expression);
return "<?php echo \$__env->renderWhen(! {$expression}, \\Hyperf\\Utils\\Arr::except(get_defined_vars(), ['__data', '__path'])); ?>";
}
/**
* Compile the include-first statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileIncludeFirst($expression)
{
$expression = $this->stripParentheses($expression);
return "<?php echo \$__env->first({$expression}, \\Hyperf\\Utils\\Arr::except(get_defined_vars(), ['__data', '__path']))->render(); ?>";
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesInjections
{
/**
* Compile the inject statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileInject($expression)
{
$segments = explode(',', preg_replace("/[\\(\\)\\\"\\']/", '', $expression));
$variable = trim($segments[0]);
$service = trim($segments[1]);
return "<?php \${$variable} = \\Hyperf\\ViewEngine\\T::inject('{$service}'); ?>";
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesJson
{
/**
* The default JSON encoding options.
*
* @var int
*/
private $encodingOptions = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT;
/**
* Compile the JSON statement into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileJson($expression)
{
$parts = explode(',', $this->stripParentheses($expression));
$options = isset($parts[1]) ? trim($parts[1]) : $this->encodingOptions;
$depth = isset($parts[2]) ? trim($parts[2]) : 512;
return "<?php echo json_encode({$parts[0]}, {$options}, {$depth}) ?>";
}
}

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
use Hyperf\ViewEngine\Factory as ViewFactory;
trait CompilesLayouts
{
/**
* The name of the last section that was started.
*
* @var string
*/
protected $lastSection;
/**
* Compile the extends statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileExtends($expression)
{
$expression = $this->stripParentheses($expression);
$echo = "<?php echo \$__env->make({$expression}, \\Hyperf\\Utils\\Arr::except(get_defined_vars(), ['__data', '__path']))->render(); ?>";
$this->footer[] = $echo;
return '';
}
/**
* Compile the section statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileSection($expression)
{
$this->lastSection = trim($expression, "()'\" ");
return "<?php \$__env->startSection{$expression}; ?>";
}
/**
* Replace the `@parent` directive to a placeholder.
*
* @return string
*/
protected function compileParent()
{
return ViewFactory::parentPlaceholder($this->lastSection ?: '');
}
/**
* Compile the yield statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileYield($expression)
{
return "<?php echo \$__env->yieldContent{$expression}; ?>";
}
/**
* Compile the show statements into valid PHP.
*
* @return string
*/
protected function compileShow()
{
return '<?php echo $__env->yieldSection(); ?>';
}
/**
* Compile the append statements into valid PHP.
*
* @return string
*/
protected function compileAppend()
{
return '<?php $__env->appendSection(); ?>';
}
/**
* Compile the overwrite statements into valid PHP.
*
* @return string
*/
protected function compileOverwrite()
{
return '<?php $__env->stopSection(true); ?>';
}
/**
* Compile the stop statements into valid PHP.
*
* @return string
*/
protected function compileStop()
{
return '<?php $__env->stopSection(); ?>';
}
/**
* Compile the end-section statements into valid PHP.
*
* @return string
*/
protected function compileEndsection()
{
return '<?php $__env->stopSection(); ?>';
}
}

View File

@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesLoops
{
/**
* Counter to keep track of nested forelse statements.
*
* @var int
*/
protected $forElseCounter = 0;
/**
* Compile the for-else statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileForelse($expression)
{
$empty = '$__empty_' . ++$this->forElseCounter;
preg_match('/\( *(.*) +as *(.*)\)$/is', $expression, $matches);
$iteratee = trim($matches[1]);
$iteration = trim($matches[2]);
$initLoop = "\$__currentLoopData = {$iteratee}; \$__env->addLoop(\$__currentLoopData);";
$iterateLoop = '$__env->incrementLoopIndices(); $loop = $__env->getLastLoop();';
return "<?php {$empty} = true; {$initLoop} foreach(\$__currentLoopData as {$iteration}): {$iterateLoop} {$empty} = false; ?>";
}
/**
* Compile the for-else-empty and empty statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileEmpty($expression)
{
if ($expression) {
return "<?php if(empty{$expression}): ?>";
}
$empty = '$__empty_' . $this->forElseCounter--;
return "<?php endforeach; \$__env->popLoop(); \$loop = \$__env->getLastLoop(); if ({$empty}): ?>";
}
/**
* Compile the end-for-else statements into valid PHP.
*
* @return string
*/
protected function compileEndforelse()
{
return '<?php endif; ?>';
}
/**
* Compile the end-empty statements into valid PHP.
*
* @return string
*/
protected function compileEndEmpty()
{
return '<?php endif; ?>';
}
/**
* Compile the for statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileFor($expression)
{
return "<?php for{$expression}: ?>";
}
/**
* Compile the for-each statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileForeach($expression)
{
preg_match('/\( *(.*) +as *(.*)\)$/is', $expression, $matches);
$iteratee = trim($matches[1]);
$iteration = trim($matches[2]);
$initLoop = "\$__currentLoopData = {$iteratee}; \$__env->addLoop(\$__currentLoopData);";
$iterateLoop = '$__env->incrementLoopIndices(); $loop = $__env->getLastLoop();';
return "<?php {$initLoop} foreach(\$__currentLoopData as {$iteration}): {$iterateLoop} ?>";
}
/**
* Compile the break statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileBreak($expression)
{
if ($expression) {
preg_match('/\(\s*(-?\d+)\s*\)$/', $expression, $matches);
return $matches ? '<?php break ' . max(1, $matches[1]) . '; ?>' : "<?php if{$expression} break; ?>";
}
return '<?php break; ?>';
}
/**
* Compile the continue statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileContinue($expression)
{
if ($expression) {
preg_match('/\(\s*(-?\d+)\s*\)$/', $expression, $matches);
return $matches ? '<?php continue ' . max(1, $matches[1]) . '; ?>' : "<?php if{$expression} continue; ?>";
}
return '<?php continue; ?>';
}
/**
* Compile the end-for statements into valid PHP.
*
* @return string
*/
protected function compileEndfor()
{
return '<?php endfor; ?>';
}
/**
* Compile the end-for-each statements into valid PHP.
*
* @return string
*/
protected function compileEndforeach()
{
return '<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>';
}
/**
* Compile the while statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileWhile($expression)
{
return "<?php while{$expression}: ?>";
}
/**
* Compile the end-while statements into valid PHP.
*
* @return string
*/
protected function compileEndwhile()
{
return '<?php endwhile; ?>';
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesRawPhp
{
/**
* Compile the raw PHP statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compilePhp($expression)
{
if ($expression) {
return "<?php {$expression}; ?>";
}
return '@php';
}
/**
* Compile the unset statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileUnset($expression)
{
return "<?php unset{$expression}; ?>";
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesStacks
{
/**
* Compile the stack statements into the content.
*
* @param string $expression
* @return string
*/
protected function compileStack($expression)
{
return "<?php echo \$__env->yieldPushContent{$expression}; ?>";
}
/**
* Compile the push statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compilePush($expression)
{
return "<?php \$__env->startPush{$expression}; ?>";
}
/**
* Compile the end-push statements into valid PHP.
*
* @return string
*/
protected function compileEndpush()
{
return '<?php $__env->stopPush(); ?>';
}
/**
* Compile the prepend statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compilePrepend($expression)
{
return "<?php \$__env->startPrepend{$expression}; ?>";
}
/**
* Compile the end-prepend statements into valid PHP.
*
* @return string
*/
protected function compileEndprepend()
{
return '<?php $__env->stopPrepend(); ?>';
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Compiler\Concern;
trait CompilesTranslations
{
/**
* Compile the lang statements into valid PHP.
*
* @param null|string $expression
* @return string
*/
protected function compileLang($expression)
{
if (is_null($expression)) {
return '<?php $__env->startTranslation(); ?>';
}
if ($expression[1] === '[') {
return "<?php \$__env->startTranslation{$expression}; ?>";
}
return "<?php echo \\Hyperf\\ViewEngine\\T::translator()->get{$expression}; ?>";
}
/**
* Compile the end-lang statements into valid PHP.
*
* @return string
*/
protected function compileEndlang()
{
return '<?php echo $__env->renderTranslation(); ?>';
}
/**
* Compile the choice statements into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileChoice($expression)
{
return "<?php echo \\Hyperf\\ViewEngine\\T::translator()->choice{$expression}; ?>";
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Component;
class AnonymousComponent extends Component
{
/**
* The component view.
*
* @var string
*/
protected $view;
/**
* The component data.
*
* @var array
*/
protected $data = [];
/**
* Create a new anonymous component instance.
*
* @param string $view
* @param array $data
*/
public function __construct($view, $data)
{
$this->view = $view;
$this->data = $data;
}
/**
* Get the view / view contents that represent the component.
*
* @return string
*/
public function render()
{
return $this->view;
}
/**
* Get the data that should be supplied to the view.
*
* @return array
*/
public function data()
{
$this->attributes = $this->attributes ?: new ComponentAttributeBag();
return $this->data + ['attributes' => $this->attributes];
}
}

View File

@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Component;
use Closure;
use Hyperf\Utils\Str;
use Hyperf\ViewEngine\Blade;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\Contract\Htmlable;
use Hyperf\ViewEngine\View;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
abstract class Component
{
/**
* The component alias name.
*
* @var string
*/
public $componentName;
/**
* The component attributes.
*
* @var null|ComponentAttributeBag
*/
public $attributes;
/**
* The cache of public property names, keyed by class.
*
* @var array
*/
protected static $propertyCache = [];
/**
* The cache of public method names, keyed by class.
*
* @var array
*/
protected static $methodCache = [];
/**
* The properties / methods that should not be exposed to the component.
*
* @var array
*/
protected $except = [];
/**
* Get the view / view contents that represent the component.
*
* @return Closure|Htmlable|string|View
*/
abstract public function render();
/**
* Resolve the Blade view or view file that should be used when rendering the component.
*
* @return Closure|Htmlable|string|View
*/
public function resolveView()
{
$view = $this->render();
if ($view instanceof View) {
return $view;
}
if ($view instanceof Htmlable) {
return $view;
}
$resolver = function ($view) {
$factory = Blade::container()->get(FactoryInterface::class);
return $factory->exists($view)
? $view
: $this->createBladeViewFromString($view);
};
return $view instanceof Closure
? function (array $data = []) use ($view, $resolver) {
return $resolver($view($data));
}
: $resolver($view);
}
/**
* Get the data that should be supplied to the view.
*
* @return array
*/
public function data()
{
$this->attributes = $this->attributes ?: new ComponentAttributeBag();
return array_merge($this->extractPublicProperties(), $this->extractPublicMethods());
}
/**
* Set the component alias name.
*
* @param string $name
* @return $this
*/
public function withName($name)
{
$this->componentName = $name;
return $this;
}
/**
* Set the extra attributes that the component should make available.
*
* @return $this
*/
public function withAttributes(array $attributes)
{
$this->attributes = $this->attributes ?: new ComponentAttributeBag();
$this->attributes->setAttributes($attributes);
return $this;
}
/**
* Determine if the component should be rendered.
*
* @return bool
*/
public function shouldRender()
{
return true;
}
/**
* Create a Blade view with the raw component string content.
*
* @param string $contents
* @return string
*/
protected function createBladeViewFromString($contents)
{
if (! is_file($viewFile = Blade::config('config.cache_path') . '/' . sha1($contents) . '.blade.php')) {
file_put_contents($viewFile, $contents);
}
return '__components::' . basename($viewFile, '.blade.php');
}
/**
* Extract the public properties for the component.
*
* @return array
*/
protected function extractPublicProperties()
{
$class = get_class($this);
if (! isset(static::$propertyCache[$class])) {
$reflection = new ReflectionClass($this);
static::$propertyCache[$class] = collect($reflection->getProperties(ReflectionProperty::IS_PUBLIC))
->reject(function (ReflectionProperty $property) {
return $property->isStatic();
})
->reject(function (ReflectionProperty $property) {
return $this->shouldIgnore($property->getName());
})
->map(function (ReflectionProperty $property) {
return $property->getName();
})->all();
}
$values = [];
foreach (static::$propertyCache[$class] as $property) {
$values[$property] = $this->{$property};
}
return $values;
}
/**
* Extract the public methods for the component.
*
* @return array
*/
protected function extractPublicMethods()
{
$class = get_class($this);
if (! isset(static::$methodCache[$class])) {
$reflection = new ReflectionClass($this);
static::$methodCache[$class] = collect($reflection->getMethods(ReflectionMethod::IS_PUBLIC))
->reject(function (ReflectionMethod $method) {
return $this->shouldIgnore($method->getName());
})
->map(function (ReflectionMethod $method) {
return $method->getName();
});
}
$values = [];
foreach (static::$methodCache[$class] as $method) {
$values[$method] = $this->createVariableFromMethod(new ReflectionMethod($this, $method));
}
return $values;
}
/**
* Create a callable variable from the given method.
*
* @return mixed
*/
protected function createVariableFromMethod(ReflectionMethod $method)
{
return $method->getNumberOfParameters() === 0
? $this->createInvokableVariable($method->getName())
: Closure::fromCallable([$this, $method->getName()]);
}
/**
* Create an invokable, toStringable variable for the given component method.
*
* @return InvokableComponentVariable
*/
protected function createInvokableVariable(string $method)
{
return new InvokableComponentVariable(function () use ($method) {
return $this->{$method}();
});
}
/**
* Determine if the given property / method should be ignored.
*
* @param string $name
* @return bool
*/
protected function shouldIgnore($name)
{
return Str::startsWith($name, '__') ||
in_array($name, $this->ignoredMethods());
}
/**
* Get the methods that should be ignored.
*
* @return array
*/
protected function ignoredMethods()
{
return array_merge([
'data',
'render',
'resolveView',
'shouldRender',
'view',
'withName',
'withAttributes',
], $this->except);
}
}

View File

@ -0,0 +1,340 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Component;
use ArrayAccess;
use ArrayIterator;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Str;
use Hyperf\Utils\Traits\Macroable;
use Hyperf\ViewEngine\Contract\Htmlable;
use Hyperf\ViewEngine\HtmlString;
use IteratorAggregate;
class ComponentAttributeBag implements ArrayAccess, Htmlable, IteratorAggregate
{
use Macroable;
/**
* The raw array of attributes.
*
* @var array
*/
protected $attributes = [];
/**
* Create a new component attribute bag instance.
*/
public function __construct(array $attributes = [])
{
$this->attributes = $attributes;
}
/**
* Merge additional attributes / values into the attribute bag.
*
* @return HtmlString
*/
public function __invoke(array $attributeDefaults = [])
{
return new HtmlString((string) $this->merge($attributeDefaults));
}
/**
* Implode the attributes into a single HTML ready string.
*
* @return string
*/
public function __toString()
{
$string = '';
foreach ($this->attributes as $key => $value) {
if ($value === false || is_null($value)) {
continue;
}
if ($value === true) {
$value = $key;
}
$string .= ' ' . $key . '="' . str_replace('"', '\\"', trim($value)) . '"';
}
return trim($string);
}
/**
* Get the first attribute's value.
*
* @param mixed $default
* @return mixed
*/
public function first($default = null)
{
return $this->getIterator()->current() ?? value($default);
}
/**
* Get a given attribute from the attribute array.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null)
{
return $this->attributes[$key] ?? value($default);
}
/**
* Only include the given attribute from the attribute array.
*
* @param array|mixed $keys
* @return static
*/
public function only($keys)
{
if (is_null($keys)) {
$values = $this->attributes;
} else {
$keys = Arr::wrap($keys);
$values = Arr::only($this->attributes, $keys);
}
$this->setAttributes($values);
return $this;
}
/**
* Exclude the given attribute from the attribute array.
*
* @param array|mixed $keys
* @return static
*/
public function except($keys)
{
if (is_null($keys)) {
$values = $this->attributes;
} else {
$keys = Arr::wrap($keys);
$values = Arr::except($this->attributes, $keys);
}
$this->setAttributes($values);
return $this;
}
/**
* Filter the attributes, returning a bag of attributes that pass the filter.
*
* @param callable $callback
* @return static
*/
public function filter($callback)
{
$this->setAttributes(collect($this->attributes)->filter($callback)->all());
return $this;
}
public function reject($callback)
{
$this->setAttributes(collect($this->attributes)->reject($callback)->all());
return $this;
}
/**
* Return a bag of attributes that have keys starting with the given value / pattern.
*
* @param string $string
* @return static
*/
public function whereStartsWith($string)
{
return $this->filter(function ($value, $key) use ($string) {
return Str::startsWith($key, $string);
});
}
/**
* Return a bag of attributes with keys that do not start with the given value / pattern.
*
* @param string $string
* @return static
*/
public function whereDoesntStartWith($string)
{
return $this->reject(function ($value, $key) use ($string) {
return Str::startsWith($key, $string);
});
}
/**
* Return a bag of attributes that have keys starting with the given value / pattern.
*
* @param string $string
* @return static
*/
public function thatStartWith($string)
{
return $this->whereStartsWith($string);
}
/**
* Exclude the given attribute from the attribute array.
*
* @param array|mixed $keys
* @return static
*/
public function exceptProps($keys)
{
$props = [];
foreach ($keys as $key => $defaultValue) {
$key = is_numeric($key) ? $defaultValue : $key;
$props[] = $key;
$props[] = Str::kebab($key);
}
return $this->except($props);
}
/**
* Merge additional attributes / values into the attribute bag.
*
* @return static
*/
public function merge(array $attributeDefaults = [])
{
$attributes = [];
$attributeDefaults = array_map(function ($value) {
if (is_object($value) || is_null($value) || is_bool($value)) {
return $value;
}
return \Hyperf\ViewEngine\T::e($value);
}, $attributeDefaults);
foreach ($this->attributes as $key => $value) {
if ($key !== 'class') {
$attributes[$key] = $value;
continue;
}
$attributes[$key] = implode(' ', array_unique(
array_filter([$attributeDefaults[$key] ?? '', $value])
));
}
$this->setAttributes(array_merge($attributeDefaults, $attributes));
return $this;
}
/**
* Get all of the raw attributes.
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Set the underlying attributes.
*/
public function setAttributes(array $attributes)
{
if (isset($attributes['attributes']) &&
$attributes['attributes'] instanceof self) {
$parentBag = $attributes['attributes'];
unset($attributes['attributes']);
$attributes = $parentBag->merge($attributes);
}
$this->attributes = $attributes;
}
/**
* Get content as a string of HTML.
*
* @return string
*/
public function toHtml()
{
return (string) $this;
}
/**
* Determine if the given offset exists.
*
* @param string $offset
* @return bool
*/
public function offsetExists($offset)
{
return isset($this->attributes[$offset]);
}
/**
* Get the value at the given offset.
*
* @param string $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* Set the value at a given offset.
*
* @param string $offset
* @param mixed $value
*/
public function offsetSet($offset, $value)
{
$this->attributes[$offset] = $value;
}
/**
* Remove the value at the given offset.
*
* @param string $offset
*/
public function offsetUnset($offset)
{
unset($this->attributes[$offset]);
}
/**
* Get an iterator for the items.
*
* @return ArrayIterator
*/
public function getIterator()
{
return new ArrayIterator($this->attributes);
}
}

View File

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Component;
use Closure;
use Hyperf\Utils\Str;
use Hyperf\ViewEngine\Blade;
use Hyperf\ViewEngine\Compiler\CompilerInterface;
use Hyperf\ViewEngine\Compiler\ComponentTagCompiler;
use Hyperf\ViewEngine\View;
class DynamicComponent extends Component
{
/**
* The name of the component.
*
* @var string
*/
public $component;
/**
* The component tag compiler instance.
*
* @var null|ComponentTagCompiler
*/
protected $compiler;
/**
* The cached component classes.
*
* @var array
*/
protected $componentClasses = [];
/**
* The cached binding keys for component classes.
*
* @var array
*/
protected $bindings = [];
/**
* Create a new component instance.
*/
public function __construct(string $component)
{
$this->component = $component;
}
/**
* Get the view / contents that represent the component.
*
* @return Closure|string|View
*/
public function render()
{
$template = <<<'EOF'
<?php extract(collect($attributes->getAttributes())->mapWithKeys(function ($value, $key) { return [Hyperf\Utils\Str::camel(str_replace(':', ' ', $key)) => $value]; })->all(), EXTR_SKIP); ?>
{{ props }}
<x-{{ component }} {{ bindings }} {{ attributes }}>
{{ slots }}
{{ defaultSlot }}
</x-{{ component }}>
EOF;
return function ($data) use ($template) {
$bindings = $this->bindings($class = $this->classForComponent());
return str_replace(
[
'{{ component }}',
'{{ props }}',
'{{ bindings }}',
'{{ attributes }}',
'{{ slots }}',
'{{ defaultSlot }}',
],
[
$this->component,
$this->compileProps($bindings),
$this->compileBindings($bindings),
class_exists($class) ? '{{ $attributes }}' : '',
$this->compileSlots($data['__laravel_slots']),
'{{ $slot ?? "" }}',
],
$template
);
};
}
/**
* Compile the `@props` directive for the component.
*
* @return string
*/
protected function compileProps(array $bindings)
{
if (empty($bindings)) {
return '';
}
return '@props(' . '[\'' . implode('\',\'', collect($bindings)->map(function ($dataKey) {
return Str::camel($dataKey);
})->all()) . '\']' . ')';
}
/**
* Compile the bindings for the component.
*
* @return string
*/
protected function compileBindings(array $bindings)
{
return collect($bindings)->map(function ($key) {
return ':' . $key . '="$' . Str::camel(str_replace(':', ' ', $key)) . '"';
})->implode(' ');
}
/**
* Compile the slots for the component.
*
* @return string
*/
protected function compileSlots(array $slots)
{
return collect($slots)->map(function ($slot, $name) {
return $name === '__default' ? null : '<x-slot name="' . $name . '">{{ $' . $name . ' }}</x-slot>';
})->filter()->implode(PHP_EOL);
}
/**
* Get the class for the current component.
*
* @return string
*/
protected function classForComponent()
{
if (isset($this->componentClasses[$this->component])) {
return $this->componentClasses[$this->component];
}
return $this->componentClasses[$this->component] =
$this->compiler()->componentClass($this->component);
}
/**
* Get the names of the variables that should be bound to the component.
*
* @return array
*/
protected function bindings(string $class)
{
if (! isset($this->bindings[$class])) {
[$data, $attributes] = $this->compiler()->partitionDataAndAttributes($class, $this->attributes->getAttributes());
$this->bindings[$class] = array_keys($data->all());
}
return $this->bindings[$class];
}
/**
* Get an instance of the Blade tag compiler.
*
* @return ComponentTagCompiler
*/
protected function compiler()
{
if (! $this->compiler) {
$this->compiler = new ComponentTagCompiler(
Blade::container()->get(CompilerInterface::class)->getClassComponentAliases(),
Blade::container()->get(CompilerInterface::class)->getClassComponentNamespaces(),
Blade::container()->get(CompilerInterface::class)
);
}
return $this->compiler;
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Component;
use ArrayIterator;
use Closure;
use Hyperf\ViewEngine\Contract\DeferringDisplayableValue;
use Hyperf\ViewEngine\Contract\Enumerable;
use Hyperf\ViewEngine\Contract\Htmlable;
use IteratorAggregate;
class InvokableComponentVariable implements DeferringDisplayableValue, IteratorAggregate
{
/**
* The callable instance to resolve the variable value.
*
* @var Closure
*/
protected $callable;
/**
* Create a new variable instance.
*/
public function __construct(Closure $callable)
{
$this->callable = $callable;
}
/**
* Dynamically proxy attribute access to the variable.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->__invoke()->{$key};
}
/**
* Dynamically proxy method access to the variable.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->__invoke()->{$method}(...$parameters);
}
/**
* Resolve the variable.
*
* @return mixed
*/
public function __invoke()
{
return call_user_func($this->callable);
}
/**
* Resolve the variable as a string.
*
* @return mixed
*/
public function __toString()
{
return (string) $this->__invoke();
}
/**
* Resolve the displayable value that the class is deferring.
*
* @return Htmlable|string
*/
public function resolveDisplayableValue()
{
return $this->__invoke();
}
/**
* Get an interator instance for the variable.
*
* @return ArrayIterator
*/
public function getIterator()
{
$result = $this->__invoke();
return new ArrayIterator($result instanceof Enumerable ? $result->all() : $result);
}
}

View File

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Concern;
use Closure;
use Hyperf\Utils\Arr;
use Hyperf\ViewEngine\Contract\Htmlable;
use Hyperf\ViewEngine\HtmlString;
use Hyperf\ViewEngine\View;
use InvalidArgumentException;
trait ManagesComponents
{
/**
* The components being rendered.
*
* @var array
*/
protected $componentStack = [];
/**
* The original data passed to the component.
*
* @var array
*/
protected $componentData = [];
/**
* The slot contents for the component.
*
* @var array
*/
protected $slots = [];
/**
* The names of the slots being rendered.
*
* @var array
*/
protected $slotStack = [];
/**
* Start a component rendering process.
*
* @param Closure|Htmlable|string|View $view
*/
public function startComponent($view, array $data = [])
{
if (ob_start()) {
$this->componentStack[] = $view;
$this->componentData[$this->currentComponent()] = $data;
$this->slots[$this->currentComponent()] = [];
}
}
/**
* Get the first view that actually exists from the given list, and start a component.
*/
public function startComponentFirst(array $names, array $data = [])
{
$name = Arr::first($names, function ($item) {
return $this->exists($item);
});
$this->startComponent($name, $data);
}
/**
* Render the current component.
*
* @return string
*/
public function renderComponent()
{
$view = array_pop($this->componentStack);
$data = $this->componentData();
if ($view instanceof Closure) {
$view = $view($data);
}
if ($view instanceof View) {
return $view->with($data)->render();
}
if ($view instanceof Htmlable) {
return $view->toHtml();
}
return $this->make($view, $data)->render();
}
/**
* Start the slot rendering process.
*
* @param string $name
* @param null|string $content
*/
public function slot($name, $content = null)
{
if (func_num_args() > 2) {
throw new InvalidArgumentException('You passed too many arguments to the [' . $name . '] slot.');
}
if (func_num_args() === 2) {
$this->slots[$this->currentComponent()][$name] = $content;
} elseif (ob_start()) {
$this->slots[$this->currentComponent()][$name] = '';
$this->slotStack[$this->currentComponent()][] = $name;
}
}
/**
* Save the slot content for rendering.
*/
public function endSlot()
{
last($this->componentStack);
$currentSlot = array_pop(
$this->slotStack[$this->currentComponent()]
);
$this->slots[$this->currentComponent()][$currentSlot] = new HtmlString(trim(ob_get_clean()));
}
/**
* Get the data for the given component.
*
* @return array
*/
protected function componentData()
{
$defaultSlot = new HtmlString(trim(ob_get_clean()));
$slots = array_merge([
'__default' => $defaultSlot,
], $this->slots[count($this->componentStack)]);
return array_merge(
$this->componentData[count($this->componentStack)],
['slot' => $defaultSlot],
$this->slots[count($this->componentStack)],
['__laravel_slots' => $slots]
);
}
/**
* Get the index for the current component.
*
* @return int
*/
protected function currentComponent()
{
return count($this->componentStack) - 1;
}
}

View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Concern;
use Closure;
use Hyperf\Utils\Str;
use Hyperf\ViewEngine\Blade;
use Hyperf\ViewEngine\Contract\ViewInterface;
trait ManagesEvents
{
/**
* Register a view creator event.
*
* @param array|string $views
* @param Closure|string $callback
* @return array
*/
public function creator($views, $callback)
{
$creators = [];
foreach ((array) $views as $view) {
$creators[] = $this->addViewEvent($view, $callback, 'creating: ');
}
return $creators;
}
/**
* Register multiple view composers via an array.
*
* @return array
*/
public function composers(array $composers)
{
$registered = [];
foreach ($composers as $callback => $views) {
$registered = array_merge($registered, $this->composer($views, $callback));
}
return $registered;
}
/**
* Register a view composer event.
*
* @param array|string $views
* @param Closure|string $callback
* @return array
*/
public function composer($views, $callback)
{
$composers = [];
foreach ((array) $views as $view) {
$composers[] = $this->addViewEvent($view, $callback, 'composing: ');
}
return $composers;
}
/**
* Call the composer for a given view.
*/
public function callComposer(ViewInterface $view)
{
// @TODO
// $this->events->dispatch('composing: '.$view->name(), [$view]);
}
/**
* Call the creator for a given view.
*/
public function callCreator(ViewInterface $view)
{
// @TODO
// $this->events->dispatch('creating: '.$view->name(), [$view]);
}
/**
* Add an event for a given view.
*
* @param string $view
* @param Closure|string $callback
* @param string $prefix
* @return null|Closure
*/
protected function addViewEvent($view, $callback, $prefix = 'composing: ')
{
$view = $this->normalizeName($view);
if ($callback instanceof Closure) {
$this->addEventListener($prefix . $view, $callback);
return $callback;
}
if (is_string($callback)) {
return $this->addClassEvent($view, $callback, $prefix);
}
}
/**
* Register a class based view composer.
*
* @param string $view
* @param string $class
* @param string $prefix
* @return Closure
*/
protected function addClassEvent($view, $class, $prefix)
{
$name = $prefix . $view;
// When registering a class based view "composer", we will simply resolve the
// classes from the application IoC container then call the compose method
// on the instance. This allows for convenient, testable view composers.
$callback = $this->buildClassEventCallback(
$class,
$prefix
);
$this->addEventListener($name, $callback);
return $callback;
}
/**
* Build a class based container callback Closure.
*
* @param string $class
* @param string $prefix
* @return Closure
*/
protected function buildClassEventCallback($class, $prefix)
{
[$class, $method] = $this->parseClassEvent($class, $prefix);
// Once we have the class and method name, we can build the Closure to resolve
// the instance out of the IoC container and call the method on it with the
// given arguments that are passed to the Closure as the composer's data.
return function () use ($class, $method) {
return call_user_func_array(
[Blade::container()->make($class), $method],
func_get_args()
);
};
}
/**
* Parse a class based composer name.
*
* @param string $class
* @param string $prefix
* @return array
*/
protected function parseClassEvent($class, $prefix)
{
return Str::parseCallback($class, $this->classEventMethodForPrefix($prefix));
}
/**
* Determine the class event method based on the given prefix.
*
* @param string $prefix
* @return string
*/
protected function classEventMethodForPrefix($prefix)
{
return Str::contains($prefix, 'composing') ? 'compose' : 'create';
}
/**
* Add a listener to the event dispatcher.
*
* @param string $name
* @param Closure $callback
*/
protected function addEventListener($name, $callback)
{
if (Str::contains($name, '*')) {
$callback = function ($name, array $data) use ($callback) {
return $callback($data[0]);
};
}
// @TODO
// $this->events->listen($name, $callback);
}
}

View File

@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Concern;
use Hyperf\ViewEngine\Contract\ViewInterface;
use Hyperf\ViewEngine\T;
use InvalidArgumentException;
trait ManagesLayouts
{
/**
* All of the finished, captured sections.
*
* @var array
*/
protected $sections = [];
/**
* The stack of in-progress sections.
*
* @var array
*/
protected $sectionStack = [];
/**
* The parent placeholder for the request.
*
* @var mixed
*/
protected static $parentPlaceholder = [];
/**
* Start injecting content into a section.
*
* @param string $section
* @param null|string|ViewInterface $content
*/
public function startSection($section, $content = null)
{
if ($content === null) {
if (ob_start()) {
$this->sectionStack[] = $section;
}
} else {
$this->extendSection($section, $content instanceof ViewInterface ? $content : T::e($content));
}
}
/**
* Inject inline content into a section.
*
* @param string $section
* @param string $content
*/
public function inject($section, $content)
{
$this->startSection($section, $content);
}
/**
* Stop injecting content into a section and return its contents.
*
* @return string
*/
public function yieldSection()
{
if (empty($this->sectionStack)) {
return '';
}
return $this->yieldContent($this->stopSection());
}
/**
* Stop injecting content into a section.
*
* @param bool $overwrite
* @throws InvalidArgumentException
* @return string
*/
public function stopSection($overwrite = false)
{
if (empty($this->sectionStack)) {
throw new InvalidArgumentException('Cannot end a section without first starting one.');
}
$last = array_pop($this->sectionStack);
if ($overwrite) {
$this->sections[$last] = ob_get_clean();
} else {
$this->extendSection($last, ob_get_clean());
}
return $last;
}
/**
* Stop injecting content into a section and append it.
*
* @throws InvalidArgumentException
* @return string
*/
public function appendSection()
{
if (empty($this->sectionStack)) {
throw new InvalidArgumentException('Cannot end a section without first starting one.');
}
$last = array_pop($this->sectionStack);
if (isset($this->sections[$last])) {
$this->sections[$last] .= ob_get_clean();
} else {
$this->sections[$last] = ob_get_clean();
}
return $last;
}
/**
* Get the string contents of a section.
*
* @param string $section
* @param string|ViewInterface $default
* @return string
*/
public function yieldContent($section, $default = '')
{
$sectionContent = $default instanceof ViewInterface ? $default : T::e($default);
if (isset($this->sections[$section])) {
$sectionContent = $this->sections[$section];
}
$sectionContent = str_replace('@@parent', '--parent--holder--', $sectionContent);
return str_replace(
'--parent--holder--',
'@parent',
str_replace(static::parentPlaceholder($section), '', $sectionContent)
);
}
/**
* Get the parent placeholder for the current request.
*
* @param string $section
* @return string
*/
public static function parentPlaceholder($section = '')
{
if (! isset(static::$parentPlaceholder[$section])) {
static::$parentPlaceholder[$section] = '##parent-placeholder-' . sha1($section) . '##';
}
return static::$parentPlaceholder[$section];
}
/**
* Check if section exists.
*
* @param string $name
* @return bool
*/
public function hasSection($name)
{
return array_key_exists($name, $this->sections);
}
/**
* Check if section does not exist.
*
* @param string $name
* @return bool
*/
public function sectionMissing($name)
{
return ! $this->hasSection($name);
}
/**
* Get the contents of a section.
*
* @param string $name
* @param null|string $default
* @return mixed
*/
public function getSection($name, $default = null)
{
return $this->getSections()[$name] ?? $default;
}
/**
* Get the entire array of sections.
*
* @return array
*/
public function getSections()
{
return $this->sections;
}
/**
* Flush all of the sections.
*/
public function flushSections()
{
$this->sections = [];
$this->sectionStack = [];
}
/**
* Append content to a given section.
*
* @param string $section
* @param string $content
*/
protected function extendSection($section, $content)
{
if (isset($this->sections[$section])) {
$content = str_replace(static::parentPlaceholder($section), $content, $this->sections[$section]);
}
$this->sections[$section] = $content;
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Concern;
use Countable;
use Hyperf\Utils\Arr;
use stdClass;
trait ManagesLoops
{
/**
* The stack of in-progress loops.
*
* @var array
*/
protected $loopsStack = [];
/**
* Add new loop to the stack.
*
* @param null|array|Countable $data
*/
public function addLoop($data)
{
$length = is_array($data) || $data instanceof Countable ? count($data) : null;
$parent = Arr::last($this->loopsStack);
$this->loopsStack[] = [
'iteration' => 0,
'index' => 0,
'remaining' => $length ?? null,
'count' => $length,
'first' => true,
'last' => isset($length) ? $length == 1 : null,
'odd' => false,
'even' => true,
'depth' => count($this->loopsStack) + 1,
'parent' => $parent ? (object) $parent : null,
];
}
/**
* Increment the top loop's indices.
*/
public function incrementLoopIndices()
{
$loop = $this->loopsStack[$index = count($this->loopsStack) - 1];
$this->loopsStack[$index] = array_merge($this->loopsStack[$index], [
'iteration' => $loop['iteration'] + 1,
'index' => $loop['iteration'],
'first' => $loop['iteration'] == 0,
'odd' => ! $loop['odd'],
'even' => ! $loop['even'],
'remaining' => isset($loop['count']) ? $loop['remaining'] - 1 : null,
'last' => isset($loop['count']) ? $loop['iteration'] == $loop['count'] - 1 : null,
]);
}
/**
* Pop a loop from the top of the loop stack.
*/
public function popLoop()
{
array_pop($this->loopsStack);
}
/**
* Get an instance of the last loop in the stack.
*
* @return null|stdClass|void
*/
public function getLastLoop()
{
if ($last = Arr::last($this->loopsStack)) {
return (object) $last;
}
}
/**
* Get the entire loop stack.
*
* @return array
*/
public function getLoopStack()
{
return $this->loopsStack;
}
}

View File

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Concern;
use InvalidArgumentException;
trait ManagesStacks
{
/**
* All of the finished, captured push sections.
*
* @var array
*/
protected $pushes = [];
/**
* All of the finished, captured prepend sections.
*
* @var array
*/
protected $prepends = [];
/**
* The stack of in-progress push sections.
*
* @var array
*/
protected $pushStack = [];
/**
* Start injecting content into a push section.
*
* @param string $section
* @param string $content
*/
public function startPush($section, $content = '')
{
if ($content === '') {
if (ob_start()) {
$this->pushStack[] = $section;
}
} else {
$this->extendPush($section, $content);
}
}
/**
* Stop injecting content into a push section.
*
* @throws InvalidArgumentException
* @return string
*/
public function stopPush()
{
if (empty($this->pushStack)) {
throw new InvalidArgumentException('Cannot end a push stack without first starting one.');
}
return tap(array_pop($this->pushStack), function ($last) {
$this->extendPush($last, ob_get_clean());
});
}
/**
* Start prepending content into a push section.
*
* @param string $section
* @param string $content
*/
public function startPrepend($section, $content = '')
{
if ($content === '') {
if (ob_start()) {
$this->pushStack[] = $section;
}
} else {
$this->extendPrepend($section, $content);
}
}
/**
* Stop prepending content into a push section.
*
* @throws InvalidArgumentException
* @return string
*/
public function stopPrepend()
{
if (empty($this->pushStack)) {
throw new InvalidArgumentException('Cannot end a prepend operation without first starting one.');
}
return tap(array_pop($this->pushStack), function ($last) {
$this->extendPrepend($last, ob_get_clean());
});
}
/**
* Get the string contents of a push section.
*
* @param string $section
* @param string $default
* @return string
*/
public function yieldPushContent($section, $default = '')
{
if (! isset($this->pushes[$section]) && ! isset($this->prepends[$section])) {
return $default;
}
$output = '';
if (isset($this->prepends[$section])) {
$output .= implode(array_reverse($this->prepends[$section]));
}
if (isset($this->pushes[$section])) {
$output .= implode($this->pushes[$section]);
}
return $output;
}
/**
* Flush all of the stacks.
*/
public function flushStacks()
{
$this->pushes = [];
$this->prepends = [];
$this->pushStack = [];
}
/**
* Append content to a given push section.
*
* @param string $section
* @param string $content
*/
protected function extendPush($section, $content)
{
if (! isset($this->pushes[$section])) {
$this->pushes[$section] = [];
}
if (! isset($this->pushes[$section][$this->renderCount])) {
$this->pushes[$section][$this->renderCount] = $content;
} else {
$this->pushes[$section][$this->renderCount] .= $content;
}
}
/**
* Prepend content to a given stack.
*
* @param string $section
* @param string $content
*/
protected function extendPrepend($section, $content)
{
if (! isset($this->prepends[$section])) {
$this->prepends[$section] = [];
}
if (! isset($this->prepends[$section][$this->renderCount])) {
$this->prepends[$section][$this->renderCount] = $content;
} else {
$this->prepends[$section][$this->renderCount] = $content . $this->prepends[$section][$this->renderCount];
}
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Concern;
use Hyperf\ViewEngine\Blade;
trait ManagesTranslations
{
/**
* The translation replacements for the translation being rendered.
*
* @var array
*/
protected $translationReplacements = [];
/**
* Start a translation block.
*
* @param array $replacements
*/
public function startTranslation($replacements = [])
{
ob_start();
$this->translationReplacements = $replacements;
}
/**
* Render the current translation.
*
* @return string
*/
public function renderTranslation()
{
return Blade::container()->make('translator')->get(
trim(ob_get_clean()),
$this->translationReplacements
);
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Hyperf\ViewEngine\Command\ViewPublishCommand;
use Hyperf\ViewEngine\Compiler\CompilerInterface;
use Hyperf\ViewEngine\Component\DynamicComponent;
use Hyperf\ViewEngine\Contract\EngineResolverInterface;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\Contract\FinderInterface;
use Hyperf\ViewEngine\Factory\CompilerFactory;
use Hyperf\ViewEngine\Factory\EngineResolverFactory;
use Hyperf\ViewEngine\Factory\FinderFactory;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => [
FactoryInterface::class => Factory::class,
EngineResolverInterface::class => EngineResolverFactory::class,
FinderInterface::class => FinderFactory::class,
CompilerInterface::class => CompilerFactory::class,
],
'commands' => [
ViewPublishCommand::class,
],
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
'collectors' => [
],
],
],
'publish' => [
[
'id' => 'config',
'description' => 'The config for view.',
'source' => __DIR__ . '/../publish/view.php',
'destination' => BASE_PATH . '/config/autoload/view.php',
],
],
'view' => [
'components' => [
'dynamic-component' => DynamicComponent::class,
],
],
];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
interface DeferringDisplayableValue
{
/**
* Resolve the displayable value that the class is deferring.
*
* @return Htmlable|string
*/
public function resolveDisplayableValue();
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
interface EngineInterface
{
/**
* Get the evaluated contents of the view.
*
* @return string
*/
public function get(string $path, array $data = []);
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
use Closure;
interface EngineResolverInterface
{
public function register(string $engine, Closure $resolver);
public function resolve(string $engine): EngineInterface;
}

View File

@ -0,0 +1,901 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
use Countable;
use Hyperf\Utils\Collection;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Contracts\Jsonable;
use IteratorAggregate;
use JsonSerializable;
interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
{
/**
* Convert the collection to its string representation.
*/
public function __toString(): string;
/**
* Dynamically access collection proxies.
*
* @param string $key
* @throws \Exception
* @return mixed
*/
public function __get($key);
/**
* Create a new collection instance if the value isn't one already.
*
* @param mixed $items
* @return static
*/
public static function make($items = []);
/**
* Create a new instance by invoking the callback a given amount of times.
*
* @return static
*/
public static function times(int $number, callable $callback = null);
/**
* Wrap the given value in a collection if applicable.
*
* @param mixed $value
* @return static
*/
public static function wrap($value);
/**
* Get the underlying items from the given collection if applicable.
*
* @param array|static $value
* @return array
*/
public static function unwrap($value);
/**
* Get all items in the enumerable.
*
* @return array
*/
public function all();
/**
* Alias for the "avg" method.
*
* @param null|callable|string $callback
* @return mixed
*/
public function average($callback = null);
/**
* Get the median of a given key.
*
* @param null|array|string $key
* @return mixed
*/
public function median($key = null);
/**
* Get the mode of a given key.
*
* @param null|array|string $key
* @return null|array
*/
public function mode($key = null);
/**
* Collapse the items into a single enumerable.
*
* @return static
*/
public function collapse();
/**
* Alias for the "contains" method.
*
* @param mixed $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function some($key, $operator = null, $value = null);
/**
* Determine if an item exists, using strict comparison.
*
* @param mixed $key
* @param mixed $value
* @return bool
*/
public function containsStrict($key, $value = null);
/**
* Get the average value of a given key.
*
* @param null|callable|string $callback
* @return mixed
*/
public function avg($callback = null);
/**
* Determine if an item exists in the enumerable.
*
* @param mixed $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function contains($key, $operator = null, $value = null);
/**
* Dump the collection and end the script.
*
* @param mixed ...$args
*/
public function dd(...$args);
/**
* Dump the collection.
*
* @return $this
*/
public function dump();
/**
* Get the items that are not present in the given items.
*
* @param mixed $items
* @return static
*/
public function diff($items);
/**
* Get the items that are not present in the given items, using the callback.
*
* @param mixed $items
* @return static
*/
public function diffUsing($items, callable $callback);
/**
* Get the items whose keys and values are not present in the given items.
*
* @param mixed $items
* @return static
*/
public function diffAssoc($items);
/**
* Get the items whose keys and values are not present in the given items, using the callback.
*
* @param mixed $items
* @return static
*/
public function diffAssocUsing($items, callable $callback);
/**
* Get the items whose keys are not present in the given items.
*
* @param mixed $items
* @return static
*/
public function diffKeys($items);
/**
* Get the items whose keys are not present in the given items, using the callback.
*
* @param mixed $items
* @return static
*/
public function diffKeysUsing($items, callable $callback);
/**
* Retrieve duplicate items.
*
* @param null|callable $callback
* @param bool $strict
* @return static
*/
public function duplicates($callback = null, $strict = false);
/**
* Retrieve duplicate items using strict comparison.
*
* @param null|callable $callback
* @return static
*/
public function duplicatesStrict($callback = null);
/**
* Execute a callback over each item.
*
* @return $this
*/
public function each(callable $callback);
/**
* Execute a callback over each nested chunk of items.
*
* @return static
*/
public function eachSpread(callable $callback);
/**
* Determine if all items pass the given truth test.
*
* @param callable|string $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function every($key, $operator = null, $value = null);
/**
* Get all items except for those with the specified keys.
*
* @param mixed $keys
* @return static
*/
public function except($keys);
/**
* Run a filter over each of the items.
*
* @return static
*/
public function filter(callable $callback = null);
/**
* Apply the callback if the value is truthy.
*
* @param bool $value
* @return mixed|static
*/
public function when($value, callable $callback, callable $default = null);
/**
* Apply the callback if the collection is empty.
*
* @return mixed|static
*/
public function whenEmpty(callable $callback, callable $default = null);
/**
* Apply the callback if the collection is not empty.
*
* @return mixed|static
*/
public function whenNotEmpty(callable $callback, callable $default = null);
/**
* Apply the callback if the value is falsy.
*
* @param bool $value
* @return mixed|static
*/
public function unless($value, callable $callback, callable $default = null);
/**
* Apply the callback unless the collection is empty.
*
* @return mixed|static
*/
public function unlessEmpty(callable $callback, callable $default = null);
/**
* Apply the callback unless the collection is not empty.
*
* @return mixed|static
*/
public function unlessNotEmpty(callable $callback, callable $default = null);
/**
* Filter items by the given key value pair.
*
* @param string $key
* @param mixed $operator
* @param mixed $value
* @return static
*/
public function where($key, $operator = null, $value = null);
/**
* Filter items by the given key value pair using strict comparison.
*
* @param string $key
* @param mixed $value
* @return static
*/
public function whereStrict($key, $value);
/**
* Filter items by the given key value pair.
*
* @param string $key
* @param mixed $values
* @param bool $strict
* @return static
*/
public function whereIn($key, $values, $strict = false);
/**
* Filter items by the given key value pair using strict comparison.
*
* @param string $key
* @param mixed $values
* @return static
*/
public function whereInStrict($key, $values);
/**
* Filter items such that the value of the given key is between the given values.
*
* @param string $key
* @param array $values
* @return static
*/
public function whereBetween($key, $values);
/**
* Filter items such that the value of the given key is not between the given values.
*
* @param string $key
* @param array $values
* @return static
*/
public function whereNotBetween($key, $values);
/**
* Filter items by the given key value pair.
*
* @param string $key
* @param mixed $values
* @param bool $strict
* @return static
*/
public function whereNotIn($key, $values, $strict = false);
/**
* Filter items by the given key value pair using strict comparison.
*
* @param string $key
* @param mixed $values
* @return static
*/
public function whereNotInStrict($key, $values);
/**
* Filter the items, removing any items that don't match the given type.
*
* @param string $type
* @return static
*/
public function whereInstanceOf($type);
/**
* Get the first item from the enumerable passing the given truth test.
*
* @param mixed $default
* @return mixed
*/
public function first(callable $callback = null, $default = null);
/**
* Get the first item by the given key value pair.
*
* @param string $key
* @param mixed $operator
* @param mixed $value
* @return mixed
*/
public function firstWhere($key, $operator = null, $value = null);
/**
* Flip the values with their keys.
*
* @return static
*/
public function flip();
/**
* Get an item from the collection by key.
*
* @param mixed $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null);
/**
* Group an associative array by a field or using a callback.
*
* @param array|callable|string $groupBy
* @param bool $preserveKeys
* @return static
*/
public function groupBy($groupBy, $preserveKeys = false);
/**
* Key an associative array by a field or using a callback.
*
* @param callable|string $keyBy
* @return static
*/
public function keyBy($keyBy);
/**
* Determine if an item exists in the collection by key.
*
* @param mixed $key
* @return bool
*/
public function has($key);
/**
* Concatenate values of a given key as a string.
*
* @param string $value
* @param null|string $glue
* @return string
*/
public function implode($value, $glue = null);
/**
* Intersect the collection with the given items.
*
* @param mixed $items
* @return static
*/
public function intersect($items);
/**
* Intersect the collection with the given items by key.
*
* @param mixed $items
* @return static
*/
public function intersectByKeys($items);
/**
* Determine if the collection is empty or not.
*
* @return bool
*/
public function isEmpty();
/**
* Determine if the collection is not empty.
*
* @return bool
*/
public function isNotEmpty();
/**
* Join all items from the collection using a string. The final items can use a separate glue string.
*
* @param string $glue
* @param string $finalGlue
* @return string
*/
public function join($glue, $finalGlue = '');
/**
* Get the keys of the collection items.
*
* @return static
*/
public function keys();
/**
* Get the last item from the collection.
*
* @param mixed $default
* @return mixed
*/
public function last(callable $callback = null, $default = null);
/**
* Run a map over each of the items.
*
* @return static
*/
public function map(callable $callback);
/**
* Run a map over each nested chunk of items.
*
* @return static
*/
public function mapSpread(callable $callback);
/**
* Run a dictionary map over the items.
*
* The callback should return an associative array with a single key/value pair.
*
* @return static
*/
public function mapToDictionary(callable $callback);
/**
* Run a grouping map over the items.
*
* The callback should return an associative array with a single key/value pair.
*
* @return static
*/
public function mapToGroups(callable $callback);
/**
* Run an associative map over each of the items.
*
* The callback should return an associative array with a single key/value pair.
*
* @return static
*/
public function mapWithKeys(callable $callback);
/**
* Map a collection and flatten the result by a single level.
*
* @return static
*/
public function flatMap(callable $callback);
/**
* Map the values into a new class.
*
* @param string $class
* @return static
*/
public function mapInto($class);
/**
* Merge the collection with the given items.
*
* @param mixed $items
* @return static
*/
public function merge($items);
/**
* Recursively merge the collection with the given items.
*
* @param mixed $items
* @return static
*/
public function mergeRecursive($items);
/**
* Create a collection by using this collection for keys and another for its values.
*
* @param mixed $values
* @return static
*/
public function combine($values);
/**
* Union the collection with the given items.
*
* @param mixed $items
* @return static
*/
public function union($items);
/**
* Get the min value of a given key.
*
* @param null|callable|string $callback
* @return mixed
*/
public function min($callback = null);
/**
* Get the max value of a given key.
*
* @param null|callable|string $callback
* @return mixed
*/
public function max($callback = null);
/**
* Create a new collection consisting of every n-th element.
*
* @param int $step
* @param int $offset
* @return static
*/
public function nth($step, $offset = 0);
/**
* Get the items with the specified keys.
*
* @param mixed $keys
* @return static
*/
public function only($keys);
/**
* "Paginate" the collection by slicing it into a smaller collection.
*
* @param int $page
* @param int $perPage
* @return static
*/
public function forPage($page, $perPage);
/**
* Partition the collection into two arrays using the given callback or key.
*
* @param callable|string $key
* @param mixed $operator
* @param mixed $value
* @return static
*/
public function partition($key, $operator = null, $value = null);
/**
* Push all of the given items onto the collection.
*
* @param iterable $source
* @return static
*/
public function concat($source);
/**
* Get one or a specified number of items randomly from the collection.
*
* @param null|int $number
* @throws \InvalidArgumentException
* @return mixed|static
*/
public function random($number = null);
/**
* Reduce the collection to a single value.
*
* @param mixed $initial
* @return mixed
*/
public function reduce(callable $callback, $initial = null);
/**
* Replace the collection items with the given items.
*
* @param mixed $items
* @return static
*/
public function replace($items);
/**
* Recursively replace the collection items with the given items.
*
* @param mixed $items
* @return static
*/
public function replaceRecursive($items);
/**
* Reverse items order.
*
* @return static
*/
public function reverse();
/**
* Search the collection for a given value and return the corresponding key if successful.
*
* @param mixed $value
* @param bool $strict
* @return mixed
*/
public function search($value, $strict = false);
/**
* Shuffle the items in the collection.
*
* @param null|int $seed
* @return static
*/
public function shuffle($seed = null);
/**
* Skip the first {$count} items.
*
* @param int $count
* @return static
*/
public function skip($count);
/**
* Get a slice of items from the enumerable.
*
* @param int $offset
* @param null|int $length
* @return static
*/
public function slice($offset, $length = null);
/**
* Split a collection into a certain number of groups.
*
* @param int $numberOfGroups
* @return static
*/
public function split($numberOfGroups);
/**
* Chunk the collection into chunks of the given size.
*
* @param int $size
* @return static
*/
public function chunk($size);
/**
* Sort through each item with a callback.
*
* @param null|callable|int $callback
* @return static
*/
public function sort($callback = null);
/**
* Sort items in descending order.
*
* @param int $options
* @return static
*/
public function sortDesc($options = SORT_REGULAR);
/**
* Sort the collection using the given callback.
*
* @param callable|string $callback
* @param int $options
* @param bool $descending
* @return static
*/
public function sortBy($callback, $options = SORT_REGULAR, $descending = false);
/**
* Sort the collection in descending order using the given callback.
*
* @param callable|string $callback
* @param int $options
* @return static
*/
public function sortByDesc($callback, $options = SORT_REGULAR);
/**
* Sort the collection keys.
*
* @param int $options
* @param bool $descending
* @return static
*/
public function sortKeys($options = SORT_REGULAR, $descending = false);
/**
* Sort the collection keys in descending order.
*
* @param int $options
* @return static
*/
public function sortKeysDesc($options = SORT_REGULAR);
/**
* Get the sum of the given values.
*
* @param null|callable|string $callback
* @return mixed
*/
public function sum($callback = null);
/**
* Take the first or last {$limit} items.
*
* @param int $limit
* @return static
*/
public function take($limit);
/**
* Pass the collection to the given callback and then return it.
*
* @return $this
*/
public function tap(callable $callback);
/**
* Pass the enumerable to the given callback and return the result.
*
* @return mixed
*/
public function pipe(callable $callback);
/**
* Get the values of a given key.
*
* @param array|string $value
* @param null|string $key
* @return static
*/
public function pluck($value, $key = null);
/**
* Create a collection of all elements that do not pass a given truth test.
*
* @param callable|mixed $callback
* @return static
*/
public function reject($callback = true);
/**
* Return only unique items from the collection array.
*
* @param null|callable|string $key
* @param bool $strict
* @return static
*/
public function unique($key = null, $strict = false);
/**
* Return only unique items from the collection array using strict comparison.
*
* @param null|callable|string $key
* @return static
*/
public function uniqueStrict($key = null);
/**
* Reset the keys on the underlying array.
*
* @return static
*/
public function values();
/**
* Pad collection to the specified length with a value.
*
* @param int $size
* @param mixed $value
* @return static
*/
public function pad($size, $value);
/**
* Count the number of items in the collection using a given truth test.
*
* @param null|callable $callback
* @return static
*/
public function countBy($callback = null);
/**
* Collect the values into a collection.
*
* @return Collection
*/
public function collect();
/**
* Add a method to the list of proxied methods.
*
* @param string $method
*/
public static function proxy($method);
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
use Closure;
use Hyperf\Utils\Contracts\Arrayable;
interface FactoryInterface
{
/**
* Determine if a given view exists.
*/
public function exists(string $view): bool;
/**
* Get the evaluated view contents for the given path.
*
* @param array|Arrayable $data
*/
public function file(string $path, $data = [], array $mergeData = []): ViewInterface;
/**
* Get the evaluated view contents for the given view.
*
* @param array|Arrayable $data
*/
public function make(string $view, $data = [], array $mergeData = []): ViewInterface;
/**
* Add a piece of shared data to the environment.
*
* @param array|string $key
* @param mixed $value
* @return mixed
*/
public function share($key, $value = null);
/**
* Register a view composer event.
*
* @param array|string $views
* @param Closure|string $callback
*/
public function composer($views, $callback);
/**
* Register a view creator event.
*
* @param array|string $views
* @param Closure|string $callback
*/
public function creator($views, $callback);
/**
* Add a new namespace to the loader.
*
* @param array|string $hints
* @return $this
*/
public function addNamespace(string $namespace, $hints);
/**
* Replace the namespace hints for the given namespace.
*
* @param array|string $hints
* @return $this
*/
public function replaceNamespace(string $namespace, $hints);
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
interface FinderInterface
{
/**
* Hint path delimiter value.
*
* @var string
*/
const HINT_PATH_DELIMITER = '::';
/**
* Get the fully qualified location of the view.
*
* @return string
*/
public function find(string $view);
/**
* Add a location to the finder.
*/
public function addLocation(string $location);
/**
* Add a namespace hint to the finder.
*
* @param array|string $hints
*/
public function addNamespace(string $namespace, $hints);
/**
* Prepend a namespace hint to the finder.
*
* @param array|string $hints
*/
public function prependNamespace(string $namespace, $hints);
/**
* Replace the namespace hints for the given namespace.
*
* @param array|string $hints
*/
public function replaceNamespace(string $namespace, $hints);
/**
* Add a valid view extension to the finder.
*/
public function addExtension(string $extension);
/**
* Flush the cache of located views.
*/
public function flush();
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
interface Htmlable
{
/**
* Get content as a string of HTML.
*/
public function toHtml();
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
interface Renderable
{
/**
* Get the evaluated contents of the object.
*/
public function render();
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Contract;
interface ViewInterface extends Renderable
{
/**
* Get the name of the view.
*/
public function name(): string;
/**
* Add a piece of data to the view.
*
* @param array|string $key
* @param mixed $value
* @return $this
*/
public function with($key, $value = null);
/**
* Get the array of view data.
*/
public function getData(): array;
}

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Engine;
use ErrorException;
use Hyperf\Utils\Filesystem\Filesystem;
use Hyperf\ViewEngine\Compiler\CompilerInterface;
use Throwable;
class CompilerEngine extends PhpEngine
{
/**
* The Blade compiler instance.
*
* @var CompilerInterface
*/
protected $compiler;
/**
* A stack of the last compiled templates.
*
* @var array
*/
protected $lastCompiled = [];
/**
* Create a new compiler engine instance.
*/
public function __construct(CompilerInterface $compiler, Filesystem $files = null)
{
parent::__construct($files ?: new Filesystem());
$this->compiler = $compiler;
}
/**
* Get the evaluated contents of the view.
*
* @param string $path
* @return string
*/
public function get($path, array $data = [])
{
$this->lastCompiled[] = $path;
// If this given view has expired, which means it has simply been edited since
// it was last compiled, we will re-compile the views so we can evaluate a
// fresh copy of the view. We'll pass the compiler the path of the view.
if ($this->compiler->isExpired($path)) {
$this->compiler->compile($path);
}
// Once we have the path to the compiled file, we will evaluate the paths with
// typical PHP just like any other templates. We also keep a stack of views
// which have been rendered for right exception messages to be generated.
$results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);
array_pop($this->lastCompiled);
return $results;
}
/**
* Get the compiler implementation.
*
* @return CompilerInterface
*/
public function getCompiler()
{
return $this->compiler;
}
/**
* Handle a view exception.
*
* @param int $obLevel
*
* @throws Throwable
*/
protected function handleViewException(Throwable $e, $obLevel)
{
$e = new ErrorException($this->getMessage($e), 0, 1, $e->getFile(), $e->getLine(), $e);
parent::handleViewException($e, $obLevel);
}
/**
* Get the exception message for an exception.
*
* @return string
*/
protected function getMessage(Throwable $e)
{
return $e->getMessage() . ' (View: ' . realpath(last($this->lastCompiled)) . ')';
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Engine;
abstract class Engine
{
/**
* The view that was last to be rendered.
*
* @var string
*/
protected $lastRendered;
/**
* Get the last view that was rendered.
*
* @return string
*/
public function getLastRendered()
{
return $this->lastRendered;
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Engine;
use Closure;
use Hyperf\ViewEngine\Contract\EngineInterface;
use Hyperf\ViewEngine\Contract\EngineResolverInterface;
use InvalidArgumentException;
class EngineResolver implements EngineResolverInterface
{
/**
* The array of engine resolvers.
*
* @var array
*/
protected $resolvers = [];
/**
* The resolved engine instances.
*
* @var array
*/
protected $resolved = [];
/**
* Register a new engine resolver.
*
* The engine string typically corresponds to a file extension.
*/
public function register(string $engine, Closure $resolver)
{
unset($this->resolved[$engine]);
$this->resolvers[$engine] = $resolver;
}
/**
* Resolve an engine instance by name.
*
* @throws InvalidArgumentException
*/
public function resolve(string $engine): EngineInterface
{
if (isset($this->resolved[$engine])) {
return $this->resolved[$engine];
}
if (isset($this->resolvers[$engine])) {
return $this->resolved[$engine] = call_user_func($this->resolvers[$engine]);
}
throw new InvalidArgumentException("Engine [{$engine}] not found.");
}
public static function getInstance($resolvers = [])
{
$resolver = new EngineResolver();
foreach ($resolvers as $engine => $engineResolver) {
$resolver->register($engine, function () use ($engineResolver) {
return make($engineResolver);
});
}
return $resolver;
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Engine;
use Hyperf\Utils\Filesystem\Filesystem;
use Hyperf\ViewEngine\Contract\EngineInterface;
class FileEngine implements EngineInterface
{
/**
* The filesystem instance.
*
* @var Filesystem
*/
protected $files;
/**
* Create a new file engine instance.
*/
public function __construct(Filesystem $files)
{
$this->files = $files;
}
/**
* Get the evaluated contents of the view.
*
* @return string
*/
public function get(string $path, array $data = [])
{
return $this->files->get($path);
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Engine;
use Hyperf\Utils\Filesystem\Filesystem;
use Hyperf\ViewEngine\Contract\EngineInterface;
use Throwable;
class PhpEngine implements EngineInterface
{
/**
* The filesystem instance.
*
* @var Filesystem
*/
protected $files;
/**
* Create a new file engine instance.
*/
public function __construct(Filesystem $files)
{
$this->files = $files;
}
/**
* Get the evaluated contents of the view.
*
* @return string
*/
public function get(string $path, array $data = [])
{
return $this->evaluatePath($path, $data);
}
/**
* Get the evaluated contents of the view at the given path.
*
* @param string $__path
* @param array $__data
* @return string
*/
protected function evaluatePath($__path, $__data)
{
$obLevel = ob_get_level();
ob_start();
extract($__data, EXTR_SKIP);
// We'll evaluate the contents of the view inside a try/catch block so we can
// flush out any stray output that might get out before an error occurs or
// an exception is thrown. This prevents any partial views from leaking.
try {
include $__path;
} catch (Throwable $e) {
$this->handleViewException($e, $obLevel);
}
return ltrim(ob_get_clean());
}
/**
* Handle a view exception.
*
* @param int $obLevel
*
* @throws Throwable
*/
protected function handleViewException(Throwable $e, $obLevel)
{
while (ob_get_level() > $obLevel) {
ob_end_clean();
}
throw $e;
}
}

546
src/view-engine/src/Factory.php Executable file
View File

@ -0,0 +1,546 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Closure;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Str;
use Hyperf\Utils\Traits\Macroable;
use Hyperf\ViewEngine\Contract\EngineInterface;
use Hyperf\ViewEngine\Contract\EngineResolverInterface;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\Contract\FinderInterface;
use Hyperf\ViewEngine\Contract\ViewInterface;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
class Factory implements FactoryInterface
{
use Macroable;
use Concern\ManagesComponents;
use Concern\ManagesEvents;
use Concern\ManagesLayouts;
use Concern\ManagesLoops;
use Concern\ManagesStacks;
use Concern\ManagesTranslations;
/**
* The engine implementation.
*
* @var EngineResolverInterface
*/
protected $engines;
/**
* The view finder implementation.
*
* @var FinderInterface
*/
protected $finder;
/**
* The event dispatcher instance.
*
* @var EventDispatcherInterface
*/
protected $events;
/**
* The IoC container instance.
*
* @var null|ContainerInterface
*/
protected $container;
/**
* Data that should be available to all templates.
*
* @var array
*/
protected $shared = [];
/**
* The extension to engine bindings.
*
* @var array
*/
protected $extensions = [
'blade.php' => 'blade',
'php' => 'php',
'css' => 'file',
'html' => 'file',
];
/**
* The view composer events.
*
* @var array
*/
protected $composers = [];
/**
* The number of active rendering operations.
*
* @var int
*/
protected $renderCount = 0;
/**
* The "once" block IDs that have been rendered.
*
* @var array
*/
protected $renderedOnce = [];
/**
* Factory constructor.
*/
public function __construct(
EngineResolverInterface $engines,
FinderInterface $finder,
EventDispatcherInterface $events
) {
$this->finder = $finder;
$this->events = $events;
$this->engines = $engines;
$this->share('__env', $this);
$this->share('errors', new ViewErrorBag());
}
/**
* Get the evaluated view contents for the given view.
*
* @param array|Arrayable $data
*/
public function file(string $path, $data = [], array $mergeData = []): ViewInterface
{
$data = array_merge($mergeData, $this->parseData($data));
return tap($this->viewInstance($path, $path, $data), function ($view) {
$this->callCreator($view);
});
}
/**
* Get the evaluated view contents for the given view.
*
* @param array|Arrayable $data
*/
public function make(string $view, $data = [], array $mergeData = []): ViewInterface
{
$path = $this->finder->find(
$view = $this->normalizeName($view)
);
// Next, we will create the view instance and call the view creator for the view
// which can set any data, etc. Then we will return the view instance back to
// the caller for rendering or performing other view manipulations on this.
$data = array_merge($mergeData, $this->parseData($data));
return tap($this->viewInstance($view, $path, $data), function ($view) {
$this->callCreator($view);
});
}
/**
* Get the first view that actually exists from the given list.
*
* @param array|Arrayable $data
* @throws InvalidArgumentException
*/
public function first(array $views, $data = [], array $mergeData = []): ViewInterface
{
$view = Arr::first($views, function ($view) {
return $this->exists($view);
});
if (! $view) {
throw new InvalidArgumentException('None of the views in the given array exist.');
}
return $this->make($view, $data, $mergeData);
}
/**
* Get the rendered content of the view based on a given condition.
*
* @param array|Arrayable $data
* @return string
*/
public function renderWhen(bool $condition, string $view, $data = [], array $mergeData = [])
{
if (! $condition) {
return '';
}
return $this->make($view, $this->parseData($data), $mergeData)->render();
}
/**
* Get the rendered contents of a partial from a loop.
*
* @return string
*/
public function renderEach(string $view, array $data, string $iterator, string $empty = 'raw|')
{
$result = '';
// If is actually data in the array, we will loop through the data and append
// an instance of the partial view to the final result HTML passing in the
// iterated value of this data array, allowing the views to access them.
if (count($data) > 0) {
foreach ($data as $key => $value) {
$result .= $this->make(
$view,
['key' => $key, $iterator => $value]
)->render();
}
}
// If there is no data in the array, we will render the contents of the empty
// view. Alternatively, the "empty view" could be a raw string that begins
// with "raw|" for convenience and to let this know that it is a string.
else {
$result = Str::startsWith($empty, 'raw|')
? substr($empty, 4)
: $this->make($empty)->render();
}
return $result;
}
/**
* Determine if a given view exists.
*/
public function exists(string $view): bool
{
try {
$this->finder->find($view);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
/**
* Get the appropriate view engine for the given path.
*
* @throws InvalidArgumentException
* @return EngineInterface
*/
public function getEngineFromPath(string $path)
{
if (! $extension = $this->getExtension($path)) {
throw new InvalidArgumentException("Unrecognized extension in file: {$path}.");
}
$engine = $this->extensions[$extension];
return $this->engines->resolve($engine);
}
/**
* Add a piece of shared data to the environment.
*
* @param array|string $key
* @param null|mixed $value
* @return mixed
*/
public function share($key, $value = null)
{
$keys = is_array($key) ? $key : [$key => $value];
foreach ($keys as $key => $value) {
$this->shared[$key] = $value;
}
return $value;
}
/**
* Increment the rendering counter.
*/
public function incrementRender()
{
++$this->renderCount;
}
/**
* Decrement the rendering counter.
*/
public function decrementRender()
{
--$this->renderCount;
}
/**
* Check if there are no active render operations.
*
* @return bool
*/
public function doneRendering()
{
return $this->renderCount == 0;
}
/**
* Determine if the given once token has been rendered.
*/
public function hasRenderedOnce(string $id): bool
{
return isset($this->renderedOnce[$id]);
}
/**
* Mark the given once token as having been rendered.
*/
public function markAsRenderedOnce(string $id)
{
$this->renderedOnce[$id] = true;
}
/**
* Add a location to the array of view locations.
*/
public function addLocation(string $location)
{
$this->finder->addLocation($location);
}
/**
* Add a new namespace to the loader.
*
* @param array|string $hints
* @return $this
*/
public function addNamespace(string $namespace, $hints)
{
$this->finder->addNamespace($namespace, $hints);
return $this;
}
/**
* Prepend a new namespace to the loader.
*
* @param array|string $hints
* @return $this
*/
public function prependNamespace(string $namespace, $hints)
{
$this->finder->prependNamespace($namespace, $hints);
return $this;
}
/**
* Replace the namespace hints for the given namespace.
*
* @param array|string $hints
* @return $this
*/
public function replaceNamespace(string $namespace, $hints)
{
$this->finder->replaceNamespace($namespace, $hints);
return $this;
}
/**
* Register a valid view extension and its engine.
*/
public function addExtension(string $extension, string $engine, ?Closure $resolver = null)
{
$this->finder->addExtension($extension);
if (isset($resolver)) {
$this->engines->register($engine, $resolver);
}
unset($this->extensions[$extension]);
$this->extensions = array_merge([$extension => $engine], $this->extensions);
}
/**
* Flush all of the factory state like sections and stacks.
*/
public function flushState()
{
$this->renderCount = 0;
$this->renderedOnce = [];
$this->flushSections();
$this->flushStacks();
}
/**
* Flush all of the section contents if done rendering.
*/
public function flushStateIfDoneRendering()
{
if ($this->doneRendering()) {
$this->flushState();
}
}
/**
* Get the extension to engine bindings.
*/
public function getExtensions(): array
{
return $this->extensions;
}
/**
* Get the engine resolver instance.
*/
public function getEngineResolver(): EngineResolverInterface
{
return $this->engines;
}
/**
* Get the view finder instance.
*/
public function getFinder(): FinderInterface
{
return $this->finder;
}
/**
* Set the view finder instance.
*/
public function setFinder(FinderInterface $finder)
{
$this->finder = $finder;
}
/**
* Flush the cache of views located by the finder.
*/
public function flushFinderCache()
{
$this->getFinder()->flush();
}
/**
* Get the event dispatcher instance.
*/
public function getDispatcher(): EventDispatcherInterface
{
return $this->events;
}
/**
* Set the event dispatcher instance.
*/
public function setDispatcher(EventDispatcherInterface $events)
{
$this->events = $events;
}
/**
* Get the IoC container instance.
*/
public function getContainer(): ContainerInterface
{
if (! $this->container) {
$this->setContainer(Blade::container());
}
return $this->container;
}
/**
* Set the IoC container instance.
*/
public function setContainer(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Get an item from the shared data.
*
* @param mixed $default
* @return mixed
*/
public function shared(string $key, $default = null)
{
return Arr::get($this->shared, $key, $default);
}
/**
* Get all of the shared data for the environment.
*/
public function getShared(): array
{
return $this->shared;
}
/**
* Normalize a view name.
*/
protected function normalizeName(string $name): string
{
$delimiter = FinderInterface::HINT_PATH_DELIMITER;
if (strpos($name, $delimiter) === false) {
return str_replace('/', '.', $name);
}
[$namespace, $name] = explode($delimiter, $name);
return $namespace . $delimiter . str_replace('/', '.', $name);
}
/**
* Parse the given data into a raw array.
*
* @param array|Arrayable $data
* @return array
*/
protected function parseData($data)
{
return $data instanceof Arrayable ? $data->toArray() : $data;
}
/**
* Create a new view instance from the given arguments.
*
* @param array|Arrayable $data
* @return ViewInterface
*/
protected function viewInstance(string $view, string $path, $data)
{
return new View($this, $this->getEngineFromPath($path), $view, $path, $data);
}
/**
* Get the extension used by the view file.
*
* @return null|string
*/
protected function getExtension(string $path)
{
$extensions = array_keys($this->extensions);
return Arr::first($extensions, function ($value) use ($path) {
return Str::endsWith($path, '.' . $value);
});
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Factory;
use Hyperf\Di\Container;
use Hyperf\Utils\Filesystem\Filesystem;
use Hyperf\ViewEngine\Blade;
use Hyperf\ViewEngine\Compiler\BladeCompiler;
class CompilerFactory
{
public function __invoke(Container $container)
{
$blade = new BladeCompiler(
$container->get(Filesystem::class),
Blade::config('config.cache_path')
);
// register view components
foreach ((array) Blade::config('components', []) as $alias => $class) {
$blade->component($class, $alias);
}
return $blade;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Factory;
use Hyperf\Di\Container;
use Hyperf\ViewEngine\Engine\CompilerEngine;
use Hyperf\ViewEngine\Engine\EngineResolver;
use Hyperf\ViewEngine\Engine\FileEngine;
use Hyperf\ViewEngine\Engine\PhpEngine;
class EngineResolverFactory
{
public function __invoke(Container $container)
{
return EngineResolver::getInstance([
'blade' => CompilerEngine::class,
'php' => PhpEngine::class,
'file' => FileEngine::class,
]);
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Factory;
use Hyperf\Utils\Filesystem\Filesystem;
use Hyperf\ViewEngine\Blade;
use Hyperf\ViewEngine\Finder;
use Psr\Container\ContainerInterface;
class FinderFactory
{
public function __invoke(ContainerInterface $container)
{
$finder = new Finder(
$container->get(Filesystem::class),
(array) Blade::config('config.view_path')
);
$this->addNamespaces($finder, (array) Blade::config('namespaces', []));
$this->addNamespace($finder, '__components', $directory = Blade::config('config.cache_path'));
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}
return $finder;
}
public function addNamespace(Finder $finder, string $namespace, $hints)
{
foreach ($finder->getPaths() as $viewPath) {
if (is_dir($appPath = $viewPath . '/vendor/' . $namespace)) {
$finder->addNamespace($namespace, $appPath);
}
}
$finder->addNamespace($namespace, $hints);
}
public function addNamespaces(Finder $finder, array $namespaces)
{
foreach ($namespaces as $namespace => $hints) {
$this->addNamespace($finder, $namespace, $hints);
}
}
}

326
src/view-engine/src/Finder.php Executable file
View File

@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Hyperf\Utils\Filesystem\Filesystem;
use Hyperf\ViewEngine\Contract\FinderInterface;
use InvalidArgumentException;
class Finder implements FinderInterface
{
/**
* The filesystem instance.
*
* @var Filesystem
*/
protected $files;
/**
* The array of active view paths.
*
* @var array
*/
protected $paths;
/**
* The array of views that have been located.
*
* @var array
*/
protected $views = [];
/**
* The namespace to file path hints.
*
* @var array
*/
protected $hints = [];
/**
* Register a view extension with the finder.
*
* @var array
*/
protected $extensions = ['blade.php', 'php', 'css', 'html'];
/**
* Create a new file view loader instance.
*/
public function __construct(Filesystem $files, array $paths, array $extensions = null)
{
$this->files = $files;
$this->paths = array_map([$this, 'resolvePath'], $paths);
if (isset($extensions) && $extensions) {
$this->extensions = $extensions;
}
}
/**
* Get the fully qualified location of the view.
*
* @param string $name
* @return string
*/
public function find($name)
{
if (isset($this->views[$name])) {
return $this->views[$name];
}
if ($this->hasHintInformation($name = trim($name))) {
return $this->views[$name] = $this->findNamespacedView($name);
}
return $this->views[$name] = $this->findInPaths($name, $this->paths);
}
/**
* Add a location to the finder.
*
* @param string $location
*/
public function addLocation($location)
{
$this->paths[] = $this->resolvePath($location);
}
/**
* Prepend a location to the finder.
*
* @param string $location
*/
public function prependLocation($location)
{
array_unshift($this->paths, $this->resolvePath($location));
}
/**
* Add a namespace hint to the finder.
*
* @param array|string $hints
*/
public function addNamespace(string $namespace, $hints)
{
$hints = (array) $hints;
if (isset($this->hints[$namespace])) {
$hints = array_merge($this->hints[$namespace], $hints);
}
$this->hints[$namespace] = $hints;
}
/**
* Prepend a namespace hint to the finder.
*
* @param string $namespace
* @param array|string $hints
*/
public function prependNamespace($namespace, $hints)
{
$hints = (array) $hints;
if (isset($this->hints[$namespace])) {
$hints = array_merge($hints, $this->hints[$namespace]);
}
$this->hints[$namespace] = $hints;
}
/**
* Replace the namespace hints for the given namespace.
*
* @param string $namespace
* @param array|string $hints
*/
public function replaceNamespace($namespace, $hints)
{
$this->hints[$namespace] = (array) $hints;
}
/**
* Register an extension with the view finder.
*
* @param string $extension
*/
public function addExtension($extension)
{
if (($index = array_search($extension, $this->extensions)) !== false) {
unset($this->extensions[$index]);
}
array_unshift($this->extensions, $extension);
}
/**
* Returns whether or not the view name has any hint information.
*
* @param string $name
* @return bool
*/
public function hasHintInformation($name)
{
return strpos($name, static::HINT_PATH_DELIMITER) > 0;
}
/**
* Flush the cache of located views.
*/
public function flush()
{
$this->views = [];
}
/**
* Get the filesystem instance.
*
* @return Filesystem
*/
public function getFilesystem()
{
return $this->files;
}
/**
* Set the active view paths.
*
* @param array $paths
* @return $this
*/
public function setPaths($paths)
{
$this->paths = $paths;
return $this;
}
/**
* Get the active view paths.
*
* @return array
*/
public function getPaths()
{
return $this->paths;
}
/**
* Get the views that have been located.
*
* @return array
*/
public function getViews()
{
return $this->views;
}
/**
* Get the namespace to file path hints.
*
* @return array
*/
public function getHints()
{
return $this->hints;
}
/**
* Get registered extensions.
*
* @return array
*/
public function getExtensions()
{
return $this->extensions;
}
/**
* Get the path to a template with a named path.
*
* @param string $name
* @return string
*/
protected function findNamespacedView($name)
{
[$namespace, $view] = $this->parseNamespaceSegments($name);
return $this->findInPaths($view, $this->hints[$namespace]);
}
/**
* Get the segments of a template with a named path.
*
* @param string $name
* @throws InvalidArgumentException
* @return array
*/
protected function parseNamespaceSegments($name)
{
$segments = explode(static::HINT_PATH_DELIMITER, $name);
if (count($segments) !== 2) {
throw new InvalidArgumentException("View [{$name}] has an invalid name.");
}
if (! isset($this->hints[$segments[0]])) {
throw new InvalidArgumentException("No hint path defined for [{$segments[0]}].");
}
return $segments;
}
/**
* Find the given view in the list of paths.
*
* @param string $name
* @param array $paths
* @throws InvalidArgumentException
* @return string
*/
protected function findInPaths($name, $paths)
{
foreach ((array) $paths as $path) {
foreach ($this->getPossibleViewFiles($name) as $file) {
if ($this->files->exists($viewPath = $path . '/' . $file)) {
return $viewPath;
}
}
}
throw new InvalidArgumentException("View [{$name}] not found.");
}
/**
* Get an array of possible view files.
*
* @param string $name
* @return array
*/
protected function getPossibleViewFiles($name)
{
return array_map(function ($extension) use ($name) {
return str_replace('.', '/', $name) . '.' . $extension;
}, $this->extensions);
}
/**
* Resolve the path.
*
* @param string $path
* @return string
*/
protected function resolvePath($path)
{
return realpath($path) ?: $path;
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Context;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\View\RenderInterface;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\Contract\ViewInterface;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
if (! function_exists('Hyperf\\ViewEngine\\view')) {
/**
* Get the evaluated view contents for the given view.
*
* @param null|string $view
* @param array|Arrayable $data
* @param array $mergeData
* @return FactoryInterface|ViewInterface
*/
function view($view = null, $data = [], $mergeData = [])
{
/** @var ContainerInterface $container */
$container = ApplicationContext::getContainer();
if (interface_exists(ResponseInterface::class) && Context::has(ResponseInterface::class)) {
$contentType = $container->get(RenderInterface::class)->getContentType();
Context::set(
ResponseInterface::class,
Context::get(ResponseInterface::class)
->withAddedHeader('content-type', $contentType)
);
}
$factory = $container->get(FactoryInterface::class);
if (func_num_args() === 0) {
return $factory;
}
return $factory->make($view, $data, $mergeData);
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Hyperf\ViewEngine\Contract\Htmlable;
class HtmlString implements Htmlable
{
/**
* The HTML string.
*
* @var string
*/
protected $html;
/**
* Create a new HTML string instance.
*
* @param string $html
*/
public function __construct($html = '')
{
$this->html = $html;
}
/**
* Get the HTML string.
*
* @return string
*/
public function __toString()
{
return $this->toHtml();
}
/**
* Get the HTML string.
*
* @return string
*/
public function toHtml()
{
return $this->html;
}
/**
* Determine if the given HTML string is empty.
*
* @return bool
*/
public function isEmpty()
{
return $this->html === '';
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Http\Middleware;
use Hyperf\Contract\SessionInterface;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\ViewErrorBag;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ShareErrorsFromSession implements MiddlewareInterface
{
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var SessionInterface
*/
protected $session;
/**
* @var FactoryInterface
*/
protected $view;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->session = $container->get(SessionInterface::class);
$this->view = $container->get(FactoryInterface::class);
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var ViewErrorBag $errors */
$errors = $this->session->get('errors') ?: new ViewErrorBag();
$this->view->share('errors', $errors);
return $handler->handle($request);
}
}

View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine\Http\Middleware;
use Hyperf\Contract\SessionInterface;
use Hyperf\Session\Session;
use Hyperf\Utils\Contracts\MessageProvider;
use Hyperf\Utils\MessageBag;
use Hyperf\Validation\ValidationException;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\ViewErrorBag;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
class ValidationExceptionHandle implements MiddlewareInterface
{
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var Session
*/
protected $session;
/**
* @var FactoryInterface
*/
protected $view;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->session = $container->get(SessionInterface::class);
$this->view = $container->get(FactoryInterface::class);
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$response = $handler->handle($request);
} catch (Throwable $throwable) {
if ($throwable instanceof ValidationException) {
/* @var ValidationException $throwable */
$this->withErrors($throwable->errors(), $throwable->errorBag);
return $this->response()->redirect($this->session->previousUrl());
}
throw $throwable;
}
return $response;
}
public function withErrors($provider, $key = 'default')
{
$value = $this->parseErrors($provider);
$errors = $this->session->get('errors', new ViewErrorBag());
if (! $errors instanceof ViewErrorBag) {
$errors = new ViewErrorBag();
}
$this->session->flash(
'errors',
$errors->put($key, $value)
);
return $this;
}
protected function response()
{
return $this->container->get(\Hyperf\HttpServer\Contract\ResponseInterface::class);
}
protected function parseErrors($provider)
{
if ($provider instanceof MessageProvider) {
return $provider->getMessageBag();
}
return new MessageBag((array) $provider);
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Hyperf\Utils\ApplicationContext;
use Hyperf\View\Engine\EngineInterface;
use Hyperf\ViewEngine\Contract\FactoryInterface;
class HyperfViewEngine implements EngineInterface
{
public function render($template, $data, $config): string
{
/** @var FactoryInterface $factory */
$factory = ApplicationContext::getContainer()->get(FactoryInterface::class);
return $factory->make($template, $data)->render();
}
}

52
src/view-engine/src/T.php Normal file
View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Hyperf\Contract\TranslatorInterface;
use Hyperf\Utils\ApplicationContext;
use Hyperf\ViewEngine\Contract\DeferringDisplayableValue;
use Hyperf\ViewEngine\Contract\Htmlable;
class T
{
/**
* Encode HTML special characters in a string.
*
* @param DeferringDisplayableValue|Htmlable|string $value
* @param bool $doubleEncode
* @return string
*/
public static function e($value, $doubleEncode = true)
{
if ($value instanceof DeferringDisplayableValue) {
$value = $value->resolveDisplayableValue();
}
if ($value instanceof Htmlable) {
return $value->toHtml();
}
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8', $doubleEncode);
}
public static function inject($name)
{
return ApplicationContext::getContainer()
->get($name);
}
public static function translator()
{
return ApplicationContext::getContainer()
->get(TranslatorInterface::class);
}
}

431
src/view-engine/src/View.php Executable file
View File

@ -0,0 +1,431 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use ArrayAccess;
use BadMethodCallException;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Contracts\MessageBag;
use Hyperf\Utils\Contracts\MessageProvider;
use Hyperf\Utils\Str;
use Hyperf\Utils\Traits\Macroable;
use Hyperf\ViewEngine\Contract\EngineInterface;
use Hyperf\ViewEngine\Contract\Htmlable;
use Hyperf\ViewEngine\Contract\Renderable;
use Hyperf\ViewEngine\Contract\ViewInterface;
use Throwable;
class View implements ArrayAccess, Htmlable, ViewInterface
{
use Macroable {
__call as macroCall;
}
/**
* The view factory instance.
*
* @var Factory
*/
protected $factory;
/**
* The engine implementation.
*
* @var EngineInterface
*/
protected $engine;
/**
* The name of the view.
*
* @var string
*/
protected $view;
/**
* The array of view data.
*
* @var array
*/
protected $data;
/**
* The path to the view file.
*
* @var string
*/
protected $path;
/**
* Create a new view instance.
*
* @param string $view
* @param string $path
* @param mixed $data
*/
public function __construct(Factory $factory, EngineInterface $engine, $view, $path, $data = [])
{
$this->view = $view;
$this->path = $path;
$this->engine = $engine;
$this->factory = $factory;
$this->data = $data instanceof Arrayable ? $data->toArray() : (array) $data;
}
/**
* Set a piece of data on the view.
*
* @param string $key
* @param mixed $value
*/
public function __set($key, $value)
{
$this->with($key, $value);
}
/**
* Check if a piece of data is bound to the view.
*
* @param string $key
* @return bool
*/
public function __isset($key)
{
return isset($this->data[$key]);
}
/**
* Remove a piece of bound data from the view.
*
* @param string $key
*/
public function __unset($key)
{
unset($this->data[$key]);
}
/**
* Dynamically bind parameters to the view.
*
* @param string $method
* @param array $parameters
* @throws BadMethodCallException
* @return View
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
if (! Str::startsWith($method, 'with')) {
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.',
static::class,
$method
));
}
return $this->with(Str::camel(substr($method, 4)), $parameters[0]);
}
/**
* Get the string contents of the view.
*
* @throws Throwable
* @return string
*/
public function __toString()
{
return $this->render();
}
/**
* Get the string contents of the view.
*
* @throws Throwable
* @return array|string
*/
public function render(callable $callback = null)
{
try {
$contents = $this->renderContents();
$response = isset($callback) ? $callback($this, $contents) : null;
// Once we have the contents of the view, we will flush the sections if we are
// done rendering all views so that there is nothing left hanging over when
// another view gets rendered in the future by the application developer.
$this->factory->flushStateIfDoneRendering();
return ! is_null($response) ? $response : $contents;
} catch (Throwable $e) {
$this->factory->flushState();
throw $e;
}
}
/**
* Get the data bound to the view instance.
*
* @return array
*/
public function gatherData()
{
$data = array_merge($this->factory->getShared(), $this->data);
foreach ($data as $key => $value) {
if ($value instanceof Renderable) {
$data[$key] = $value->render();
}
}
return $data;
}
/**
* Get the sections of the rendered view.
*
* @throws Throwable
* @return array
*/
public function renderSections()
{
return $this->render(function () {
return $this->factory->getSections();
});
}
/**
* Add a piece of data to the view.
*
* @param array|string $key
* @param mixed $value
* @return $this
*/
public function with($key, $value = null)
{
if (is_array($key)) {
$this->data = array_merge($this->data, $key);
} else {
$this->data[$key] = $value;
}
return $this;
}
/**
* Add a view instance to the view data.
*
* @param string $key
* @param string $view
* @return $this
*/
public function nest($key, $view, array $data = [])
{
return $this->with($key, $this->factory->make($view, $data));
}
/**
* Add validation errors to the view.
*
* @param array|MessageProvider $provider
* @param string $bag
* @return $this
*/
public function withErrors($provider, $bag = 'default')
{
return $this->with('errors', (new ViewErrorBag())->put(
$bag,
$this->formatErrors($provider)
));
}
/**
* Get the name of the view.
*/
public function name(): string
{
return $this->getName();
}
/**
* Get the name of the view.
*
* @return string
*/
public function getName()
{
return $this->view;
}
/**
* Get the array of view data.
*/
public function getData(): array
{
return $this->data;
}
/**
* Get the path to the view file.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* Set the path to the view.
*
* @param string $path
*/
public function setPath($path)
{
$this->path = $path;
}
/**
* Get the view factory instance.
*
* @return Factory
*/
public function getFactory()
{
return $this->factory;
}
/**
* Get the view's rendering engine.
*
* @return EngineInterface
*/
public function getEngine()
{
return $this->engine;
}
/**
* Determine if a piece of data is bound.
*
* @param string $key
* @return bool
*/
public function offsetExists($key)
{
return array_key_exists($key, $this->data);
}
/**
* Get a piece of bound data to the view.
*
* @param string $key
* @return mixed
*/
public function offsetGet($key)
{
return $this->data[$key];
}
/**
* Set a piece of data on the view.
*
* @param string $key
* @param mixed $value
*/
public function offsetSet($key, $value)
{
$this->with($key, $value);
}
/**
* Unset a piece of data from the view.
*
* @param string $key
*/
public function offsetUnset($key)
{
unset($this->data[$key]);
}
/**
* Get a piece of data from the view.
*
* @param string $key
* @return mixed
*/
public function &__get($key)
{
return $this->data[$key];
}
/**
* Get content as a string of HTML.
*
* @return string
*/
public function toHtml()
{
return $this->render();
}
/**
* Get the contents of the view instance.
*
* @return string
*/
protected function renderContents()
{
// We will keep track of the amount of views being rendered so we can flush
// the section after the complete rendering operation is done. This will
// clear out the sections for any separate views that may be rendered.
$this->factory->incrementRender();
$this->factory->callComposer($this);
$contents = $this->getContents();
// Once we've finished rendering the view, we'll decrement the render count
// so that each sections get flushed out next time a view is created and
// no old sections are staying around in the memory of an environment.
$this->factory->decrementRender();
return $contents;
}
/**
* Get the evaluated contents of the view.
*
* @return string
*/
protected function getContents()
{
return $this->engine->get($this->path, $this->gatherData());
}
/**
* Parse the given errors into an appropriate value.
*
* @param array|MessageProvider|string $provider
* @return \Hyperf\Utils\MessageBag|MessageBag
*/
protected function formatErrors($provider)
{
return $provider instanceof MessageProvider
? $provider->getMessageBag()
: new \Hyperf\Utils\MessageBag((array) $provider);
}
}

View File

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ViewEngine;
use Countable;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Contracts\MessageBag;
class ViewErrorBag implements Countable
{
/**
* The array of the view error bags.
*
* @var array
*/
protected $bags = [];
/**
* Dynamically call methods on the default bag.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->getBag('default')->{$method}(...$parameters);
}
/**
* Dynamically access a view error bag.
*
* @param string $key
* @return MessageBag
*/
public function __get($key)
{
return $this->getBag($key);
}
/**
* Dynamically set a view error bag.
*
* @param string $key
* @param MessageBag $value
*/
public function __set($key, $value)
{
$this->put($key, $value);
}
/**
* Convert the default bag to its string representation.
*
* @return string
*/
public function __toString()
{
return (string) $this->getBag('default');
}
/**
* Checks if a named MessageBag exists in the bags.
*
* @param string $key
* @return bool
*/
public function hasBag($key = 'default')
{
return isset($this->bags[$key]);
}
/**
* Get a MessageBag instance from the bags.
*
* @param string $key
* @return \Hyperf\Utils\MessageBag
*/
public function getBag($key)
{
return Arr::get($this->bags, $key) ?: new \Hyperf\Utils\MessageBag();
}
/**
* Get all the bags.
*
* @return array
*/
public function getBags()
{
return $this->bags;
}
/**
* Add a new MessageBag instance to the bags.
*
* @param string $key
* @return $this
*/
public function put($key, MessageBag $bag)
{
$this->bags[$key] = $bag;
return $this;
}
/**
* Determine if the default message bag has any messages.
*
* @return bool
*/
public function any()
{
return $this->count() > 0;
}
/**
* Get the number of messages in the default bag.
*
* @return int
*/
public function count()
{
return $this->getBag('default')->count();
}
}

View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ViewEngine;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Di\Container;
use Hyperf\Di\Definition\DefinitionSource;
use Hyperf\Event\EventDispatcher;
use Hyperf\Event\ListenerProvider;
use Hyperf\Utils\ApplicationContext;
use Hyperf\View\Mode;
use Hyperf\ViewEngine\Compiler\BladeCompiler;
use Hyperf\ViewEngine\Compiler\CompilerInterface;
use Hyperf\ViewEngine\Component\DynamicComponent;
use Hyperf\ViewEngine\ConfigProvider;
use Hyperf\ViewEngine\Contract\FactoryInterface;
use Hyperf\ViewEngine\Contract\FinderInterface;
use Hyperf\ViewEngine\Contract\ViewInterface;
use Hyperf\ViewEngine\Factory\FinderFactory;
use Hyperf\ViewEngine\HyperfViewEngine;
use HyperfTest\ViewEngine\Stub\Alert;
use HyperfTest\ViewEngine\Stub\AlertSlot;
use PHPUnit\Framework\TestCase;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use function Hyperf\ViewEngine\view;
/**
* @internal
* @coversNothing
*/
class BladeTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// create container
$container = new Container(new DefinitionSource(array_merge([
EventDispatcherInterface::class => EventDispatcher::class,
ListenerProviderInterface::class => ListenerProvider::class,
], (new ConfigProvider())()['dependencies'])));
ApplicationContext::setContainer($container);
// register config
$container->set(ConfigInterface::class, new Config([
'view' => [
'engine' => HyperfViewEngine::class,
'mode' => Mode::SYNC,
'config' => [
'view_path' => __DIR__ . '/storage/view/',
'cache_path' => __DIR__ . '/storage/cache/',
],
'components' => [
'alert' => Alert::class,
'alert-slot' => AlertSlot::class,
'dynamic-component' => DynamicComponent::class,
],
'namespaces' => [
'admin_config' => __DIR__ . '/admin',
],
],
]));
// vendor 下的命令空间
if (! file_exists(__DIR__ . '/storage/view/vendor/admin/simple_4.blade.php')) {
@mkdir(__DIR__ . '/storage/view/vendor');
@mkdir(__DIR__ . '/storage/view/vendor/admin_custom');
@mkdir(__DIR__ . '/storage/view/vendor/admin_config');
file_put_contents(__DIR__ . '/storage/view/vendor/admin_custom/simple_4.blade.php', 'from_vendor');
file_put_contents(__DIR__ . '/storage/view/vendor/admin_config/simple_4.blade.php', 'from_vendor');
}
}
public function testRegisterComponents()
{
$this->assertSame('success', trim((string) view('simple_8', ['message' => 'success'])));
$this->assertSame('success', trim((string) view('simple_9', ['message' => 'success'])));
}
public function testRegisterNamespace()
{
$this->assertSame('from_admin', trim((string) view('admin_config::simple_3')));
$this->assertSame('from_vendor', trim((string) view('admin_config::simple_4')));
}
public function testViewFunction()
{
$this->assertInstanceOf(FactoryInterface::class, view());
$this->assertInstanceOf(ViewInterface::class, view('index'));
}
public function testHyperfEngine()
{
$engine = new HyperfViewEngine();
$this->assertSame('<h1>fangx/view</h1>', $engine->render('index', [], []));
$this->assertSame('<h1>fangx</h1>', $engine->render('home', ['user' => 'fangx'], []));
}
public function testRender()
{
$this->assertSame('<h1>fangx/view</h1>', trim((string) view('index')));
$this->assertSame('<h1>fangx</h1>', trim((string) view('home', ['user' => 'fangx'])));
// *.php
$this->assertSame('fangx', trim((string) view('simple_1')));
// *.html
$this->assertSame('fangx', trim((string) view('simple_2')));
// @extends & @yield & @section..@stop
$this->assertSame('yield-content', trim((string) view('simple_5')));
// @if..@else..@endif
$this->assertSame('fangx', trim((string) view('simple_6')));
// @{{ name }}
$this->assertSame('{{ name }}', trim((string) view('simple_7')));
// @json()
$this->assertSame('{"email":"nfangxu@gmail.com","name":"fangx"}', trim((string) view('simple_10')));
}
public function testUseNamespace()
{
$finder = ApplicationContext::getContainer()->get(FinderInterface::class);
$factory = new FinderFactory();
$factory->addNamespace($finder, 'admin_custom', __DIR__ . '/admin');
$this->assertSame('from_admin', trim((string) view('admin_custom::simple_3')));
$this->assertSame('from_vendor', trim((string) view('admin_custom::simple_4')));
}
public function testComponent()
{
/** @var BladeCompiler $compiler */
$compiler = ApplicationContext::getContainer()
->get(CompilerInterface::class);
$compiler->component(Alert::class, 'alert');
$compiler->component(AlertSlot::class, 'alert-slot');
$this->assertSame('success', trim((string) view('simple_8', ['message' => 'success'])));
$this->assertSame('success', trim((string) view('simple_9', ['message' => 'success'])));
}
public function testDynamicComponent()
{
/** @var BladeCompiler $compiler */
$compiler = ApplicationContext::getContainer()
->get(CompilerInterface::class);
$compiler->component(Alert::class, 'alert');
$compiler->component(AlertSlot::class, 'alert-slot');
$this->assertSame('ok', trim((string) view('simple_11', ['componentName' => 'alert', 'message' => 'ok'])));
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ViewEngine\Stub;
use Hyperf\Utils\ApplicationContext;
use Hyperf\ViewEngine\Component\Component;
use Hyperf\ViewEngine\Contract\FactoryInterface;
class Alert extends Component
{
public $message;
public function __construct($message)
{
$this->message = $message;
}
public function render()
{
$factory = ApplicationContext::getContainer()
->get(FactoryInterface::class);
return $factory->make('components.alert');
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ViewEngine\Stub;
use Hyperf\Utils\ApplicationContext;
use Hyperf\ViewEngine\Component\Component;
use Hyperf\ViewEngine\Contract\FactoryInterface;
class AlertSlot extends Component
{
public function render()
{
$factory = ApplicationContext::getContainer()
->get(FactoryInterface::class);
return $factory->make('components.alert-2');
}
}

View File

@ -0,0 +1 @@
from_admin

View File

@ -0,0 +1 @@
from_admin

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1 @@
!./vendor/admin

View File

@ -0,0 +1 @@
{{ $slot }}

View File

@ -0,0 +1 @@
{{ $message ?? 'alert' }}

View File

@ -0,0 +1 @@
<h1>{{ $user }}</h1>

View File

@ -0,0 +1 @@
<h1>fangx/view</h1>

View File

@ -0,0 +1,3 @@
@yield("content")
yield-content

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
echo 'fangx';

View File

@ -0,0 +1 @@
@json(['email' => 'nfangxu@gmail.com', 'name' => 'fangx'])

View File

@ -0,0 +1 @@
<x-dynamic-component :component="$componentName" :message="$message" class="mt-4" />

View File

@ -0,0 +1 @@
fangx

View File

@ -0,0 +1,5 @@
@extends("layout")
@section("content")
@stop

View File

@ -0,0 +1,5 @@
@if(true)
fangx
@else
nfangxu
@endif

View File

@ -0,0 +1 @@
@{{ name }}

View File

@ -0,0 +1 @@
<x-alert :message="$message"/>

View File

@ -0,0 +1,3 @@
<x-alert-slot>
{{ $message }}
</x-alert-slot>

View File

@ -18,5 +18,6 @@ return [
'config' => [
'view_path' => BASE_PATH . '/storage/view/',
'cache_path' => BASE_PATH . '/runtime/view/',
'charset' => 'UTF-8',
],
];

View File

@ -61,7 +61,7 @@ class Render implements RenderInterface
public function render(string $template, array $data = []): ResponseInterface
{
return $this->response()
->withAddedHeader('content-type', 'text/html')
->withAddedHeader('content-type', $this->getContentType())
->withBody(new SwooleStream($this->getContents($template, $data)));
}
@ -87,6 +87,13 @@ class Render implements RenderInterface
}
}
public function getContentType(): string
{
$charset = ! empty($this->config['charset']) ? '; charset=' . $this->config['charset'] : '';
return 'text/html' . $charset;
}
protected function response(): ResponseInterface
{
return Context::get(ResponseInterface::class);

View File

@ -18,4 +18,6 @@ interface RenderInterface
public function render(string $template, array $data = []): ResponseInterface;
public function getContents(string $template, array $data = []): string;
public function getContentType(): string;
}