You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

259 lines
7.5 KiB

2 months ago
<?php
namespace App\Support;
use App\Models\Application;
use App\Models\Competition;
/**
* 报名表 schema 字段与 applications 表列对齐。
* 标量列名单从 Application::$fillable 自动派生;新增数据库列后只需加入 fillable 即可全链路生效。
*/
final class SignupApplicationFieldRegistry
{
public const COMMITMENT_KEY = 'commitment_accepted';
public const PROMISE_SIGNATURE_KEY = 'promise_signature';
/** @var list<string> */
public const FILE_FIELD_KEYS = ['plan', 'supporting'];
/** 无专属类型约束时,标量字段允许的 schema type */
private const GENERIC_SCALAR_TYPES = [
'text',
'textarea',
'email',
'tel',
'select',
'number',
'date',
];
/**
* 可选:为常用字段收窄允许的 schema type;未列出的 fillable 列使用 GENERIC_SCALAR_TYPES。
*
* @var array<string, list<string>>
*/
private const SCALAR_TYPE_HINTS = [
'player_name' => ['text'],
'school' => ['text'],
'degree' => ['select'],
'contact_email' => ['email', 'text'],
'contact_mobile' => ['tel', 'text'],
'company_name' => ['text'],
'project_name' => ['text'],
'track' => ['select'],
'location_country' => ['select'],
'location_province' => ['select', 'text'],
'location_city' => ['select', 'text'],
'oversea_country' => ['text'],
1 month ago
'event_location' => ['select', 'text'],
'team_members' => ['text', 'textarea'],
2 months ago
'intro' => ['textarea', 'text'],
'recommend' => ['text', 'textarea'],
];
/**
* @return list<string>
*/
public static function scalarColumnKeys(): array
{
return Application::signupScalarFillableKeys();
}
/**
* @return list<string>
*/
public static function allSupportedFormKeys(): array
{
return array_values(array_unique([
...self::scalarColumnKeys(),
...self::FILE_FIELD_KEYS,
self::COMMITMENT_KEY,
]));
}
/**
* 供管理端报名表编辑器拉取:与后端 fillable 实时同步。
*
* @return array{
* scalar_field_keys: list<string>,
* file_field_keys: list<string>,
* commitment_field_key: string,
* scalar_type_hints: array<string, list<string>>,
* generic_scalar_types: list<string>,
* all_supported_form_keys: list<string>
* }
*/
public static function manifest(): array
{
$hints = [];
foreach (self::scalarColumnKeys() as $key) {
if (isset(self::SCALAR_TYPE_HINTS[$key])) {
$hints[$key] = self::SCALAR_TYPE_HINTS[$key];
}
}
return [
'scalar_field_keys' => self::scalarColumnKeys(),
'file_field_keys' => self::FILE_FIELD_KEYS,
'commitment_field_key' => self::COMMITMENT_KEY,
'scalar_type_hints' => $hints,
'generic_scalar_types' => self::GENERIC_SCALAR_TYPES,
'all_supported_form_keys' => self::allSupportedFormKeys(),
];
}
/**
* @param array<int, mixed>|null $schemaJson
* @return list<string>
*/
public static function keysFromSchemaJson(?array $schemaJson): array
{
if (! is_array($schemaJson)) {
return [];
}
$keys = [];
foreach ($schemaJson as $row) {
if (! is_array($row)) {
continue;
}
$key = trim((string) ($row['key'] ?? ''));
if ($key !== '') {
$keys[] = $key;
}
}
return array_values(array_unique($keys));
}
public static function schemaHasKey(Competition $competition, string $key): bool
{
$competition->loadMissing('formSchema');
$rows = $competition->formSchema?->schema_json;
return in_array($key, self::keysFromSchemaJson(is_array($rows) ? $rows : null), true);
}
/**
* @return list<string>
*/
public static function allowedTypesForScalarKey(string $key): array
{
return self::SCALAR_TYPE_HINTS[$key] ?? self::GENERIC_SCALAR_TYPES;
}
/**
* @param array<int, mixed> $schemaJson
*/
public static function validateSignupSchemaJson(array $schemaJson): ?string
{
$seen = [];
$scalarKeys = array_flip(self::scalarColumnKeys());
foreach ($schemaJson as $index => $row) {
if (! is_array($row)) {
return '报名表字段定义第 '.($index + 1).' 项格式无效';
}
$key = trim((string) ($row['key'] ?? ''));
$type = trim((string) ($row['type'] ?? ''));
$label = trim((string) ($row['label'] ?? ''));
if ($key === '') {
return '报名表存在未填写字段 key 的项(须与 applications 表列名一致)';
}
if ($label === '') {
return '字段「'.$key.'」缺少显示标签';
}
if (isset($seen[$key])) {
return '字段 key「'.$key.'」重复';
}
$seen[$key] = true;
if ($key === self::COMMITMENT_KEY) {
if ($type !== 'checkbox') {
return '参赛承诺书字段须为 checkbox 类型(key='.self::COMMITMENT_KEY.')';
}
continue;
}
if (in_array($key, self::FILE_FIELD_KEYS, true)) {
if ($type !== 'file') {
return '附件字段「'.$key.'」须为 file 类型';
}
continue;
}
if (! isset($scalarKeys[$key])) {
1 month ago
if (! in_array($type, self::GENERIC_SCALAR_TYPES, true)) {
return '自定义字段「'.$key.'」类型须为:'.implode(' / ', self::GENERIC_SCALAR_TYPES);
}
continue;
2 months ago
}
$allowedTypes = self::allowedTypesForScalarKey($key);
if ($type === '' || ! in_array($type, $allowedTypes, true)) {
return '字段「'.$key.'」类型须为:'.implode(' / ', $allowedTypes);
}
}
return null;
}
/**
* @return array<string, mixed>
*/
public static function scalarAttributesFromApplication(Application $app): array
{
$out = [];
foreach (self::scalarColumnKeys() as $key) {
$out[$key] = $app->{$key};
}
return $out;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public static function filterFillableScalarData(array $data): array
{
$allowed = array_flip(self::scalarColumnKeys());
$out = [];
foreach ($data as $key => $value) {
if (isset($allowed[$key])) {
$out[$key] = $value;
}
}
return $out;
}
1 month ago
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public static function filterAnswersJsonData(array $data): array
{
$columnKeys = array_flip(self::scalarColumnKeys());
$out = [];
foreach ($data as $key => $value) {
if (! is_string($key) || $key === '' || isset($columnKeys[$key])) {
continue;
}
if (in_array($key, self::FILE_FIELD_KEYS, true) || $key === self::COMMITMENT_KEY || $key === self::PROMISE_SIGNATURE_KEY) {
continue;
}
$out[$key] = $value;
}
return $out;
}
2 months ago
}