service instanceof \ABeCe\Booking\Contracts\SchedulePreview) { foreach(['/'.$this->route.'/preview','/'.$this->route.'/(?P[1-9][0-9]*)/preview'] as $previewRoute) register_rest_route(self::REST_NAMESPACE,$previewRoute,['methods'=>'POST','permission_callback'=>$permission,'callback'=>fn($r)=>$this->respond(fn()=>new WP_REST_Response($this->service->preview($this->body($r),$r['id']?(int)$r['id']:null)))]); } $args = [ 'page' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 2147483647, 'default' => 1, 'sanitize_callback' => 'absint' ], 'per_page' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 25, 'sanitize_callback' => 'absint' ], ]; foreach ( $this->filters as $filter ) { $args[ $filter ] = $this->route==='members' ? ['type'=>'string','maxLength'=>150] : ('search' === $filter ? [ 'type' => 'string', 'maxLength' => 'external-contacts' === $this->route ? 190 : 150, 'sanitize_callback' => 'sanitize_text_field' ] : [ 'type' => 'integer', 'minimum' => 1, 'maximum' => PHP_INT_MAX, 'sanitize_callback' => 'absint' ]); } register_rest_route( self::REST_NAMESPACE, '/' . $this->route, [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'list' ], 'permission_callback' => $permission, 'args' => $args ], [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create' ], 'permission_callback' => $permission ], ] ); if($this->route==='resources')register_rest_route(self::REST_NAMESPACE,'/resources/suggestions',['methods'=>'GET','permission_callback'=>$permission,'args'=>['sport_id'=>['type'=>'integer','minimum'=>1]],'callback'=>fn($r)=>$this->respond(fn()=>new WP_REST_Response($this->service->suggestions($r['sport_id']===null?null:(int)$r['sport_id'])))]); $detail = [ 'args' => [ 'id' => [ 'type' => 'integer', 'minimum' => 1, 'sanitize_callback' => 'absint' ] ], [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get' ], 'permission_callback' => $permission ], [ 'methods' => 'PATCH,PUT', 'callback' => [ $this, 'update' ], 'permission_callback' => $permission ], ]; if ( $this->allowDelete ) { $detail[] = [ 'methods' => WP_REST_Server::DELETABLE, 'callback' => [ $this, 'delete' ], 'permission_callback' => $permission ]; } register_rest_route( self::REST_NAMESPACE, '/' . $this->route . '/(?P[1-9][0-9]*)', $detail ); } public function permission(): bool|WP_Error { $message = Capabilities::MANAGE_BOOKINGS === $this->capability ? __( 'You do not have permission to manage bookings.', 'abece-booking' ) : ( Capabilities::MANAGE_CANCELLATION_POLICIES === $this->capability ? __( 'You do not have permission to manage cancellation policies.', 'abece-booking' ) : ( Capabilities::MANAGE_PRICES === $this->capability ? __( 'You do not have permission to manage prices.', 'abece-booking' ) : ( Capabilities::MANAGE_MEMBERS === $this->capability ? __( 'You do not have permission to manage members and seasons.', 'abece-booking' ) : __( 'You do not have permission to manage sports and resources.', 'abece-booking' ) ) ) ); return current_user_can( $this->capability ) ?: new WP_Error( 'abece_booking_forbidden', $message, [ 'status' => rest_authorization_required_code() ] ); } public function list( WP_REST_Request $request ): WP_REST_Response|WP_Error { return $this->respond( function () use ( $request ): WP_REST_Response { $filters = []; foreach ( $this->filters as $filter ) { if ( null !== $request[ $filter ] ) { $filters[ $filter ] = $request[ $filter ]; } } $result = $this->service->list( (int) $request['page'], (int) $request['per_page'], $filters ); $response = new WP_REST_Response( $result['items'] ); $response->header( 'X-WP-Total', (string) $result['total'] ); $response->header( 'X-WP-TotalPages', (string) ceil( $result['total'] / $request['per_page'] ) ); return $response; } ); } public function get( WP_REST_Request $request ): WP_REST_Response|WP_Error { return $this->respond( fn() => new WP_REST_Response( $this->service->get( (int) $request->get_url_params()['id'] ) ) ); } public function create( WP_REST_Request $request ): WP_REST_Response|WP_Error { return $this->respond( fn() => new WP_REST_Response( $this->service->save( $this->body( $request ) ), 201 ) ); } public function update( WP_REST_Request $request ): WP_REST_Response|WP_Error { return $this->respond( fn() => new WP_REST_Response( $this->service->save( $this->body( $request ), (int) $request->get_url_params()['id'] ) ) ); } public function delete( WP_REST_Request $request ): WP_REST_Response|WP_Error { return $this->respond( function () use ( $request ): WP_REST_Response { $this->service->delete( (int) $request->get_url_params()['id'] ); return new WP_REST_Response( [ 'deleted' => true ] ); } ); } private function body( WP_REST_Request $request ): array { // JSON preserves integer, boolean and null types for strict service validation. $data = $request->get_json_params(); if ( ! is_array( $data ) || ! is_object( json_decode( $request->get_body() ) ) ) { throw new CatalogException( 'abece_booking_invalid_body', __( 'A JSON object is required.', 'abece-booking' ) ); } return $data; } private function respond( callable $callback ): WP_REST_Response|WP_Error { try { $response=$callback(); if (in_array($this->route,['external-contacts','members','memberships','admin/usage-limit-rules','resource-blocks','schedule-exceptions'],true) && $response instanceof WP_REST_Response) { $response->header('Cache-Control','private, no-store'); } return $response; } catch ( CatalogException $error ) { return new WP_Error( $error->errorCode, $error->getMessage(), [ 'status' => $error->status, 'field' => $error->field ] + $error->details ); } } }