12: 高级

Phlo 故意保持模块化。您可以保持应用程序小巧,仅激活所需的资源,或组合多个源路径和资源组。

12.1: 应用程序代码和运行时资源

应用特定的代码应放在应用程序内部。不要将其放在 /opt/phlo/resources/ 中。

/opt/phlo/resources/ 是 Phlo 运行时目录:框架范围内的资源,可以在多个应用之间共享,并且与运行时一起有意维护。只有通用、稳定的代码才能放在那里。

如果您想在应用之间共享代码,请首先创建一个明确的共享模块或应用库路径,并指定明确的所有者。只有在代码确实是框架功能时,才可以将其提升到 Phlo 运行时目录。

运行时资源可以提供对象、函数、样式或脚本。文件顶部的元数据有助于 Phlo Control Center 和手册:

@ summary: Send app notifications
@ package: notifications
@ frontend: false
@ backend: true

method send($message){
	return HTTP(%creds->notify->url, POST: ['message' => $message])
}

12.2: 多个源路径

保持默认简单:应用源代码在应用路径中。只有在代码库确实需要共享时,才添加额外路径。

路径选择应保持可预测:

12.3: 与现有的 PHP 集成

通过加载运行时,将 Phlo 与现有的 PHP 一起使用,并使 Phlo 入口点仅负责应用处理的 routes。现有的静态文件仍然由 webserver 直接提供服务。

<?php
require('/opt/phlo/phlo.php');
phlo_app (
	id: 'Legacy',
	host: 'dev.legacy.test',
	build: true,
	debug: true,
	app: '/var/www/legacy/',
);

12.4: 安全与访客

对于公共网站,通常的基准是:

{
    "resources": [
        "cookies",
        "security/security",
        "security/token",
        "session",
        "useragent",
        "visitors",
        "phlo.async",
        "DOM/form"
    ]
}

对于本地开发,您可以排除跟踪:

{
    "exclude": [
        "visitors",
        "useragent"
    ]
}

visitors 资源跟踪参与度,而不仅仅是点击量。一个小的 heartbeat 脚本在标签页实际可见时累积 active time(一个单调时钟,在 hidden 状态下暂停,在卸载时通过 keepalive 请求刷新)到 active_seconds,并在 visitor_pages 表中记录每页的行,以便您可以看到每页的停留时间,而不仅仅是着陆 URL。它开箱即用地支持用户同意(请参见下面的 cookiewall):在获得同意的情况下,它存储 IP 以及完整的浏览器、操作系统和设备;如果没有同意,访客则通过每日哈希键入,没有 IP 和哈希浏览器字符串。机器人会被跳过。

12.4.1 CSP 模式

security/security 设置基线响应头(Referrer-Policy、nosniff、frame 和跨源策略)以及内容安全策略。选择与应用表面匹配的策略并调用其方法:

模式 策略 用于
%security->strict() 基于 nonce:仅允许 'nonce-...' 脚本和样式,以及 Cache-Control: no-store 具有 XSS 表面的应用(用户内容、表单)
%security->basic() 允许 'self' 脚本和内联样式 静态或受信任内容网站
%security->marketing() 类似于 basic,加上 img-src https: 拉取远程图像的公共页面
%security->api() default-src 'none' 仅 JSON 端点

debug 模式下,basicmarketingscript-src 放宽为 'unsafe-inline',以便内联调试控制台运行;strict 始终使用 nonce,因此即使在调试开启时,XSS 表面应用也保持锁定。在 strict 模式下,通过 %app->nonce 在您的 <script>/<style> 标签中渲染每个请求的 nonce。

12.5: Cookiewall: GDPR 同意

DOM/cookiewall 是一个内置的、微妙的同意横幅。通过以下 3 个步骤激活它:

1. 在 data/app.json 中添加资源

{ "resources": [..., "DOM/cookiewall"] }

2. 在您的布局中添加横幅:

view layout:
<body>
	{{ %cookiewall->banner }}
	<main>...</main>
</body>

横幅仅在访客尚未做出选择时出现。两个按钮:“仅限基本”和“接受”。选择存储在一个名为 cookieChoice 的 cookie 中('essential''all'),有效期为 1 年。

3. 跟踪会自动遵循选择:内置的 visitors 资源无需保护和分析脚本。它的 heartbeat 包含选择,服务器仅在“接受”后存储 IP、浏览器、操作系统和设备;在未获得同意的情况下,访客仍然会被计算在内,以匿名方式(每日哈希,无 IP)。

方法 返回值
%cookiewall->hasChosen() 一旦访客做出选择,返回 true
%cookiewall->canTrack 仅在选择为 'all' 时返回 true
%cookiewall->canAnalytics canTrack 的别名,在分析桥接中语义上有用
%cookiewall->choice 'essential' / 'all' / null

语言:默认使用英语,并且一旦加载语言系统(en()),它会自动翻译,因此多语言应用无需额外设置。重写 prop labels 以设置固定语言的文本,或 prop translate 以强制开启或关闭翻译。

12.5.1: 验证码

security/captcha 是一个自包含的滑块拼图验证码:没有外部服务和第三方脚本。服务器选择一个秘密的缺口位置,使用 GD 渲染背景和松动的拼图块,访客将拼图块拖动到合适的位置;缺口位置始终保留在服务器上。它需要 GD 扩展。

使用 %captcha->widget 在您的表单中渲染小部件:

<form.async method=post action="/signup">
	...
	{{ %captcha->widget }}
	<button>Sign up</button>
</form>

该小部件包含两个隐藏字段,captcha_xcaptcha_t,捆绑的脚本会用放置位置和拖动遥测填充这两个字段。在提交时,读取这两个字段并控制操作:

