/manifest.json and exposes their manifests, defaults and templates. * The shared "Advanced" controls live in widgets/_common/controls.json and are appended to every widget. */ final class WidgetRegistry { private string $base_path; /** @var array|null */ private ?array $manifests = null; /** @var array */ private array $common_controls = []; /** @var array */ private array $errors = []; public function __construct(string $base_path) { $this->base_path = rtrim($base_path, '/\\') . '/'; } /** @return array type => manifest */ public function all(): array { if ($this->manifests === null) { $this->load(); } return $this->manifests; } public function get(string $type): ?array { return $this->all()[$type] ?? null; } public function has(string $type): bool { return isset($this->all()[$type]); } /** @return array control id => control (widget + common) */ public function controls(string $type): array { $manifest = $this->get($type); if (!$manifest) { return []; } $out = []; foreach ($manifest['controls'] ?? [] as $control) { if (isset($control['id'])) { $out[$control['id']] = $control; } } foreach ($this->common_controls() as $control) { if (isset($control['id']) && !isset($out[$control['id']])) { $out[$control['id']] = $control; } } return $out; } /** @return array base defaults (no breakpoint / state keys) */ public function defaults(string $type): array { $defaults = []; foreach ($this->controls($type) as $id => $control) { if (array_key_exists('default', $control)) { $defaults[$id] = $control['default']; } } return $defaults; } public function template_path(string $type): ?string { $file = $this->base_path . $type . '/render.php'; return is_file($file) ? $file : null; } public function common_controls(): array { if ($this->manifests === null) { $this->load(); } return $this->common_controls; } /** @return array type => error */ public function errors(): array { $this->all(); return $this->errors; } /** Manifests grouped for the editor: list sorted by category then title. */ public function for_editor(): array { $list = array_values($this->all()); usort($list, static fn(array $a, array $b): int => [$a['category'] ?? '', $a['title'] ?? ''] <=> [$b['category'] ?? '', $b['title'] ?? '']); return $list; } private function load(): void { $this->manifests = []; $common = $this->base_path . '_common/controls.json'; if (is_readable($common)) { $decoded = wp_json_file_decode($common, ['associative' => true]); $this->common_controls = is_array($decoded) ? ($decoded['controls'] ?? $decoded) : []; } foreach (glob($this->base_path . '*/manifest.json') ?: [] as $file) { $decoded = wp_json_file_decode($file, ['associative' => true]); $dir = basename(dirname($file)); if (!is_array($decoded) || empty($decoded['type']) || !is_string($decoded['type'])) { $this->errors[$dir] = 'Invalid manifest (missing type)'; continue; } if ($decoded['type'] !== $dir) { $this->errors[$dir] = sprintf('Manifest type "%s" does not match folder "%s"', $decoded['type'], $dir); continue; } $decoded += ['title' => ucfirst($decoded['type']), 'icon' => 'box', 'category' => 'basic', 'render' => 'client', 'controls' => [], 'sections' => [], 'keywords' => [], 'assets' => []]; $decoded['isContainer'] = !empty($decoded['isContainer']); $decoded = \Zadex\Loop\ControlGroups::expand($decoded, $this->base_path); // "controlGroups": ["query"] $this->manifests[$decoded['type']] = $decoded; } ksort($this->manifests); $this->manifests = apply_filters('zadex/widgets/manifests', $this->manifests); } }