tobento/app-view

应用视图支持。

1.0.7 2024-08-13 15:37 UTC

This package is auto-updated.

Last update: 2024-09-13 15:54:49 UTC


README

应用视图支持创建菜单、表单等,以创建任何类型的Web应用程序。它包含一个默认布局,使用Basis Css,你可以使用或不使用。虽然某些应用包可能依赖于它。

目录

入门指南

运行此命令以添加运行中的应用视图项目的最新版本。

composer require tobento/app-view

要求

  • PHP 8.0 或更高版本

文档

应用

如果你使用的是骨架,请查看 应用骨架

你也可以查看 应用 以了解有关应用程序的一般信息。

视图启动

视图启动执行以下操作:

  • 迁移默认布局的视图和CSS资产
  • 实现ViewInterface
  • 如果可用,则添加来自不同服务的全局视图数据
  • 添加多个视图宏
use Tobento\App\AppFactory;

// Create the app
$app = (new AppFactory())->createApp();

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'vendor', 'vendor')
    ->dir($app->dir('app').'views', 'views', group: 'views')
    ->dir($app->dir('root').'public', 'public');
    
// Adding boots
$app->boot(\Tobento\App\View\Boot\View::class);

// Run the app
$app->run();

渲染视图

你可以以几种方式渲染视图

使用应用程序

use Tobento\App\AppFactory;
use Tobento\Service\View\ViewInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'vendor', 'vendor')
    ->dir($app->dir('app').'views', 'views', group: 'views')
    ->dir($app->dir('root').'public', 'public');
    
// Adding boots
$app->boot(\Tobento\App\View\Boot\View::class);
$app->booting();

$view = $app->get(ViewInterface::class);
$content = $view->render(view: 'about', data: []);

// or using the app macro:
$content = $app->renderView(view: 'about', data: []);

// Run the app
$app->run();

使用自动注入

你还可以在任何由应用程序解析的类中请求 ViewInterface::class

use Tobento\Service\View\ViewInterface;

class SomeService
{
    public function __construct(
        protected ViewInterface $view,
    ) {}
}

使用视图启动

use Tobento\App\Boot;
use Tobento\App\View\Boot\View;

class AnyServiceBoot extends Boot
{
    public const BOOT = [
        // you may ensure the view boot.
        View::class,
    ];
    
    public function boot(View $view)
    {
        $content = $view->render(view: 'about', data: []);
    }
}

查看 视图服务 以了解更多信息。

使用响应器

如果你已启动 App Http - 请求者和响应者 启动,则可以使用 render 方法

use Tobento\Service\Responser\ResponserInterface;
use Psr\Http\Message\ResponseInterface;

class SomeHttpController
{
    public function index(ResponserInterface $responser): ResponseInterface
    {
        return $responser->render(view: 'register', data: []);
    }
}

全局视图数据和变量

它添加了以下可在视图文件中访问的全局视图数据和变量

// by variable:
$htmlLang
$locale
$routeName

// by view get method:
$htmlLang = $view->get('htmlLang', 'en');
$locale = $view->get('locale', 'en');
$routeName = $view->get('routeName', '');

$htmlLang / $locale

视图启动的代码片段

use Tobento\Service\Language\LanguagesInterface;

if ($this->app->has(LanguagesInterface::class)) {
    $locale = $this->app->get(LanguagesInterface::class)->current()->locale();
    $view->with('htmlLang', str_replace('_', '-', $locale));
    $view->with('locale', $locale);
}

$routeName

视图启动的代码片段

use Tobento\Service\Routing\RouterInterface;

if ($this->app->has(RouterInterface::class)) {
    $matchedRoute = $this->app->get(RouterInterface::class)->getMatchedRoute();
    $view->with('routeName', $matchedRoute?->getName() ?: '');
}

添加全局数据和变量

你可以通过使用启动添加更多全局数据

use Tobento\App\Boot;
use Tobento\Service\View\ViewInterface;

class SomeGlobalViewDataBoot extends Boot
{
    public function boot()
    {
        // only add if view is requested:
        $this->app->on(ViewInterface::class, function(ViewInterface $view) {
            
            // using the with method:
            $view->with(name: 'someVariable', value: 'someValue');
            
            // using the data method:
            $view->data([
                'someVariable' => 'someValue',
            ]);
        });
    }
}

可用的视图宏

app

返回应用程序实例。

use Tobento\App\AppInterface;

