query_handler_registry = Handlers\QueryHandlerRegistry::get_instance(); $this->register_default_handlers(); add_action( 'rest_api_init', [ $this, 'register_routes' ] ); /** * Allows themes and plugins to register their own custom query handlers. * * @param Handlers\QueryHandlerRegistry $registry The registry instance. */ apply_filters( 'adpresso_register_query_handlers', $this->query_handler_registry ); } /** * Registers the default handlers for core filtering and sorting capabilities. */ private function register_default_handlers(): void { $this->query_handler_registry->register( new Handlers\PostStatusQueryHandler() ); $this->query_handler_registry->register( new Handlers\AuthorQueryHandler() ); $this->query_handler_registry->register( new Handlers\IdQueryHandler() ); $this->query_handler_registry->register( new Handlers\TitleQueryHandler() ); $this->query_handler_registry->register( new Handlers\ConditionsQueryHandler() ); $this->query_handler_registry->register( new Handlers\UsedInQueryHandler() ); $this->query_handler_registry->register( new Handlers\DateQueryHandler() ); // A generic handler for simple meta-queries based on a mapping. $this->query_handler_registry->register( new Handlers\MetaQueryHandler( [ 'ads' => [ 'type' => Constants::META_PREFIX . 'ad_type' ], 'placements' => [ 'type' => Constants::META_PREFIX . 'placement_type' ], 'groups' => [ 'output_type' => Constants::META_PREFIX . 'output' ], ] ) ); // Sort Handler --- $sort_map = apply_filters( 'adpresso_rest_sortable_columns_map', [ 'id' => 'ID', 'title' => 'title', 'status' => 'status', 'date' => 'date', 'date_modified' => 'modified', 'author' => 'author', 'conditions' => [ 'meta_key' => Constants::META_PREFIX . 'conditions_tree' ], 'preview' => [ 'meta_key' => Constants::META_PREFIX . 'image_id', 'orderby' => 'meta_value_num' ], 'target_url' => [ 'meta_key' => Constants::META_PREFIX . 'target_url' ], 'refresh' => [ 'meta_key' => Constants::META_PREFIX . 'refresh', 'orderby' => 'meta_value_num', ], ] ); $this->query_handler_registry->register( new Handlers\SortQueryHandler( $sort_map ) ); } /** * Registers all REST API routes for this controller. * This method is called by the main plugin's hook system. */ public function register_routes(): void { // Route for the collection (e.g., GET /wp-json/adpresso/v1/ads) register_rest_route( $this->namespace, '/' . $this->rest_base, [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_items' ], 'permission_callback' => [ $this, 'get_items_permissions_check' ], 'args' => $this->get_collection_params(), ], [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_item' ], 'permission_callback' => [ $this, 'create_item_permissions_check' ], 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ], ] ); // Route for a single item (e.g., GET /wp-json/adpresso/v1/ads/123) register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_item' ], 'permission_callback' => [ $this, 'get_item_permissions_check' ], 'args' => [ 'context' => $this->get_context_param(), ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_item' ], 'permission_callback' => [ $this, 'update_item_permissions_check' ], 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ], [ 'methods' => WP_REST_Server::DELETABLE, 'callback' => [ $this, 'delete_item' ], 'permission_callback' => [ $this, 'delete_item_permissions_check' ], 'args' => [ 'force' => [ 'type' => 'boolean', 'default' => false ], ], ], ] ); register_rest_route( $this->namespace, '/schema/(?P[\w-]+)', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_item_schema_data' ], 'permission_callback' => [ $this, 'create_item_permissions_check' ], 'args' => [ 'entity_type' => [ 'required' => true, 'type' => 'string' ], ], ], ] ); } // Methods to be implemented by child classes --- abstract public function get_items( WP_REST_Request $request ): WP_REST_Response|WP_Error; abstract public function get_item( WP_REST_Request $request ): WP_REST_Response|WP_Error; abstract public function create_item( WP_REST_Request $request ): WP_REST_Response|WP_Error; abstract public function update_item( WP_REST_Request $request ): WP_REST_Response|WP_Error; abstract public function delete_item( WP_REST_Request $request ): WP_REST_Response|WP_Error; abstract public function get_items_permissions_check( WP_REST_Request $request ): bool|WP_Error; abstract public function get_item_permissions_check( WP_REST_Request $request ): bool|WP_Error; abstract public function create_item_permissions_check( WP_REST_Request $request ): bool|WP_Error; abstract public function update_item_permissions_check( WP_REST_Request $request ): bool|WP_Error; abstract public function delete_item_permissions_check( WP_REST_Request $request ): bool|WP_Error; abstract protected function get_collection_params(): array; abstract protected function get_endpoint_args_for_item_schema( string $method ): array; /** * Get the context parameters. * * @param array $args * * @return array */ protected function get_context_param( array $args = [] ): array { return array_merge( [ 'description' => 'Scope under which the request is made; determines fields returned.', 'type' => 'string', 'enum' => [ 'view', 'edit' ], 'default' => 'view', ], $args ); } /** * Builds the WP_Query arguments from the REST request parameters * by delegating to the registered Query Handlers. * * @param WP_REST_Request $request The request object. * * @return array The arguments for WP_Query. */ protected function build_query_args( WP_REST_Request $request ): array { $params = $request->get_params(); $entity_type = $this->rest_base; // e.g., 'ads', 'groups' $args = [ 'post_type' => $this->post_type, 'post_status' => 'any', 'posts_per_page' => $params['per_page'], 'paged' => $params['page'], 'order' => ( strtolower( $params['order'] ) === 'asc' ) ? 'ASC' : 'DESC', ]; // Apply Sorting $orderby_key = $params['orderby']; $sort_handler = $this->query_handler_registry->find( 'orderby', $entity_type ); if ( $sort_handler ) { $args = $sort_handler->apply( $args, $orderby_key, $entity_type ); } else { $args['orderby'] = 'date'; // Default fallback } // Apply Search Term if ( ! empty( $params['s'] ) ) { $args['s'] = $params['s']; } // Apply Filters $filters = $params['filters'] ?? []; $args['filters'] = $filters; foreach ( $filters as $filter_key => $filter_value ) { if ( empty( $filter_value ) ) { continue; } $filter_handler = $this->query_handler_registry->find( (string) $filter_key, $entity_type ); if ( $filter_handler ) { $args = $filter_handler->apply( $args, $filter_value, $entity_type ); } } return $args; } /** * Returns the UI schema and default values for creating a new item. * * @param WP_REST_Request $request * * @return WP_REST_Response|WP_Error */ public function get_item_schema_data( WP_REST_Request $request ): WP_REST_Response|WP_Error { $entity_type_slug = $request->get_param( 'entity_type' ); $entity = $this->create_empty_entity( $entity_type_slug ); if ( ! $entity ) { return new WP_Error( 'rest_invalid_entity_type', 'invalid entity type' ); } $response_data = $entity->prepare_for_rest_response(); // Override ID to signal that it's a new entity. $response_data['id'] = 0; $response_data['title'] = ''; $response_data['status'] = 'publish'; // We need to ensure that 'meta_data' is not empty. // We return default metadata for virtual entities. $response_data['meta_data'] = $entity->get_default_meta_data(); return new WP_REST_Response( $response_data, 200 ); } /** * Helper to create an empty entity instance based on slug. * * @param string $slug * * @return Entity|null */ private function create_empty_entity( string $slug ): ?Entity { return match ( $slug ) { 'ads' => new Ad( null ), 'groups' => new Group( null ), 'placements' => new Placement( null ), default => null, }; } }