PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
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 / page-setup-wizard / step-start-duration-times / class-wpbc-setup-wizard-start-duration-times.php

class-wpbc-setup-wizard-start-duration-times.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-start-duration-times/class-wpbc-setup-wizard-start-duration-times.php

264 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Start Time and Duration proposal service for the Setup Wizard.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Build, validate, and present reusable start-time and duration choices.
14 *
15 * Values use the `HH:MM` storage contract accepted by Form Builder's
16 * `starttime` and `durationtime` fields. The proposal remains checkpoint-only
17 * until the Booking Form step applies it to the selected template.
18 */
19 final class WPBC_Setup_Wizard_Start_Duration_Times {
20
21 const MAX_TIMES_PER_LIST = 288;
22 const MAX_PAYLOAD_LENGTH = 10000;
23 const TIME_INCREMENT = 5;
24
25 /** @var WPBC_Setup_Wizard_Start_End_Times */
26 private $clock_times;
27
28 /**
29 * Build the service around the shared clock-time helper.
30 *
31 * @param WPBC_Setup_Wizard_Start_End_Times|null $clock_times Optional helper for tests or composition.
32 */
33 public function __construct( $clock_times = null ) {
34 $this->clock_times = $clock_times instanceof WPBC_Setup_Wizard_Start_End_Times
35 ? $clock_times
36 : new WPBC_Setup_Wizard_Start_End_Times();
37 }
38
39 /**
40 * Return starter choices aligned with the recommended Form Builder template.
41 *
42 * @return array{start_times:string[],duration_times:string[]} Starter choices.
43 */
44 public function get_initial_start_duration_times() {
45 return array(
46 'start_times' => $this->clock_times->generate_time_values( '10:00', '15:40', 20 ),
47 'duration_times' => array( '00:20', '00:40', '01:00', '01:20', '01:40', '02:00' ),
48 );
49 }
50
51 /**
52 * Build presentation data for the shared sortable choice-list editor.
53 *
54 * @param array<string,mixed> $step_values Validated values for this module.
55 * @param string $time_format Validated WordPress time format.
56 *
57 * @return array<string,mixed> Data-only template context.
58 */
59 public function get_context( array $step_values, $time_format ) {
60 $start_duration_times = isset( $step_values['start_duration_times'] ) && is_array( $step_values['start_duration_times'] )
61 ? $step_values['start_duration_times']
62 : $this->get_initial_start_duration_times();
63
64 return array(
65 'values' => $step_values,
66 'draft_field_name' => 'start_duration_times',
67 'draft_values' => $start_duration_times,
68 'editor_title' => __( 'Set start and duration times', 'booking' ),
69 'editor_description' => __( 'Choose the start times and duration options customers can use when booking.', 'booking' ),
70 'validation_rule' => 'independent_lists',
71 'list_definitions' => array(
72 'start_times' => array(
73 'title' => __( 'Start times', 'booking' ),
74 'description' => __( 'Choices shown in the booking form.', 'booking' ),
75 'list_label' => __( 'Start times', 'booking' ),
76 'clear_aria_label' => __( 'Clear Start times', 'booking' ),
77 'clear_status' => __( 'Start times cleared.', 'booking' ),
78 'add_label' => __( 'Add one time slot', 'booking' ),
79 'generator_interval' => 20,
80 'options' => $this->clock_times->get_time_options( $time_format ),
81 ),
82 'duration_times' => array(
83 'title' => __( 'Duration times', 'booking' ),
84 'description' => __( 'Duration choices shown in the booking form.', 'booking' ),
85 'list_label' => __( 'Duration times', 'booking' ),
86 'clear_aria_label' => __( 'Clear Duration times', 'booking' ),
87 'clear_status' => __( 'Duration times cleared.', 'booking' ),
88 'add_label' => __( 'Add one duration', 'booking' ),
89 'generator_interval' => 20,
90 'options' => $this->get_duration_options(),
91 ),
92 ),
93 'copy_action' => array(),
94 'interval_options' => $this->clock_times->get_interval_options(),
95 'max_times_per_list' => self::MAX_TIMES_PER_LIST,
96 'time_increment' => self::TIME_INCREMENT,
97 );
98 }
99
100 /**
101 * Validate one complete start-time and duration-choice DTO.
102 *
103 * @param mixed $raw_start_duration_times JSON transport string or stored array.
104 * @param bool $require_complete Whether both lists are required.
105 *
106 * @return array{start_times:string[],duration_times:string[]}|WP_Error Normalized choices or an error.
107 */
108 public function validate_start_duration_times( $raw_start_duration_times, $require_complete ) {
109 if ( is_string( $raw_start_duration_times ) ) {
110 if ( strlen( $raw_start_duration_times ) > self::MAX_PAYLOAD_LENGTH ) {
111 return new WP_Error( 'wpbc_setup_wizard_start_duration_times_too_large', __( 'The start time and duration draft is too large.', 'booking' ) );
112 }
113
114 $raw_start_duration_times = json_decode( $raw_start_duration_times, true );
115 if ( JSON_ERROR_NONE !== json_last_error() ) {
116 return new WP_Error( 'wpbc_setup_wizard_start_duration_times_invalid_json', __( 'The start time and duration draft is invalid.', 'booking' ) );
117 }
118 }
119
120 if ( ! is_array( $raw_start_duration_times ) ) {
121 return new WP_Error( 'wpbc_setup_wizard_start_duration_times_invalid', __( 'Enter valid start time and duration choices.', 'booking' ) );
122 }
123
124 if ( array_diff( array_keys( $raw_start_duration_times ), array( 'start_times', 'duration_times' ) ) ) {
125 return new WP_Error( 'wpbc_setup_wizard_start_duration_times_unknown_field', __( 'The start time and duration draft contains an unsupported field.', 'booking' ) );
126 }
127
128 $start_times = $this->clock_times->validate_time_list(
129 isset( $raw_start_duration_times['start_times'] ) ? $raw_start_duration_times['start_times'] : array(),
130 $require_complete,
131 __( 'Add at least one start time.', 'booking' )
132 );
133 if ( is_wp_error( $start_times ) ) {
134 return $start_times;
135 }
136
137 $duration_times = $this->validate_duration_list(
138 isset( $raw_start_duration_times['duration_times'] ) ? $raw_start_duration_times['duration_times'] : array(),
139 $require_complete
140 );
141 if ( is_wp_error( $duration_times ) ) {
142 return $duration_times;
143 }
144
145 return array(
146 'start_times' => $start_times,
147 'duration_times' => $duration_times,
148 );
149 }
150
151 /**
152 * Return every supported positive duration in five-minute increments.
153 *
154 * @return array<int,array{value:string,label:string}> Ordered duration options.
155 */
156 public function get_duration_options() {
157 $options = array();
158 for ( $minute = self::TIME_INCREMENT; $minute <= DAY_IN_SECONDS / MINUTE_IN_SECONDS; $minute += self::TIME_INCREMENT ) {
159 $options[] = array(
160 'value' => $this->minutes_to_duration( $minute ),
161 'label' => $this->format_duration_label( $minute ),
162 );
163 }
164
165 return $options;
166 }
167
168 /**
169 * Format one validated duration for customer-facing summaries and choices.
170 *
171 * @param int $duration_minutes Positive duration in minutes.
172 *
173 * @return string Localized duration label.
174 */
175 public function format_duration_label( $duration_minutes ) {
176 $duration_minutes = absint( $duration_minutes );
177 $hours = (int) floor( $duration_minutes / 60 );
178 $minutes = $duration_minutes % 60;
179
180 if ( 0 < $hours && 0 < $minutes ) {
181 /* translators: 1: Number of hours, 2: Number of minutes. */
182 return sprintf( __( '%1$d hr %2$d min', 'booking' ), $hours, $minutes );
183 }
184 if ( 0 < $hours ) {
185 /* translators: %d: Number of hours. */
186 return sprintf( _n( '%d hour', '%d hours', $hours, 'booking' ), $hours );
187 }
188
189 /* translators: %d: Number of minutes. */
190 return sprintf( _n( '%d minute', '%d minutes', $minutes, 'booking' ), $minutes );
191 }
192
193 /**
194 * Validate one ordered list of unique positive durations.
195 *
196 * @param mixed $raw_duration_list Untrusted duration-list candidate.
197 * @param bool $require_complete Whether an empty list is invalid.
198 *
199 * @return string[]|WP_Error Normalized durations or an error.
200 */
201 private function validate_duration_list( $raw_duration_list, $require_complete ) {
202 if ( ! is_array( $raw_duration_list ) ) {
203 return new WP_Error( 'wpbc_setup_wizard_duration_list_invalid', __( 'The duration list must contain valid choices.', 'booking' ) );
204 }
205
206 $raw_duration_list = array_values( $raw_duration_list );
207 if ( count( $raw_duration_list ) > self::MAX_TIMES_PER_LIST ) {
208 /* translators: %d: Maximum number of duration choices. */
209 return new WP_Error( 'wpbc_setup_wizard_duration_list_limit', sprintf( __( 'Add no more than %d duration choices.', 'booking' ), self::MAX_TIMES_PER_LIST ) );
210 }
211
212 $normalized_durations = array();
213 foreach ( $raw_duration_list as $raw_duration ) {
214 $duration_minutes = $this->duration_to_minutes( $raw_duration );
215 if ( null === $duration_minutes || 0 !== $duration_minutes % self::TIME_INCREMENT ) {
216 return new WP_Error( 'wpbc_setup_wizard_duration_value_invalid', __( 'Use a valid positive five-minute duration for every choice.', 'booking' ) );
217 }
218
219 $normalized_duration = $this->minutes_to_duration( $duration_minutes );
220 if ( in_array( $normalized_duration, $normalized_durations, true ) ) {
221 return new WP_Error( 'wpbc_setup_wizard_duration_value_duplicate', __( 'The same duration cannot appear twice in the list.', 'booking' ) );
222 }
223 $normalized_durations[] = $normalized_duration;
224 }
225
226 if ( $require_complete && empty( $normalized_durations ) ) {
227 return new WP_Error( 'wpbc_setup_wizard_duration_list_required', __( 'Add at least one duration.', 'booking' ) );
228 }
229
230 return $normalized_durations;
231 }
232
233 /**
234 * Convert one exact duration value to minutes.
235 *
236 * @param mixed $raw_duration Untrusted `HH:MM` duration.
237 *
238 * @return int|null Positive duration up to 24 hours, or null when invalid.
239 */
240 private function duration_to_minutes( $raw_duration ) {
241 if ( ! is_scalar( $raw_duration ) || ! preg_match( '/^(?:(?:[01]\d|2[0-3]):[0-5]\d|24:00)$/', (string) $raw_duration ) ) {
242 return null;
243 }
244
245 list( $hour, $minute ) = array_map( 'intval', explode( ':', (string) $raw_duration ) );
246 $duration_minutes = ( $hour * 60 ) + $minute;
247
248 return 0 < $duration_minutes ? $duration_minutes : null;
249 }
250
251 /**
252 * Convert minutes to the Form Builder `HH:MM` duration contract.
253 *
254 * @param int $duration_minutes Positive duration up to 24 hours.
255 *
256 * @return string Normalized duration value.
257 */
258 private function minutes_to_duration( $duration_minutes ) {
259 $duration_minutes = min( 24 * 60, max( self::TIME_INCREMENT, absint( $duration_minutes ) ) );
260
261 return sprintf( '%02d:%02d', (int) floor( $duration_minutes / 60 ), $duration_minutes % 60 );
262 }
263 }
264