var_dump($view->app() instanceof AppInterface);
// bool(true)

assetPath

assetPath 函数返回应用程序资产目录的完全限定路径。

var_dump($view->assetPath('assets/editor/script.js'));
// string(33) "/basepath/assets/editor/script.js"

menu

返回指定名称的菜单。

use Tobento\Service\Menu\MenuInterface;

var_dump($view->menu('main') instanceof MenuInterface);
// bool(true)

查看 菜单服务 以了解更多信息。

trans / etrans

如果应用程序中可用,则返回翻译器翻译的消息。

默认情况下,翻译器不可用,你可能需要安装 App Translation 包来这样做。

$translated = $view->trans(
    message: 'Hi :name',
    parameters: [':name' => 'John'],
    locale: 'de',
);

// The etrans method will escape the translated message
// with htmlspecialchars.

echo $view->etrans(
    message: 'Hi :name',
    parameters: [':name' => 'John'],
    locale: 'de',
);

routeUrl

如果应用程序中可用 Tobento\Service\Routing\RouterInterface,则返回指定路由的URL,这是在启动了 App Http - 路由启动 时的情况。

use Tobento\Service\Routing\UrlInterface;

$url = $view->routeUrl(
    name: 'route.name',
    parameters: [],
);

var_dump($url instanceof UrlInterface);
// bool(true)

tagAttributes

返回指定标签名称的属性。

use Tobento\Service\Tag\AttributesInterface;

var_dump($view->tagAttributes('body') instanceof AttributesInterface);
// bool(true)

查看标签服务 - 属性接口以了解更多相关信息。

日期 / dateTime / formatDate

您可以使用datedateTimeformatDate方法,通过日期格式化器来格式化和显示任何日期。

// date:
var_dump($view->date('2024-02-15 10:15'));
// string(27) "Thursday, 15. February 2024"

var_dump($view->date(
    value: 'now',
    format: 'EE, dd. MMMM yyyy',
    locale: 'de_DE',
));
// string(19) "Sa., 23. März 2024"

// dateTime:
var_dump($view->dateTime('now'));
// string(31) "Saturday, 23. March 2024, 07:36"

var_dump($view->dateTime(
    value: 'now',
    format: 'EE, dd. MMMM yyyy, HH:mm',
    locale: 'de_DE',
));
// string(26) "Sa., 23. März 2024, 07:45"

// formatDate:
var_dump($view->formatDate(
    value: '2024-02-15 10:15',
    format: 'd.m.Y H:i',
));
// string(16) "15.02.2024 10:15"

菜单启动

菜单引导程序执行以下操作

use Tobento\App\AppFactory;
use Tobento\Service\Menu\MenusInterface;
use Tobento\Service\Menu\MenuInterface;
use Tobento\Service\View\ViewInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'vendor', 'vendor')
    ->dir($app->dir('app').'views', 'views', group: 'views')
    ->dir($app->dir('root').'public', 'public');
    
// Adding boots
$app->boot(\Tobento\App\View\Boot\View::class);
// no need to boot as already loaded by the view boot:
// $app->boot(\Tobento\App\View\Boot\Menus::class);
$app->booting();

// Get the menus:
$menus = $app->get(MenusInterface::class);

// View menu macro:
$view = $app->get(ViewInterface::class);

var_dump($view->menu('main') instanceof MenuInterface);
// bool(true)

// Run the app
$app->run();

查看 菜单服务 以了解更多信息。

使用菜单引导程序

您可以使用菜单引导程序添加菜单项

use Tobento\App\Boot;
use Tobento\App\View\Boot\Menus;

class AnyServiceBoot extends Boot
{
    public const BOOT = [
        // you may ensure the menus boot.
        Menus::class,
    ];
    
    public function boot(Menus $menus)
    {
        $menu = $menus->menu('main');
        $menu->link('https://example.com/foo', 'Foo')->id('foo');
    }
}

使用app方法

只有当从应用程序请求菜单时,您才能添加菜单项

use Tobento\App\Boot;
use Tobento\Service\Menu\MenusInterface;

class AnyServiceBoot extends Boot
{
    public function boot()
    {
        $this->app->on(MenusInterface::class, function(MenusInterface $menus) {
            $menu = $menus->menu('main');
            $menu->link('https://example.com/foo', 'Foo')->id('foo');
        });
    }
}

表单启动

表单引导程序执行以下操作

此引导程序需要app-http

