Files
wordpress-theme-aksrt/inc/classes/admin/class-aksrt-admin-schema-provider.php
T

385 lines
13 KiB
PHP

<?php
/**
* AKSRT Admin Schema Provider
*
* Defines the canonical PHP-side settings contract:
* domain -> section -> group -> field.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Admin_Schema_Provider {
private static $instance = null;
/**
* @var array<string, array<string, mixed>>
*/
private $domain_schemas = [];
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function normalize_domain(string $domain): string {
$domain = trim(str_replace('-', '_', $domain));
return $domain !== '' ? $domain : 'global';
}
public function normalize_section(string $section): string {
return trim($section);
}
public function get_domain_keys(): array {
return [
'global',
'page_display',
'post_list',
'permissions',
'users',
'payment',
'extensions',
'tools',
'shop',
'forum',
];
}
public function get_schema(string $domain): ?array {
$domain = $this->normalize_domain($domain);
if (isset($this->domain_schemas[$domain])) {
return $this->domain_schemas[$domain];
}
$builder_map = $this->get_schema_builder_map();
if (!isset($builder_map[$domain])) {
return null;
}
$builder = $builder_map[$domain];
$this->domain_schemas[$domain] = $builder::build();
return $this->domain_schemas[$domain];
}
public function get_sections(string $domain): array {
$schema = $this->get_schema($domain);
return is_array($schema['sections'] ?? null) ? $schema['sections'] : [];
}
public function get_section_schema(string $domain, string $section): ?array {
$sections = $this->get_sections($domain);
$section = $this->normalize_section($section);
return $sections[$section] ?? null;
}
public function get_option_key(string $domain, string $section = ''): string {
if ($section !== '') {
$schema = $this->get_section_schema($domain, $section);
return (string) ($schema['option_key'] ?? 'aksrt_settings');
}
$primary = [
'global' => 'aksrt_settings',
'page_display' => 'aksrt_settings',
'post_list' => 'aksrt_settings',
'permissions' => 'aksrt_settings',
'users' => 'aksrt_settings',
'payment' => 'aksrt_settings',
'extensions' => 'aksrt_settings',
'tools' => 'aksrt_settings',
'shop' => 'aksrt_settings',
'forum' => 'aksrt_settings',
];
return $primary[$this->normalize_domain($domain)] ?? 'aksrt_settings';
}
public function get_defaults(string $domain, string $section = ''): array {
if ($section !== '') {
$schema = $this->get_section_schema($domain, $section);
return $schema ? $this->collect_defaults($schema) : [];
}
$defaults = [];
foreach ($this->get_sections($domain) as $section_schema) {
$defaults = array_merge($defaults, $this->collect_defaults($section_schema));
}
return $defaults;
}
public function get_allowed_fields(string $domain, string $section = ''): array {
if ($section !== '') {
$schema = $this->get_section_schema($domain, $section);
return $schema ? $this->collect_allowed_fields($schema) : [];
}
$allowed = [];
foreach ($this->get_sections($domain) as $section_schema) {
$allowed = array_merge($allowed, $this->collect_allowed_fields($section_schema));
}
return $allowed;
}
public function validate(string $domain, array $data, string $section = ''): array {
$schemas = [];
if ($section !== '') {
$schema = $this->get_section_schema($domain, $section);
if (!$schema) {
return ['valid' => false, 'errors' => ['Unknown section']];
}
$schemas[] = $schema;
} else {
$schemas = array_values($this->get_sections($domain));
}
$errors = [];
foreach ($schemas as $schema) {
foreach ($this->get_all_fields($schema) as $field) {
$key = $field['key'];
if (!array_key_exists($key, $data)) {
continue;
}
$value = $data[$key];
if (!empty($field['required']) && ($value === '' || $value === null)) {
$errors[] = sprintf('%s is required', $field['label'] ?? $key);
continue;
}
$type = $field['type'] ?? 'input';
if (($type === 'number' || $type === 'slider') && !is_numeric($value)) {
$errors[] = sprintf('%s must be a number', $field['label'] ?? $key);
continue;
}
if ($type === 'checkbox' && !is_array($value)) {
$errors[] = sprintf('%s must be an array', $field['label'] ?? $key);
continue;
}
if ($type === 'repeater' && !is_array($value)) {
$errors[] = sprintf('%s must be an array of items', $field['label'] ?? $key);
continue;
}
if (isset($field['min']) && is_numeric($value) && $value < $field['min']) {
$errors[] = sprintf('%s must be at least %s', $field['label'] ?? $key, $field['min']);
}
if (isset($field['max']) && is_numeric($value) && $value > $field['max']) {
$errors[] = sprintf('%s must be at most %s', $field['label'] ?? $key, $field['max']);
}
if (in_array($type, ['select', 'radio'], true) && !empty($field['options'])) {
$valid_values = array_column($field['options'], 'value');
if (!in_array($value, $valid_values, true)) {
$errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
}
}
if ($type === 'checkbox' && !empty($field['options'])) {
$valid_values = array_column($field['options'], 'value');
foreach ((array) $value as $item) {
if (!in_array($item, $valid_values, true)) {
$errors[] = sprintf('%s has an invalid value', $field['label'] ?? $key);
break;
}
}
}
}
}
return ['valid' => empty($errors), 'errors' => $errors];
}
public function read_settings(string $domain, string $section = ''): array {
if ($section !== '') {
$schema = $this->get_section_schema($domain, $section);
if (!$schema) {
return [];
}
return $this->read_section_settings($schema);
}
$settings = [];
foreach ($this->get_sections($domain) as $section_schema) {
$settings = array_merge($settings, $this->read_section_settings($section_schema));
}
return $settings;
}
public function write_settings(string $domain, array $data, string $section = ''): bool {
if ($section !== '') {
$schema = $this->get_section_schema($domain, $section);
if (!$schema) {
return false;
}
return $this->write_section_settings($schema, $data);
}
$result = true;
foreach ($this->get_sections($domain) as $section_schema) {
$allowed = array_keys($this->collect_allowed_fields($section_schema));
$payload = array_intersect_key($data, array_flip($allowed));
if ($payload === []) {
continue;
}
$result = $this->write_section_settings($section_schema, $payload) && $result;
}
return $result;
}
public function reset_to_defaults(string $domain, string $section = ''): bool {
if ($section !== '') {
return $this->write_settings($domain, $this->get_defaults($domain, $section), $section);
}
return $this->write_settings($domain, $this->get_defaults($domain));
}
private function get_schema_builder_map(): array {
return [
'global' => 'AKSRT_Admin_Schema_Global',
'page_display' => 'AKSRT_Admin_Schema_Page_Display',
'post_list' => 'AKSRT_Admin_Schema_Post_List',
'permissions' => 'AKSRT_Admin_Schema_Permissions',
'users' => 'AKSRT_Admin_Schema_Users',
'payment' => 'AKSRT_Admin_Schema_Payment',
'extensions' => 'AKSRT_Admin_Schema_Extensions',
'tools' => 'AKSRT_Admin_Schema_Tools',
'shop' => 'AKSRT_Admin_Schema_Shop',
'forum' => 'AKSRT_Admin_Schema_Forum',
];
}
private function read_section_settings(array $schema): array {
$option_key = (string) ($schema['option_key'] ?? 'aksrt_settings');
$stored = get_option($option_key, []);
if (!is_array($stored)) {
$stored = [];
}
$data = $this->collect_defaults($schema);
foreach ($this->get_all_fields($schema) as $field) {
if (array_key_exists($field['key'], $stored)) {
$data[$field['key']] = $stored[$field['key']];
}
}
return $data;
}
private function write_section_settings(array $schema, array $data): bool {
$option_key = (string) ($schema['option_key'] ?? 'aksrt_settings');
$stored = get_option($option_key, []);
if (!is_array($stored)) {
$stored = [];
}
$allowed = $this->collect_allowed_fields($schema);
foreach ($allowed as $key => $type) {
if (!array_key_exists($key, $data)) {
continue;
}
$value = $this->sanitize_value($data[$key], $type);
$stored[$key] = $value;
}
$updated = update_option($option_key, $stored);
AKSRT_Settings::flush_settings_cache();
if (isset($stored['site_favicon'])) {
AKSRT_Settings::sync_native_site_icon((string) $stored['site_favicon']);
}
return $updated || true;
}
private function collect_defaults(array $schema): array {
$defaults = [];
foreach ($this->get_all_fields($schema) as $field) {
if (array_key_exists('default', $field)) {
$defaults[$field['key']] = $field['default'];
}
}
return $defaults;
}
private function collect_allowed_fields(array $schema): array {
$allowed = [];
foreach ($this->get_all_fields($schema) as $field) {
$allowed[$field['key']] = $field['sanitize_type'] ?? $this->infer_sanitize_type($field['type'] ?? 'input');
}
return $allowed;
}
private function get_all_fields(array $schema): array {
$fields = [];
foreach ($schema['groups'] ?? [] as $group) {
foreach ($group['fields'] ?? [] as $field) {
$fields[] = $field;
}
}
return $fields;
}
private function sanitize_value($value, string $type) {
switch ($type) {
case 'html':
return wp_kses_post((string) $value);
case 'code':
return is_string($value) ? wp_unslash($value) : '';
case 'url':
return esc_url_raw((string) $value);
case 'int':
return (int) $value;
case 'bool':
return !empty($value);
case 'float':
return (float) $value;
case 'array':
return is_array($value) ? $value : [];
default:
return sanitize_text_field((string) $value);
}
}
private function infer_sanitize_type(string $field_type): string {
$map = [
'switch' => 'bool',
'input' => 'string',
'text' => 'string',
'textarea' => 'string',
'number' => 'int',
'slider' => 'int',
'select' => 'string',
'radio' => 'string',
'checkbox' => 'array',
'repeater' => 'array',
'color' => 'string',
'icon' => 'string',
'media' => 'url',
'code' => 'code',
];
return $map[$field_type] ?? 'string';
}
}