{ values: { id: resolved } } * GET /dynamic/options ?tag=custom-field&control=key&search=&postId= -> { options: [{value,label}] } * * The context post is only used when the user may edit it; otherwise tags render without a post (fallbacks). */ final class DynamicController { public const NS = 'zadex/v1'; public const MAX_ITEMS = 200; private ?Registry $registry; public function __construct(?Registry $registry = null) { $this->registry = $registry; } public function register(): void { add_action('rest_api_init', [$this, 'routes']); } public function routes(): void { register_rest_route(self::NS, '/dynamic/resolve', [ 'methods' => 'POST', 'callback' => [$this, 'resolve'], 'permission_callback' => [$this, 'can_use'], ]); register_rest_route(self::NS, '/dynamic/options', [ 'methods' => 'GET', 'callback' => [$this, 'options'], 'permission_callback' => [$this, 'can_use'], ]); } public function can_use(): bool { return current_user_can('edit_posts'); } /** @return WP_REST_Response|WP_Error */ public function resolve(WP_REST_Request $request) { $body = $request->get_json_params(); if (!is_array($body) || !isset($body['items']) || !is_array($body['items'])) { return new WP_Error('zadex_bad_request', __('Missing items.', 'zadex'), ['status' => 400]); } $ctx = $this->context((int) ($body['postId'] ?? 0)); $registry = $this->registry(); $values = []; foreach (array_slice($body['items'], 0, self::MAX_ITEMS) as $item) { if (!is_array($item) || !isset($item['id']) || !is_scalar($item['id']) || !Value::is($item['value'] ?? null)) { continue; } $type = isset($item['type']) && is_string($item['type']) && Value::category_for_control($item['type']) !== null ? $item['type'] : 'text'; $values[(string) $item['id']] = Resolver::resolve($item['value'], $ctx, $type, $registry); } return new WP_REST_Response(['values' => (object) $values]); } /** @return WP_REST_Response|WP_Error */ public function options(WP_REST_Request $request) { $name = is_string($request['tag']) ? sanitize_key($request['tag']) : ''; $tag = $this->registry()->get($name); if (!$tag) { return new WP_Error('zadex_unknown_tag', __('Unknown dynamic tag.', 'zadex'), ['status' => 404]); } $control = is_string($request['control']) ? sanitize_key($request['control']) : ''; $search = is_string($request['search']) ? sanitize_text_field($request['search']) : ''; $ctx = $this->context((int) ($request['postId'] ?? 0)); $options = []; foreach ($tag->options($control, $search, $ctx) as $opt) { if (is_array($opt) && isset($opt['value']) && is_scalar($opt['value'])) { $options[] = ['value' => (string) $opt['value'], 'label' => (string) ($opt['label'] ?? $opt['value'])]; } } return new WP_REST_Response(['options' => $options]); } private function context(int $post_id): Context { if ($post_id > 0 && current_user_can('edit_post', $post_id)) { return Context::for_post($post_id); } return Context::for_post(0); } private function registry(): Registry { return $this->registry ?? Registry::instance(); } }