composer require tobento/app-http
use Tobento\App\AppFactory;
use Tobento\Service\Form\FormFactoryInterface;
use Tobento\Service\Form\Form;
use Tobento\Service\View\ViewInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'vendor', 'vendor')
    ->dir($app->dir('app').'views', 'views', group: 'views')
    ->dir($app->dir('root').'public', 'public');
    
// Adding boots
$app->boot(\Tobento\App\View\Boot\View::class);
$app->boot(\Tobento\App\View\Boot\Form::class);
$app->booting();

// Get the form factory:
$formFactory = $app->get(FormFactoryInterface::class);

// View form macro:
$view = $app->get(ViewInterface::class);

$form = $view->form();
// var_dump($form instanceof Form);
// bool(true)

// Run the app
$app->run();

查看表单服务以了解更多相关信息。

您可能还需要引导App Http - 错误处理器引导程序,它已经处理了由表单引起的异常。

CSRF保护

CSRF保护已实现,并且CSRF令牌将由已注册的VerifyCsrfToken中间件进行验证。

CSRF令牌

表单引导程序将CSRF令牌添加到全局视图数据,该数据可在您的视图文件中访问

$token = $view->data()->get('csrfToken');

X-CSRF-TOKEN

除了检查作为POST参数的CSRF令牌之外,VerifyCsrfToken中间件还将检查X-CSRF-TOKEN请求头。

当使用默认布局时,在视图文件inc/head中,令牌将存储在名为csrf-token的HTML元标签中

<meta name="csrf-token" content="token-string">

您可以使用此元标签在设置X-CSRF-TOKEN头时获取令牌

fetch("https://example.com", {
    method: "POST",
    headers: {
        "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute('content')
    },
    body: JSON.stringify(data)
});

表单消息

由于Tobento\Service\Form\ResponserFormFactory::class是默认的Tobento\Service\Form\FormFactoryInterface::class实现,您可以通过以下方式将消息传递给表单字段

use Tobento\Service\Responser\ResponserInterface;
use Tobento\Service\Requester\RequesterInterface;
use Psr\Http\Message\ResponseInterface;

class RegisterController
{
    public function index(ResponserInterface $responser): ResponseInterface
    {        
        // set the key corresponding to your form field name:
        $responser->messages()->add('info', 'Some info message', key: 'firstname');

        return $responser->render(view: 'register');
    }
    
    public function register(
        RequesterInterface $requester,
        ResponserInterface $responser,
    ): ResponseInterface {
        // validate request data:
        //$requester->input();
        
        // add message on error:
        $responser->messages()->add('error', 'Message error', key: 'firstname');
        
        // redirect - messages and input data will be flashed:
        return $responser->redirect(
            uri: 'uri',
        )->withInput($requester->input()->all());
    }    
}

在您的视图文件中

<?php $form = $view->form(); ?>
<?= $form->form() ?>
<?= $form->input(
    type: 'text',
    name: 'firstname',
) ?>
<?= $form->button('Register') ?>
<?= $form->close() ?>

消息启动

// ...
$app->boot(\Tobento\App\View\Boot\Messages::class);
// ...

消息引导程序执行以下操作

如果传入的视图消息是Tobento\Service\Message\MessagesInterface的实例,则渲染视图消息

use Tobento\Service\Message\MessagesInterface;
use Tobento\Service\Message\Messages;

class SomeHttpController
{
    public function index(ViewInterface $view): string
    {
        $messages = new Messages();
        $messages->add(level: 'error', message: 'Error message');

        return $view->render(
            view: 'register',
            data: [
                'messages' => $messages,
            ],
        );
    }
}

查看消息服务以了解更多相关信息。

响应器消息

如果已引导App Http - 请求者与响应者引导程序,则渲染响应器中的消息

use Tobento\Service\Responser\ResponserInterface;
use Psr\Http\Message\ResponseInterface;

class SomeHttpController
{
    public function index(ResponserInterface $responser): ResponseInterface
    {
        $responser->messages()->add('info', 'Message info');
        $responser->messages()->add('success', 'Message success');
        $responser->messages()->add('error', 'Message error');
        $responser->messages()->add('warning', 'Message warning');
        
        // if a key is specified, the message will not be rendered,
        // as it may belong to a form field.
        $responser->messages()->add('error', 'Message error', key: 'firstname');

        return $responser->render(view: 'register');
    }
}

视图文件

在您的视图文件中,渲染消息视图

