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: Worker mode

In the classic request lifecycle, request state gets a fresh boundary for every request. With thread: true in phlo_app(...), the runtime stays in memory between requests in a FrankenPHP worker.

The performance gain is large (no boot per request), but three rules apply:

1. No die() or exit() in the HTTP path. Both kill the entire worker, not just the current request. view(), apply() and location() prepare or render a response but do not terminate PHP execution, so return them from a route guard or otherwise let the routine end immediately afterwards.

2. No request state in static properties. PHP static properties and function-local statics survive between requests. Phlo clears its computed-static cache (obj::$classProps) during the worker reset, but session, user, payload, time and DB state still belong on an instance with an explicit lifetime or on %req.

3. Mark long-lived objects with $objPers = true. By default Phlo clears its instance map between requests. For objects you explicitly want to reuse (DB connection, prepared statements), set $this->objPers = true so the cleanup leaves them alone.

The database connection is the usual example, and it is transient by default: %MySQL does not keep its PDO across requests, so an idle connection the database has dropped (the low-traffic "server has gone away") can never be reused stale. Opt into one persistent connection with prop %MySQL.objPers = true from app.phlo; query() reconnects and retries once on a dropped connection, so that opt-in stays safe.

Combining with build: true is not allowed: build writes files between requests, and in a worker that is a race condition. Phlo throws a runtime error if you enable both.

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: File metadata: the complete @ reference

Every .phlo file can open with @ key: value lines. Any key is stored as file metadata; these have engine or tooling meaning:

Key Effect
@ class: Override the PHP class name
@ extends: PHP inheritance (default: obj)
@ implements: PHP interfaces, comma-separated
@ use: PHP use statement (Full\Name as Alias)
@ namespace: PHP namespace
@ type: class (default), abstract class, interface or trait
@ summary: One-line description, shown in the manual, reflection and the Phlo Control Center
@ package: Group name for tooling
@ frontend: / @ backend: Marks a resource frontend- or backend-only
@ requires: Dependencies, resolved when the resource is enabled; name? is optional, php-ext: and creds: entries are informational
@ provides: / @ binds: Frontend APIs offered / selectors hooked; feeds reflect::selectorGraph
@ tags: Free-form labels, shown in reflection indexes and matched by the manual search
@ advice: Developer guidance, shown on the object card, in reflect::objectIndex and in the Phlo Control Center

12.8.1 Writing metadata a reader can use

@ summary and @ advice are both expected on a resource. They answer different questions and neither replaces the other. The summary says what it is, in one sentence: someone scanning an index decides from that line whether to open the resource at all. The advice says how to use it well: what to configure, what people get wrong, what the resource deliberately does not do. Write the sentence you would say to someone about to use it for the first time.

On a node inside the file, prefer a type over a comment. A type is checked by PHP, shows up in the manual signature and in reflection without any work, and cannot fall out of date:

static day($file):?string => ...

Three things to know before adding one. There is no declare(strict_types=1), so scalars are coerced and a declared type only bites on null and on values that cannot convert. :void cannot go on an arrow node, because => compiles to a return. And do not type a configuration property that an app redeclares in a subclass, such as static table or static objCache: a typed property must keep its type in every child, which turns a one-line model into a migration.

Write a node comment only for what a type cannot say: a reason, an ordering constraint, a trap. The comment goes on the line before the node, and its first line has to be a complete sentence, because reflection uses that line on its own as the summary. A first line that runs on into the next is served everywhere as half a thought.

12.9: 最佳实践

最近更新于 2026年8月23日

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