.json`, e.g. `query.json`). * * A manifest lists them with `"controlGroups": ["query"]` or * `[{"group": "query", "defaults": {"query_post_type": "product"}, "exclude": ["query_source"]}]`. * The group's sections are inserted before the manifest's first style-tab section and its controls are appended * (ids the manifest already defines win). The editor does the same in `src/editor/core/controlGroups.ts`, so * PHP (defaults, sanitizer) and JS (panel, CSS) see the same control list. Expansion is idempotent. */ final class ControlGroups { /** @var array */ private static array $cache = []; /** @return array{sections: array, controls: array}|null */ public static function group(string $name, string $base_path): ?array { $name = preg_replace('/[^a-z0-9_-]/', '', strtolower($name)) ?? ''; if ($name === '' || $name === 'controls') { return null; } $file = rtrim($base_path, '/\\') . '/_common/' . $name . '.json'; if (!array_key_exists($file, self::$cache)) { $decoded = is_readable($file) ? wp_json_file_decode($file, ['associative' => true]) : null; self::$cache[$file] = is_array($decoded) ? [ 'sections' => is_array($decoded['sections'] ?? null) ? $decoded['sections'] : [], 'controls' => is_array($decoded['controls'] ?? null) ? $decoded['controls'] : [], ] : null; } return self::$cache[$file]; } public static function expand(array $manifest, string $base_path): array { $groups = $manifest['controlGroups'] ?? null; if (!is_array($groups) || !$groups) { return $manifest; } $sections = is_array($manifest['sections'] ?? null) ? $manifest['sections'] : []; $controls = is_array($manifest['controls'] ?? null) ? $manifest['controls'] : []; $section_ids = array_column($sections, 'id'); $control_ids = array_column($controls, 'id'); foreach ($groups as $entry) { $spec = is_string($entry) ? ['group' => $entry] : (is_array($entry) ? $entry : []); $group = self::group((string) ($spec['group'] ?? ''), $base_path); if ($group === null) { continue; } $defaults = is_array($spec['defaults'] ?? null) ? $spec['defaults'] : []; $exclude = is_array($spec['exclude'] ?? null) ? $spec['exclude'] : []; $new_sections = []; foreach ($group['sections'] as $section) { if (is_array($section) && isset($section['id']) && !in_array($section['id'], $section_ids, true)) { $new_sections[] = $section; $section_ids[] = $section['id']; } } if ($new_sections) { $at = count($sections); foreach ($sections as $i => $section) { if (($section['tab'] ?? 'content') === 'style') { $at = $i; break; } } array_splice($sections, $at, 0, $new_sections); } foreach ($group['controls'] as $control) { if (!is_array($control) || !isset($control['id']) || in_array($control['id'], $control_ids, true) || in_array($control['id'], $exclude, true)) { continue; } if (array_key_exists($control['id'], $defaults)) { $control['default'] = $defaults[$control['id']]; } $controls[] = $control; $control_ids[] = $control['id']; } } $manifest['sections'] = $sections; $manifest['controls'] = $controls; return $manifest; } }