booking
/
includes
/
booking-resource-selector
/
catalog
/
class-wpbc-booking-resource-query-service.php
class-wpbc-booking-resource-query-service.php in Booking Calendar 11.9, at includes/booking-resource-selector/catalog/class-wpbc-booking-resource-query-service.php
| 1 | <?php |
| 2 | /** |
| 3 | * Public Booking Resource repository and query service. |
| 4 | * |
| 5 | * @package Booking Calendar |
| 6 | * @since 11.6.0 |
| 7 | */ |
| 8 | |
| 9 | if ( ! defined( 'ABSPATH' ) ) { |
| 10 | exit; |
| 11 | } |
| 12 | |
| 13 | /** |
| 14 | * Read canonical Resource data and expose a frontend-safe DTO collection. |
| 15 | * |
| 16 | * This service reads established edition-specific Resource providers but does |
| 17 | * not render HTML. It is the shared data boundary for public selectors, |
| 18 | * shortcodes, blocks, and future frontend catalog integrations. |
| 19 | */ |
| 20 | final class WPBC_Booking_Resource_Query_Service { |
| 21 | |
| 22 | /** |
| 23 | * Query public Booking Resources. |
| 24 | * |
| 25 | * Supported query keys are `resource_ids` (ordered allow-list) and |
| 26 | * `include_summaries`. Unknown keys are ignored so callers cannot widen the |
| 27 | * public record or request admin-only data. |
| 28 | * |
| 29 | * @param array<string,mixed> $query Public Resource query. |
| 30 | * |
| 31 | * @return array<int,array<string,mixed>> Resources keyed by Resource ID. |
| 32 | */ |
| 33 | public function get_resources( $query = array() ) { |
| 34 | $query = wp_parse_args( |
| 35 | is_array( $query ) ? $query : array(), |
| 36 | array( |
| 37 | 'resource_ids' => array(), |
| 38 | 'include_summaries' => true, |
| 39 | ) |
| 40 | ); |
| 41 | $allowed_resource_ids = function_exists( 'wpbc_booking_resource_selector_normalize_ids' ) |
| 42 | ? wpbc_booking_resource_selector_normalize_ids( $query['resource_ids'] ) |
| 43 | : array_values( array_unique( array_filter( array_map( 'absint', (array) $query['resource_ids'] ) ) ) ); |
| 44 | $raw_resources = (array) apply_bk_filter( 'wpdebk_get_keyed_all_bk_resources', array() ); |
| 45 | $search_options = function_exists( 'wpbc_searchable_resources__get_all_options' ) |
| 46 | ? (array) wpbc_searchable_resources__get_all_options() |
| 47 | : array(); |
| 48 | $resources = array(); |
| 49 | |
| 50 | foreach ( $raw_resources as $resource_key => $raw_resource ) { |
| 51 | $resource_id = $this->get_resource_id( $raw_resource, $resource_key ); |
| 52 | if ( ! $resource_id ) { |
| 53 | continue; |
| 54 | } |
| 55 | $resource_options = isset( $search_options[ $resource_id ] ) && is_array( $search_options[ $resource_id ] ) |
| 56 | ? $search_options[ $resource_id ] |
| 57 | : array(); |
| 58 | $resources[ $resource_id ] = $this->normalize_resource( $raw_resource, $resource_id, $resource_options, ! empty( $query['include_summaries'] ) ); |
| 59 | } |
| 60 | |
| 61 | if ( empty( $resources ) && ! class_exists( 'wpdev_bk_personal' ) ) { |
| 62 | $resource_id = class_exists( 'WPBC_FE_Attr_Postprocessor' ) |
| 63 | ? WPBC_FE_Attr_Postprocessor::get_default_booking_resource_id() |
| 64 | : 1; |
| 65 | $resources[ $resource_id ] = $this->normalize_resource( array(), $resource_id, array(), ! empty( $query['include_summaries'] ) ); |
| 66 | } |
| 67 | |
| 68 | $resources = $this->attach_hierarchy( $resources ); |
| 69 | if ( ! empty( $allowed_resource_ids ) ) { |
| 70 | $ordered_resources = array(); |
| 71 | foreach ( $allowed_resource_ids as $resource_id ) { |
| 72 | if ( isset( $resources[ $resource_id ] ) ) { |
| 73 | $ordered_resources[ $resource_id ] = $resources[ $resource_id ]; |
| 74 | } |
| 75 | } |
| 76 | $resources = $ordered_resources; |
| 77 | } |
| 78 | |
| 79 | /** |
| 80 | * Filters frontend-safe Booking Resource DTO arrays after query filtering. |
| 81 | * |
| 82 | * @param array<int,array<string,mixed>> $resources Public Resources keyed by ID. |
| 83 | * @param array<string,mixed> $query Normalized public query. |
| 84 | */ |
| 85 | return (array) apply_filters( 'wpbc_booking_resource_catalog_query_results', $resources, $query ); |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * Resolve one Resource ID from a raw object, array, or keyed collection. |
| 90 | * |
| 91 | * @param mixed $raw_resource Raw Resource value. |
| 92 | * @param mixed $fallback_id Key supplied by the Resource collection. |
| 93 | * |
| 94 | * @return int Positive Resource ID or zero. |
| 95 | */ |
| 96 | private function get_resource_id( $raw_resource, $fallback_id ) { |
| 97 | $resource = is_object( $raw_resource ) ? get_object_vars( $raw_resource ) : (array) $raw_resource; |
| 98 | |
| 99 | if ( ! empty( $resource['id'] ) ) { |
| 100 | return absint( $resource['id'] ); |
| 101 | } |
| 102 | if ( ! empty( $resource['booking_type_id'] ) ) { |
| 103 | return absint( $resource['booking_type_id'] ); |
| 104 | } |
| 105 | |
| 106 | return absint( $fallback_id ); |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Normalize one raw Resource into a frontend-safe DTO array. |
| 111 | * |
| 112 | * @param mixed $raw_resource Raw Resource object or array. |
| 113 | * @param int $resource_id Resource ID. |
| 114 | * @param array<string,mixed> $resource_options Searchable Resource presentation fallback. |
| 115 | * @param bool $include_summaries Whether summary providers should run. |
| 116 | * |
| 117 | * @return array<string,mixed> Public Resource DTO values. |
| 118 | */ |
| 119 | private function normalize_resource( $raw_resource, $resource_id, $resource_options, $include_summaries ) { |
| 120 | $resource = is_object( $raw_resource ) ? get_object_vars( $raw_resource ) : (array) $raw_resource; |
| 121 | $raw_title = isset( $resource['title'] ) && is_scalar( $resource['title'] ) ? (string) $resource['title'] : ''; |
| 122 | $content = function_exists( 'wpbc_booking_resource_content_repository' ) |
| 123 | ? wpbc_booking_resource_content_repository()->get( $resource_id, $raw_title, true ) |
| 124 | : array(); |
| 125 | $title = ! empty( $content['title'] ) ? (string) $content['title'] : $raw_title; |
| 126 | if ( '' === trim( $title ) ) { |
| 127 | /* translators: %d: Booking Resource ID. */ |
| 128 | $title = sprintf( __( 'Booking Resource #%d', 'booking' ), $resource_id ); |
| 129 | } |
| 130 | |
| 131 | $description = ! empty( $content['description'] ) ? (string) $content['description'] : ''; |
| 132 | if ( '' === $description && ! empty( $resource['description'] ) && is_scalar( $resource['description'] ) ) { |
| 133 | $description = (string) $resource['description']; |
| 134 | } |
| 135 | if ( '' === $description && ! empty( $resource_options['description'] ) && is_scalar( $resource_options['description'] ) ) { |
| 136 | $description = (string) $resource_options['description']; |
| 137 | } |
| 138 | $image_url = ! empty( $content['picture_url'] ) ? (string) $content['picture_url'] : ''; |
| 139 | if ( '' === $image_url && ! empty( $resource['image_url'] ) && is_scalar( $resource['image_url'] ) ) { |
| 140 | $image_url = (string) $resource['image_url']; |
| 141 | } |
| 142 | if ( '' === $image_url && ! empty( $resource['picture'] ) && is_scalar( $resource['picture'] ) ) { |
| 143 | $image_url = (string) $resource['picture']; |
| 144 | } |
| 145 | if ( '' === $image_url && ! empty( $resource_options['picture'] ) ) { |
| 146 | $image_value = is_array( $resource_options['picture'] ) ? reset( $resource_options['picture'] ) : $resource_options['picture']; |
| 147 | $image_url = is_scalar( $image_value ) ? (string) $image_value : ''; |
| 148 | } |
| 149 | |
| 150 | $dto_values = array( |
| 151 | 'resource_id' => absint( $resource_id ), |
| 152 | 'title' => wp_strip_all_tags( wpbc_lang( $title ) ), |
| 153 | 'description' => wp_strip_all_tags( wpbc_lang( $description ) ), |
| 154 | 'image_url' => esc_url_raw( wpbc_lang( $image_url ) ), |
| 155 | 'attachment_id' => ! empty( $content['attachment_id'] ) ? absint( $content['attachment_id'] ) : 0, |
| 156 | 'parent_id' => isset( $resource['parent'] ) ? absint( $resource['parent'] ) : 0, |
| 157 | 'parent_title' => '', |
| 158 | 'resource_type' => isset( $resource['parent'] ) && absint( $resource['parent'] ) ? 'child' : 'single', |
| 159 | 'child_ids' => array(), |
| 160 | 'child_count' => 0, |
| 161 | 'capacity' => 1, |
| 162 | 'count' => isset( $resource['count'] ) ? absint( $resource['count'] ) : 0, |
| 163 | 'availability_summary' => $include_summaries ? $this->get_availability_summary( $resource_id, $resource ) : array(), |
| 164 | 'price_summary' => $include_summaries ? $this->get_price_summary( $resource_id, $resource ) : array(), |
| 165 | ); |
| 166 | |
| 167 | /** |
| 168 | * Filters one public DTO before it enters the Resource catalog. |
| 169 | * |
| 170 | * @param array<string,mixed> $dto_values Frontend-safe Resource values. |
| 171 | * @param array<string,mixed> $resource Raw Resource values. |
| 172 | * @param array<string,mixed> $resource_options Searchable Resource fallbacks. |
| 173 | */ |
| 174 | $dto_values = (array) apply_filters( 'wpbc_booking_resource_catalog_dto', $dto_values, $resource, $resource_options ); |
| 175 | |
| 176 | return ( new WPBC_Booking_Resource_DTO( $dto_values ) )->to_array(); |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * Derive parent and child metadata without exposing the admin hierarchy UI. |
| 181 | * |
| 182 | * @param array<int,array<string,mixed>> $resources Resources keyed by ID. |
| 183 | * |
| 184 | * @return array<int,array<string,mixed>> Resources with hierarchy values. |
| 185 | */ |
| 186 | private function attach_hierarchy( $resources ) { |
| 187 | $child_ids_by_parent = array(); |
| 188 | foreach ( $resources as $resource ) { |
| 189 | $parent_id = absint( $resource['parent_id'] ); |
| 190 | if ( $parent_id && isset( $resources[ $parent_id ] ) ) { |
| 191 | if ( ! isset( $child_ids_by_parent[ $parent_id ] ) ) { |
| 192 | $child_ids_by_parent[ $parent_id ] = array(); |
| 193 | } |
| 194 | $child_ids_by_parent[ $parent_id ][] = absint( $resource['resource_id'] ); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | foreach ( $resources as $resource_id => $resource ) { |
| 199 | $parent_id = absint( $resource['parent_id'] ); |
| 200 | $child_ids = isset( $child_ids_by_parent[ $resource_id ] ) ? $child_ids_by_parent[ $resource_id ] : array(); |
| 201 | if ( $parent_id && isset( $resources[ $parent_id ] ) ) { |
| 202 | $resource['resource_type'] = 'child'; |
| 203 | $resource['parent_title'] = (string) $resources[ $parent_id ]['title']; |
| 204 | } elseif ( ! empty( $child_ids ) ) { |
| 205 | $resource['resource_type'] = 'parent'; |
| 206 | } else { |
| 207 | $resource['resource_type'] = 'single'; |
| 208 | } |
| 209 | $resource['child_ids'] = array_values( array_map( 'absint', $child_ids ) ); |
| 210 | $resource['child_count'] = count( $child_ids ); |
| 211 | $resource['capacity'] = 'parent' === $resource['resource_type'] ? count( $child_ids ) + 1 : 1; |
| 212 | $resource['count'] = $resource['capacity']; |
| 213 | $resources[ $resource_id ] = ( new WPBC_Booking_Resource_DTO( $resource ) )->to_array(); |
| 214 | } |
| 215 | |
| 216 | return $resources; |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * Build a truthful context-free availability summary. |
| 221 | * |
| 222 | * Exact availability depends on dates, duration, capacity, and edition |
| 223 | * rules. The catalog therefore prompts visitors to check dates rather than |
| 224 | * claiming that a Resource is available. Date-aware integrations may replace |
| 225 | * this structure through the documented filter. |
| 226 | * |
| 227 | * @param int $resource_id Resource ID. |
| 228 | * @param array<string,mixed> $resource Raw Resource values. |
| 229 | * |
| 230 | * @return array<string,string> Structured availability summary. |
| 231 | */ |
| 232 | private function get_availability_summary( $resource_id, $resource ) { |
| 233 | $summary = array( |
| 234 | 'status' => 'requires_dates', |
| 235 | 'label' => __( 'Check available dates', 'booking' ), |
| 236 | 'description' => __( 'Availability is confirmed after you choose dates.', 'booking' ), |
| 237 | ); |
| 238 | |
| 239 | /** |
| 240 | * Filters the public availability summary for one Resource. |
| 241 | * |
| 242 | * @param array<string,string> $summary Default context-free summary. |
| 243 | * @param int $resource_id Resource ID. |
| 244 | * @param array<string,mixed> $resource Raw Resource values. |
| 245 | */ |
| 246 | return (array) apply_filters( 'wpbc_booking_resource_catalog_availability_summary', $summary, $resource_id, $resource ); |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Build the edition-aware starting-price summary for one Resource. |
| 251 | * |
| 252 | * @param int $resource_id Resource ID. |
| 253 | * @param array<string,mixed> $resource Raw Resource values. |
| 254 | * |
| 255 | * @return array<string,string> Structured price summary, or an empty array. |
| 256 | */ |
| 257 | private function get_price_summary( $resource_id, $resource ) { |
| 258 | $summary = array(); |
| 259 | $cost = isset( $resource['cost'] ) && is_scalar( $resource['cost'] ) ? (string) $resource['cost'] : ''; |
| 260 | if ( class_exists( 'wpdev_bk_biz_s' ) && '' !== $cost && function_exists( 'wpbc_get_cost_with_currency_for_user' ) ) { |
| 261 | $formatted_cost = html_entity_decode( wp_strip_all_tags( (string) wpbc_get_cost_with_currency_for_user( $cost, $resource_id ) ), ENT_QUOTES, 'UTF-8' ); |
| 262 | $period = function_exists( 'wpbc_get_cost_per_period_for_user' ) |
| 263 | ? sanitize_key( (string) wpbc_get_cost_per_period_for_user( $resource_id ) ) |
| 264 | : ''; |
| 265 | $period_labels = array( |
| 266 | 'day' => __( 'per day', 'booking' ), |
| 267 | 'night' => __( 'per night', 'booking' ), |
| 268 | 'hour' => __( 'per hour', 'booking' ), |
| 269 | 'fixed' => __( 'fixed price', 'booking' ), |
| 270 | ); |
| 271 | $summary = array( |
| 272 | 'amount' => $cost, |
| 273 | 'formatted' => $formatted_cost, |
| 274 | 'period' => $period, |
| 275 | 'period_label' => isset( $period_labels[ $period ] ) ? $period_labels[ $period ] : '', |
| 276 | 'label' => sprintf( __( 'From %s', 'booking' ), $formatted_cost ), |
| 277 | ); |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Filters the public starting-price summary for one Resource. |
| 282 | * |
| 283 | * @param array<string,string> $summary Default edition-aware price summary. |
| 284 | * @param int $resource_id Resource ID. |
| 285 | * @param array<string,mixed> $resource Raw Resource values. |
| 286 | */ |
| 287 | return (array) apply_filters( 'wpbc_booking_resource_catalog_price_summary', $summary, $resource_id, $resource ); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * Return the shared public Booking Resource query service. |
| 293 | * |
| 294 | * @return WPBC_Booking_Resource_Query_Service Query service singleton. |
| 295 | */ |
| 296 | function wpbc_booking_resource_catalog_query_service() { |
| 297 | static $query_service = null; |
| 298 | |
| 299 | if ( null === $query_service ) { |
| 300 | $query_service = new WPBC_Booking_Resource_Query_Service(); |
| 301 | } |
| 302 | |
| 303 | return $query_service; |
| 304 | } |
| 305 |