<?= $view->render('inc.messages') ?>

面包屑启动

// ...
$app->boot(\Tobento\App\View\Boot\Breadcrumb::class);
// ...

面包屑引导程序执行以下操作

添加面包屑视图并创建基于主菜单的面包屑菜单,使用全局视图数据中的routeName确定活动菜单树。

视图文件

在您的视图文件中,渲染面包屑视图

<?= $view->render('inc.breadcrumb') ?>

表格启动

表格引导程序执行以下操作

  • 将table.css添加到您指定的公共CSS目录public/assets/css/table.css
  • 添加表格视图宏
// ...
$app->boot(\Tobento\App\View\Boot\Table::class);
// ...

视图文件

<?php
$table = $view->table(name: 'demo');
$table->row([
    'title' => 'Title',
    'description' => 'Description',
])->heading();

$table->row([
    'title' => 'Lorem',
    'description' => 'Lorem ipsum',
]);
?>
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        
        <title>Demo Table</title>
        
        <?= $view->assets()->render() ?>
        
        <?php
        // add table.css
        $view->asset('assets/css/table.css');
        ?>
    </head>
    <body>
        <?= $table ?>
    </body>
</html>

查看表格服务以了解更多相关信息。

默认布局

视图

默认布局使用 public/assets/css/app.cssBasis Css public/assets/css/basis.css 来样式化视图文件。

使用视图启动器的一个视图文件可能看起来像

<!DOCTYPE html>
<html lang="<?= $view->esc($view->get('htmlLang', 'en')) ?>">
	
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        
        <title>Demo View</title>
        
        <?= $view->render('inc/head') ?>
        <?= $view->assets()->render() ?>
    </head>
    
    <body<?= $view->tagAttributes('body')->add('class', 'page')->render() ?>>

        <?= $view->render('inc/header') ?>
        <?= $view->render('inc/nav') ?>

        <main class="page-main">

            <?= $view->render('inc.breadcrumb') ?>
            <?= $view->render('inc.messages') ?>

            <h1 class="text-xl">Demo View</h1>

        </main>

        <?= $view->render('inc/footer') ?>
    </body>
</html>

不建议修改现有的视图和资源,因为任何应用程序更新都可能覆盖这些文件。相反,创建一个 主题

异常视图

您的视图目录中提供以下异常视图

  • exception/error.php
  • exception/error.xml.php

您可以安装并启动 App Http - 错误处理器启动,该启动器将使用这些视图来渲染 html 和 xml 异常。

使用响应器渲染异常视图的示例

如果你已启动 App Http - 请求者和响应者 启动,则可以使用 render 方法

use Tobento\Service\Responser\ResponserInterface;
use Psr\Http\Message\ResponseInterface;

class SomeHttpController
{
    public function index(ResponserInterface $responser): ResponseInterface
    {
        return $responser->render(
            view: 'exception/error',
            data: [
                'code' => '403',
                'message' => 'Forbidden',
            ],
            code: 403,
        );
    }
}

主题

主题视图

您可以创建一个 "主题" 来自定义现有的视图和资源,否则任何应用程序更新都可能覆盖这些文件。

首先,创建一个主题启动器

use Tobento\App\Boot;

class SomeThemeBoot extends Boot
{
    public function boot()
    {
        // add a new view directory to load views from
        // with a higher priority as the default.

        $this->app->dirs()->dir(
            dir: $this->app->dir('app').'/theme/',
            name: 'theme',
            group: 'views',
            priority: 500, // default is 100
        );
    }
}

接下来,只需将您想要自定义的视图文件放置在指定的目录中。如果视图文件不存在,它将使用默认视图文件。

主题资产

您可以通过以下方式处理您自定义的资源

替换资源

在您自定义的视图中,您可以直接用您自定义的资源替换资源

$view->asset('assets/css/my-app.css');

使用资源处理器

您可以创建一个资源处理器来压缩、合并或替换资源。

use Tobento\App\Boot;
use Tobento\Service\View\ViewInterface;
use Tobento\Service\View\AssetsHandlerInterface;

class SomeThemeBoot extends Boot
{
    public function boot()
    {
        $this->app->on(ViewInterface::class, function(ViewInterface $view) {
            
            $view->assets()->setAssetsHandler(
                assetsHandler: $assetsHandler, // AssetsHandlerInterface
            );
        });
    }
}

一个默认的资源处理器正在开发中,用于压缩、合并或替换资源!

致谢