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: 多个源路径
保持默认简单:应用源代码在应用路径中。只有在代码库确实需要共享时,才添加额外路径。
路径选择应保持可预测:
- 应用源代码在
/var/www/example.nl/ - 运行时在
/opt/phlo/ - 发布在
/var/www/example.nl/release/ - 数据和凭据在
/var/www/example.nl/data/
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 模式下,basic 和 marketing 将 script-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_x 和 captcha_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/social 在 security/OAuth2 的基础上添加了“使用 Google 登录”(以及 Microsoft 和 Apple):它构建授权 URL,并将回调代码转换为经过验证的用户资料,因此您的应用程序只处理用户端。它从 data/creds.ini 中的 [google](或 [microsoft],[apple])部分读取每个提供者的 client_id 和 client_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 替换为 static,prop 替换为 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 class,interface 或 trait |
@ summary: |
一行描述,在手册、反射和 Phlo Control Center 中显示 |
@ package: |
工具的组名 |
@ frontend: / @ backend: |
标记为仅前端或后端的资源 |
@ requires: |
依赖项,在资源启用时解析;name? 是可选的,php-ext: 和 creds: 条目是信息性 |
@ provides: / @ binds: |
提供的前端 API / 连接的选择器;供 reflect::selectorGraph 使用 |
@ tags: |
自由格式标签,在反射索引中显示 |
@ advice: |
开发者指导,在 reflect::objectIndex 中显示 |
12.9: 最佳实践
- 保持入口点明确;避免隐藏配置。
- 让发布输出来自
build::release。 - 切勿在源文件中放置凭据。
- 使用反射来验证资源、路由和函数。
- 仅在多个应用程序确实受益时才添加抽象。