if (!captcha::verify($x, $telemetry)){
	return apply(errors: ['captcha' => 'Drag the slider to continue'])
}
captcha::consume()

verify($x, $telemetry) 检查掉落位置是否与服务器的密钥匹配,并拒绝非人类的拖动(过快、样本太少、路径变化不足)。它不会清除挑战,因此仅在成功检查后调用 consume()

12.5.2: 社交登录

security/socialsecurity/OAuth2 的基础上添加了“使用 Google 登录”(以及 Microsoft 和 Apple):它构建授权 URL,并将回调代码转换为经过验证的用户资料,因此您的应用程序只处理用户端。它从 data/creds.ini 中的 [google](或 [microsoft][apple])部分读取每个提供者的 client_idclient_secret;没有凭据的提供者将无法使用。

登录由两个路由组成。第一个将访客发送到提供者,并在会话中保留一次性 state

route GET auth google {
	%session->social_state = $state = bin2hex(random_bytes(16))
	return location(social::authUrl('google', $state))
}

第二个是您与提供者注册的重定向 URI,/auth/google/callback。检查 state,交换代码,您将获得一个经过标准化和签名验证的个人资料:

route GET auth google callback {
	if (!hash_equals((string)%session->social_state, (string)%req->query['state'])) return location('/login')
	$profile = social::profile('google', (string)%req->query['code'])
	if (!$profile || !$profile['verified']) return location('/login')
	// $profile: provider, uid, email, name, verified
}

profile() 验证 id_token 签名是否与提供者的 JWKS 匹配,在信任任何声明之前,并检查受众、过期时间和发行者。将账户键入 provider 加上 uid(稳定的主题),并将电子邮件视为声明:仅在提供者实际证明了对其的所有权时,才通过电子邮件采用现有的本地账户。该资源对用户、会话或 routes 没有意见;这些由你来决定。

12.6: 工作模式

默认情况下,Phlo 每个请求运行一次:PHP 进程启动,处理请求,进程结束。在 phlo_app(...) 中使用 thread: true 时,运行时在请求之间保持在内存中,旨在与 FrankenPHP、ReactPHP 或 RoadRunner 一起使用。

性能提升非常显著(每个请求不需要启动),但有三条规则适用:

1. 在 HTTP 路径中不要使用 die()exit() 两者都会终止整个 worker,而不仅仅是当前请求。使用 return 或让终止调用(view()apply()location())发送响应。

2. 静态属性中不要存储请求状态。 静态属性在请求之间存活。请求 A 的数据会泄漏到请求 B。静态属性仅适用于类结构或对所有请求都相同的计算元数据,而不适用于会话、用户、负载、时间或数据库状态。

3. 用 $objPers = true 标记长生命周期对象。 默认情况下,Phlo 在请求之间清除其实例映射。对于您明确希望重用的对象(数据库连接、预处理语句),设置 $this->objPers = true,以便清理时不干扰它们。

数据库连接是一个常见的例子,默认情况下是临时的:%MySQL 不会在请求之间保持其 PDO,因此数据库丢弃的空闲连接(低流量的“服务器已离开”)永远无法重用。通过 app.phlo 中的 prop %MySQL.objPers = true 选择一个持久连接;query() 在连接丢失时会重新连接并重试一次,因此这种选择是安全的。

build: true 结合使用是不允许的:构建在请求之间写入文件,而在 worker 中这会导致竞争条件。如果您同时启用两者,Phlo 会抛出运行时错误。

12.7: 在不分叉的情况下修改资源

有时您希望共享资源在一个应用中表现得稍微不同,而不必复制或更改该资源。在任何 .phlo 文件中,您可以通过将节点命名为 %<class>.<node> 来注入或覆盖 不同 类中的节点:

static %visitors.table = 'control.visitors'
prop %visitors.db = 'control'
method %model.greet => 'hi'

第一行覆盖了 visitors 模型的 static $table;第二行为 visitors 添加了一个 db prop;第三行为 model 添加了一个 greet method。在构建过程中,转译器会去掉 %<class>. 前缀,并将节点写入 <class>:具有该名称的现有节点会被 覆盖,一个新节点会被添加。目标类必须是构建的一部分(其资源已加载),否则修饰符会被静默忽略。保持节点类型与您替换的类型相同(static 替换为 staticprop 替换为 prop):整个节点会被交换。

一个实际的例子:让共享的 visitors 模型写入一个中央分析数据库,而应用中的所有其他查询则保持在应用自己的连接上:

static %visitors.table = 'control.visitors'

这使得共享资源保持中立,同时每个应用程序都给它自己的解释。

12.8: 文件元数据:完整的 @ 引用

每个 .phlo 文件可以以 @ key: value 行开头。任何键都作为文件元数据存储;这些具有引擎或工具的含义:

效果
@ class: 重写 PHP 类名
@ extends: PHP 继承(默认:obj
@ implements: PHP 接口,以逗号分隔
@ use: PHP 使用语句(Full\Name as Alias
@ namespace: PHP 命名空间
@ type: class(默认),abstract classinterfacetrait
@ summary: 一行描述,在手册、反射和 Phlo Control Center 中显示
@ package: 工具的组名
@ frontend: / @ backend: 标记为仅前端或后端的资源
@ requires: 依赖项,在资源启用时解析;name? 是可选的,php-ext:creds: 条目是信息性
@ provides: / @ binds: 提供的前端 API / 连接的选择器;供 reflect::selectorGraph 使用
@ tags: 自由格式标签,在反射索引中显示
@ advice: 开发者指导,在 reflect::objectIndex 中显示

12.9: 最佳实践

我们使用必要的cookie来使该网站正常工作。在您的许可下,我们还使用分析工具来改善网站。