PluginProbe
Booking Calendar / 11.6
Booking Calendar v11.6
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / booking-resource-selector / catalog / class-wpbc-booking-resource-query-service.php

class-wpbc-booking-resource-query-service.php in Booking Calendar 11.6, at includes/booking-resource-selector/catalog/class-wpbc-booking-resource-query-service.php

305 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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