Implement Phase 10: REST API Endpoints (v0.10.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m10s

- Add complete REST API infrastructure under src/Api/
- ResponseFormatter for standardized responses
- RateLimiter with tiered limits (public 60/min, availability 30/min, booking 10/min, admin 120/min)
- AbstractController base class with common functionality
- BuildingsController: list, get, rooms endpoints
- RoomsController: list, get, availability, calendar, search endpoints
- BookingsController: CRUD + confirm/check-in/check-out status transitions
- GuestsController: list, get, search, booking history (admin only)
- ServicesController: list, get, calculate endpoints
- PricingController: calculate, seasons endpoints
- API settings tab with enable/disable toggles
- Comprehensive API documentation in README

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 21:24:40 +01:00
parent 87aa89b1a6
commit 81c97c31d7
15 changed files with 4447 additions and 7 deletions

View File

@@ -0,0 +1,323 @@
<?php
/**
* Buildings REST Controller
*
* Handles REST API endpoints for buildings.
*
* @package Magdev\WpBnb\Api\Controllers
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Api\Controllers;
use Magdev\WpBnb\PostTypes\Building;
use Magdev\WpBnb\PostTypes\Room;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WP_Error;
/**
* Buildings Controller class.
*/
final class BuildingsController extends AbstractController {
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'buildings';
/**
* Register routes.
*
* @return void
*/
public function register_routes(): void {
// GET /buildings - List all buildings.
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'public_permission' ),
'args' => $this->get_collection_params(),
),
)
);
// GET /buildings/{id} - Get single building.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'public_permission' ),
'args' => array(
'id' => array(
'description' => __( 'Building ID.', 'wp-bnb' ),
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
),
),
),
)
);
// GET /buildings/{id}/rooms - Get rooms in building.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)/rooms',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_building_rooms' ),
'permission_callback' => array( $this, 'public_permission' ),
'args' => array(
'id' => array(
'description' => __( 'Building ID.', 'wp-bnb' ),
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
),
'status' => array(
'description' => __( 'Filter by room status.', 'wp-bnb' ),
'type' => 'string',
'enum' => array( 'available', 'occupied', 'maintenance', 'blocked' ),
'sanitize_callback' => 'sanitize_text_field',
),
),
),
)
);
}
/**
* Get collection of buildings.
*
* @param WP_REST_Request $request Current request.
* @return WP_REST_Response|WP_Error Response object or error.
*/
public function get_items( $request ) {
// Check rate limit.
$rate_limit_error = $this->check_rate_limit( $request );
if ( $rate_limit_error ) {
return $rate_limit_error;
}
$pagination = $this->get_pagination_params( $request );
$sorting = $this->get_sorting_params( $request, array( 'title', 'date' ), 'title' );
$args = array(
'post_type' => Building::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => $pagination['per_page'],
'offset' => $pagination['offset'],
'orderby' => $sorting['orderby'],
'order' => $sorting['order'],
);
// Search filter.
$search = $request->get_param( 'search' );
if ( $search ) {
$args['s'] = $search;
}
$query = new \WP_Query( $args );
$items = array();
foreach ( $query->posts as $post ) {
$items[] = $this->prepare_building_response( $post );
}
$response = $this->formatter->collection(
$items,
$query->found_posts,
$pagination['page'],
$pagination['per_page']
);
return $this->add_rate_limit_headers( $response, $request );
}
/**
* Get single building.
*
* @param WP_REST_Request $request Current request.
* @return WP_REST_Response|WP_Error Response object or error.
*/
public function get_item( $request ) {
// Check rate limit.
$rate_limit_error = $this->check_rate_limit( $request );
if ( $rate_limit_error ) {
return $rate_limit_error;
}
$id = $request->get_param( 'id' );
$post = get_post( $id );
if ( ! $post || Building::POST_TYPE !== $post->post_type || 'publish' !== $post->post_status ) {
return $this->formatter->not_found( __( 'Building', 'wp-bnb' ) );
}
$data = $this->prepare_building_response( $post, true );
$response = $this->formatter->success( $data );
return $this->add_rate_limit_headers( $response, $request );
}
/**
* Get rooms in a building.
*
* @param WP_REST_Request $request Current request.
* @return WP_REST_Response|WP_Error Response object or error.
*/
public function get_building_rooms( $request ) {
// Check rate limit.
$rate_limit_error = $this->check_rate_limit( $request );
if ( $rate_limit_error ) {
return $rate_limit_error;
}
$building_id = $request->get_param( 'id' );
$building = get_post( $building_id );
if ( ! $building || Building::POST_TYPE !== $building->post_type || 'publish' !== $building->post_status ) {
return $this->formatter->not_found( __( 'Building', 'wp-bnb' ) );
}
$args = array(
'post_type' => Room::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_bnb_room_building_id',
'value' => $building_id,
),
),
'orderby' => 'meta_value',
'meta_key' => '_bnb_room_room_number',
'order' => 'ASC',
);
// Filter by status.
$status = $request->get_param( 'status' );
if ( $status ) {
$args['meta_query'][] = array(
'key' => '_bnb_room_status',
'value' => $status,
);
}
$rooms = get_posts( $args );
$items = array();
foreach ( $rooms as $room ) {
$items[] = $this->prepare_room_summary( $room );
}
$response = $this->formatter->success( $items );
return $this->add_rate_limit_headers( $response, $request );
}
/**
* Prepare building data for response.
*
* @param \WP_Post $post Building post object.
* @param bool $full Include full details.
* @return array Building data.
*/
private function prepare_building_response( \WP_Post $post, bool $full = false ): array {
$data = $this->format_post_base( $post );
// Featured image.
$data['featured_image'] = $this->format_featured_image( $post->ID );
$data['permalink'] = get_permalink( $post->ID );
// Address.
$data['address'] = array(
'street' => get_post_meta( $post->ID, '_bnb_building_street', true ),
'street2' => get_post_meta( $post->ID, '_bnb_building_street2', true ),
'city' => get_post_meta( $post->ID, '_bnb_building_city', true ),
'state' => get_post_meta( $post->ID, '_bnb_building_state', true ),
'postal_code' => get_post_meta( $post->ID, '_bnb_building_zip', true ),
'country' => get_post_meta( $post->ID, '_bnb_building_country', true ),
);
// Contact.
$data['contact'] = array(
'phone' => get_post_meta( $post->ID, '_bnb_building_phone', true ),
'email' => get_post_meta( $post->ID, '_bnb_building_email', true ),
'website' => get_post_meta( $post->ID, '_bnb_building_website', true ),
);
// Details.
$data['details'] = array(
'rooms_count' => (int) get_post_meta( $post->ID, '_bnb_building_total_rooms', true ),
'floors' => (int) get_post_meta( $post->ID, '_bnb_building_floors', true ),
'year_built' => (int) get_post_meta( $post->ID, '_bnb_building_year_built', true ),
'check_in_time' => get_post_meta( $post->ID, '_bnb_building_check_in_time', true ) ?: '14:00',
'check_out_time' => get_post_meta( $post->ID, '_bnb_building_check_out_time', true ) ?: '11:00',
);
// Count actual rooms.
$actual_rooms = Room::get_rooms_for_building( $post->ID );
$data['details']['actual_rooms_count'] = count( $actual_rooms );
// Full address formatted.
if ( $full ) {
$data['address']['formatted'] = Building::get_formatted_address( $post->ID );
// Country name.
$countries = Building::get_countries();
$country_code = $data['address']['country'];
$data['address']['country_name'] = $countries[ $country_code ] ?? $country_code;
}
// Add HATEOAS links.
$data['_links'] = array(
'self' => array(
array( 'href' => rest_url( $this->namespace . '/buildings/' . $post->ID ) ),
),
'rooms' => array(
array( 'href' => rest_url( $this->namespace . '/buildings/' . $post->ID . '/rooms' ) ),
),
);
return $data;
}
/**
* Prepare room summary for building rooms list.
*
* @param \WP_Post $room Room post object.
* @return array Room summary data.
*/
private function prepare_room_summary( \WP_Post $room ): array {
return array(
'id' => $room->ID,
'title' => get_the_title( $room ),
'slug' => $room->post_name,
'permalink' => get_permalink( $room->ID ),
'room_number' => get_post_meta( $room->ID, '_bnb_room_room_number', true ),
'floor' => (int) get_post_meta( $room->ID, '_bnb_room_floor', true ),
'capacity' => (int) get_post_meta( $room->ID, '_bnb_room_capacity', true ),
'status' => get_post_meta( $room->ID, '_bnb_room_status', true ) ?: 'available',
'thumbnail' => get_the_post_thumbnail_url( $room->ID, 'thumbnail' ) ?: null,
'_links' => array(
'self' => array(
array( 'href' => rest_url( $this->namespace . '/rooms/' . $room->ID ) ),
),
),
);
}
}