Files
FamilyBook/app/helpers.php
T

167 lines
4.4 KiB
PHP
Raw Normal View History

<?php
/**
* 全局辅助函数(由 bootstrap.php 引入,页面无需重复 require)
*/
declare(strict_types=1);
/** HTML 转义:所有输出到页面的动态值都必须过一道 */
function e($value): string
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
/** 静态资源地址,带版本号(mtime + size)——没版本号的话浏览器会一直用旧缓存 */
function asset(string $rel): string
{
$p = dirname(__DIR__) . $rel;
if (!is_file($p)) {
return $rel;
}
return $rel . '?v=' . substr(md5(filemtime($p) . filesize($p)), 0, 8);
}
/**
* 附件(头像等)地址 —— 全站唯一出口。
* 走这里是纪律:数据库只存相对键,云端/本地的差异由 Storage 自己吸收。
*/
function storage_url(?string $key): string
{
$fid = \App\Auth::familyId();
return \App\Storage\Storage::url((int)$fid, $key);
}
/** 头像地址(无头像自动回退内置剪影) */
function avatar_url(?string $key): string
{
$fid = (int)\App\Auth::familyId();
return \App\Upload::avatarUrl($fid, $key);
}
/** 站内链接(统一走 query-string 路由,不依赖 Apache 重写) */
function url(string $route = 'home', array $query = []): string
{
$q = array_merge(['r' => $route], $query);
return 'index.php?' . http_build_query($q);
}
function redirect(string $route = 'home', array $query = []): void
{
header('Location: ' . url($route, $query));
exit;
}
function is_post(): bool
{
return ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
}
/**
* 读配置。
*
* ⚠️ `?string $key = null` 里的 `?` 不能省:PHP 8.4 起,
* 「有类型 + 默认 null」而没写 `?` 的形式(隐式可空)会报 Deprecated。
* 这个警告一旦在页面顶部打出来,就等于响应体已经输出 ——
* 后面所有 header() 全部失效(典型症状:安装完/登录后**不再跳转**,
* 返回 200 而不是 302,但功能其实是好的,极难排查)。
* 线上跑 PHP 7.4/8.1 时不报,所以本地用新 PHP 才抓得到。
*/
function fb_config(?string $key = null, $default = null)
{
$c = $GLOBALS['config'];
if ($key === null) {
return $c;
}
return array_key_exists($key, $c) ? $c[$key] : $default;
}
// ---------------- CSRF ----------------
function csrf_token(): string
{
if (empty($_SESSION['_csrf'])) {
$_SESSION['_csrf'] = bin2hex(random_bytes(16));
}
return $_SESSION['_csrf'];
}
function csrf_field(): string
{
return '<input type="hidden" name="_csrf" value="' . e(csrf_token()) . '">';
}
/**
* 校验 CSRF。失败直接结束请求 —— 不做这种假返回:
* 「返回 false 让调用层自己处理」的写法,迟早有人忘了判断。
*/
function csrf_check(): void
{
$ok = isset($_POST['_csrf'], $_SESSION['_csrf'])
&& hash_equals($_SESSION['_csrf'], (string)$_POST['_csrf']);
if (!$ok) {
http_response_code(419);
echo '页面已过期,请刷新后重试(CSRF 校验失败)';
exit;
}
}
// ---------------- 一次性提示 ----------------
function flash(string $type, string $message): void
{
$_SESSION['_flash'][] = ['type' => $type, 'message' => $message];
}
function take_flash(): array
{
$f = $_SESSION['_flash'] ?? [];
unset($_SESSION['_flash']);
return $f;
}
/** 表单回填(校验失败时保留用户输入) */
function old(string $key, $default = '')
{
return $_SESSION['_old'][$key] ?? $default;
}
function set_old(array $data): void
{
$_SESSION['_old'] = $data;
}
function clear_old(): void
{
unset($_SESSION['_old']);
}
/** 生日相关:返回年龄与距下次生日天数 */
function age_from(string $birthday): ?int
{
if ($birthday === '' || $birthday === null) {
return null;
}
try {
$b = new DateTimeImmutable($birthday);
} catch (Exception $ex) {
return null;
}
return (int)$b->diff(new DateTimeImmutable('today'))->y;
}
function days_until_birthday(string $birthday): ?int
{
if (!$birthday) {
return null;
}
try {
$b = new DateTimeImmutable($birthday);
$now = new DateTimeImmutable('today');
$next = new DateTimeImmutable($now->format('Y') . '-' . $b->format('m-d'));
if ($next < $now) {
$next = $next->modify('+1 year');
}
return (int)$now->diff($next)->days;
} catch (Exception $ex) {
return null;
}
}