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-date-selection / class-wpbc-setup-wizard-date-selection-save-handler.php

class-wpbc-setup-wizard-date-selection-save-handler.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-date-selection/class-wpbc-setup-wizard-date-selection-save-handler.php

291 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Progressive save handler for Setup Wizard Date Selection.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Persist the wizard-owned Date Selection settings through canonical options.
14 *
15 * The handler owns only the options represented by the Date Selection step. It
16 * repeats field and journey validation at the mutation boundary, observes the
17 * established edition split from Calendar settings, verifies the normalized
18 * canonical read model, and restores exact before-images after a failed write.
19 */
20 final class WPBC_Setup_Wizard_Date_Selection_Save_Handler implements WPBC_Setup_Wizard_Step_Save_Handler {
21
22 /** @var WPBC_Setup_Wizard_Date_Selection */
23 private $date_selection;
24
25 /** @var WPBC_Setup_Wizard_Date_Selection_Module */
26 private $date_selection_module;
27
28 /**
29 * Build the canonical writer around the Date Selection domain contract.
30 *
31 * @param WPBC_Setup_Wizard_Date_Selection|null $date_selection Optional domain service for testing.
32 */
33 public function __construct( $date_selection = null ) {
34 $this->date_selection = $date_selection instanceof WPBC_Setup_Wizard_Date_Selection
35 ? $date_selection
36 : new WPBC_Setup_Wizard_Date_Selection();
37 $this->date_selection_module = new WPBC_Setup_Wizard_Date_Selection_Module( $this->date_selection );
38 }
39
40 /**
41 * Return the owned step identifier.
42 *
43 * @return string Stable step identifier.
44 */
45 public function get_step_id() {
46 return 'date_selection';
47 }
48
49 /**
50 * Return the scalar-settings save label.
51 *
52 * @param array<string,mixed> $checkpoint Current checkpoint.
53 * @param array<string,mixed> $step_context Authorized presentation context.
54 *
55 * @return string Translated action label.
56 */
57 public function get_primary_action_label( array $checkpoint, array $step_context = array() ) {
58 unset( $checkpoint, $step_context );
59
60 return __( 'Save & continue', 'booking' );
61 }
62
63 /**
64 * Revalidate, save, and verify canonical Date Selection options.
65 *
66 * @param array<string,mixed> $validated_fields Validated current-step fields.
67 * @param array<string,mixed> $checkpoint Current checkpoint.
68 * @param array<string,mixed> $operation_context Stable operation metadata.
69 *
70 * @return array<string,mixed>|WP_Error Verified result or bounded error.
71 */
72 public function save( array $validated_fields, array $checkpoint, array $operation_context ) {
73 unset( $operation_context );
74
75 if ( ! WPBC_Setup_Wizard_Access::current_user_can_access() ) {
76 return new WP_Error( 'wpbc_setup_wizard_date_selection_forbidden', __( 'You are not allowed to change Booking Calendar Date Selection settings.', 'booking' ) );
77 }
78
79 $journey_id = $this->get_customer_journey_id( $checkpoint );
80 if ( '' === $journey_id ) {
81 return new WP_Error( 'wpbc_setup_wizard_date_selection_journey_missing', __( 'Choose and save a Customer Journey before saving Date Selection.', 'booking' ) );
82 }
83
84 $validated_fields = $this->revalidate_fields( $validated_fields, $journey_id );
85 if ( is_wp_error( $validated_fields ) ) {
86 return $validated_fields;
87 }
88
89 $previous_day_mode = (string) get_bk_option( 'booking_type_of_day_selections' );
90 $canonical_values = $this->get_writable_canonical_values( $validated_fields, $journey_id, $previous_day_mode );
91 $missing_marker = 'wpbc_setup_wizard_missing_' . wp_generate_uuid4();
92 $before_images = array();
93 $attempted = array();
94 $updated_ids = array();
95
96 foreach ( $canonical_values as $option_name => $canonical_value ) {
97 $before_value = get_bk_option( $option_name, $missing_marker );
98 $before_images[ $option_name ] = array(
99 'exists' => $missing_marker !== $before_value,
100 'value' => $before_value,
101 );
102
103 if ( $before_images[ $option_name ]['exists'] && (string) $before_value === (string) $canonical_value ) {
104 continue;
105 }
106
107 $attempted[] = $option_name;
108 update_bk_option( $option_name, $canonical_value );
109 if ( (string) $canonical_value !== (string) get_bk_option( $option_name, $missing_marker ) ) {
110 return $this->create_failed_save_error( $attempted, $before_images, $missing_marker );
111 }
112
113 $updated_ids[] = $option_name;
114 }
115
116 $verified_values = $this->get_writable_canonical_values( $this->date_selection->get_initial_values(), $journey_id, $previous_day_mode );
117 if ( $canonical_values !== $verified_values ) {
118 return $this->create_failed_save_error( $attempted, $before_images, $missing_marker );
119 }
120
121 return array(
122 'status' => 'saved',
123 'summary' => empty( $updated_ids )
124 ? __( 'Date Selection settings already matched this setup.', 'booking' )
125 : __( 'Date Selection settings saved.', 'booking' ),
126 'canonical_fingerprint' => hash( 'sha256', wp_json_encode( $canonical_values ) ),
127 'created_ids' => array(),
128 'updated_ids' => $updated_ids,
129 'warnings' => array(),
130 'writes_canonical_settings' => true,
131 );
132 }
133
134 /**
135 * Revalidate the complete Date Selection contract at apply time.
136 *
137 * @param array<string,mixed> $submitted_fields Submitted normalized fields.
138 * @param string $journey_id Validated Customer Journey ID.
139 *
140 * @return array<string,mixed>|WP_Error Revalidated values or error.
141 */
142 private function revalidate_fields( array $submitted_fields, $journey_id ) {
143 $required_fields = $this->date_selection_module->get_required_fields();
144 $validated_fields = array();
145
146 foreach ( $this->date_selection_module->get_field_names() as $field_id ) {
147 if ( ! array_key_exists( $field_id, $submitted_fields ) ) {
148 return new WP_Error( 'wpbc_setup_wizard_date_selection_field_missing', __( 'Date Selection settings are incomplete. Reload this step and try again.', 'booking' ) );
149 }
150
151 $field_value = $this->date_selection_module->validate_field(
152 $field_id,
153 $submitted_fields[ $field_id ],
154 in_array( $field_id, $required_fields, true )
155 );
156 if ( is_wp_error( $field_value ) ) {
157 return $field_value;
158 }
159
160 $validated_fields[ $field_id ] = $field_value;
161 }
162
163 $validated_fields = $this->date_selection_module->validate_values( $validated_fields );
164 if ( is_wp_error( $validated_fields ) ) {
165 return $validated_fields;
166 }
167
168 return $this->date_selection_module->validate_values_with_context(
169 $validated_fields,
170 array(
171 'customer_journey' => array(
172 'customer_journey' => $journey_id,
173 ),
174 )
175 );
176 }
177
178 /**
179 * Return only canonical options writable by the current edition.
180 *
181 * Free and Personal editions retain the same free-option boundary as the
182 * Calendar settings page. A newly selected two-click range also records its
183 * dynamic subtype so a later edition upgrade preserves the intended mode.
184 *
185 * @param array<string,mixed> $field_values Date Selection values.
186 * @param string $journey_id Validated Customer Journey ID.
187 * @param string $previous_day_mode Day mode captured before this operation.
188 *
189 * @return array<string,string> Edition-supported canonical option values.
190 */
191 private function get_writable_canonical_values( array $field_values, $journey_id, $previous_day_mode ) {
192 $canonical_values = $this->date_selection->get_canonical_option_values( $field_values, $journey_id );
193 $free_option_names = array(
194 'booking_type_of_day_selections',
195 'booking_recurrent_time',
196 'booking_is_show_legend',
197 'booking_legend_is_show_numbers',
198 'booking_legend_is_vertical',
199 );
200
201 foreach ( $this->date_selection->get_legend_items() as $legend_item ) {
202 $legend_id = $legend_item['id'];
203 $free_option_names[] = 'booking_legend_is_show_item_' . $legend_id;
204 $free_option_names[] = 'booking_legend_text_for_item_' . $legend_id;
205 }
206
207 if ( $this->date_selection->supports_advanced_range_rules() ) {
208 return $canonical_values;
209 }
210
211 $writable_values = array_intersect_key( $canonical_values, array_fill_keys( $free_option_names, true ) );
212 if (
213 'range' === $canonical_values['booking_type_of_day_selections']
214 && 'range' !== (string) $previous_day_mode
215 ) {
216 $writable_values['booking_range_selection_type'] = 'dynamic';
217 }
218
219 return $writable_values;
220 }
221
222 /**
223 * Read the previously saved Customer Journey from the checkpoint.
224 *
225 * @param array<string,mixed> $checkpoint Current normalized checkpoint.
226 *
227 * @return string Sanitized journey ID or an empty string.
228 */
229 private function get_customer_journey_id( array $checkpoint ) {
230 $journey_value = isset( $checkpoint['values']['customer_journey']['customer_journey'] )
231 ? $checkpoint['values']['customer_journey']['customer_journey']
232 : '';
233
234 return is_scalar( $journey_value ) ? sanitize_key( (string) $journey_value ) : '';
235 }
236
237 /**
238 * Restore attempted writes and return one controlled persistence error.
239 *
240 * @param string[] $attempted Attempted option names.
241 * @param array<string,array<string,mixed>> $before_images Exact option before-images.
242 * @param string $missing_marker Unique missing-value marker.
243 *
244 * @return WP_Error Controlled save failure.
245 */
246 private function create_failed_save_error( array $attempted, array $before_images, $missing_marker ) {
247 $compensated = $this->restore_before_images( $attempted, $before_images, $missing_marker );
248
249 return new WP_Error(
250 'wpbc_setup_wizard_date_selection_not_saved',
251 $compensated
252 ? __( 'Booking Calendar could not save Date Selection settings. Previous values were restored.', 'booking' )
253 : __( 'Booking Calendar could not save Date Selection settings, and some previous values could not be restored. Review Calendar settings before continuing.', 'booking' )
254 );
255 }
256
257 /**
258 * Restore attempted option writes from exact before-images.
259 *
260 * @param string[] $attempted Attempted option names in write order.
261 * @param array<string,array<string,mixed>> $before_images Exact option before-images.
262 * @param string $missing_marker Unique missing-value marker.
263 *
264 * @return bool True when every attempted option matches its before-image.
265 */
266 private function restore_before_images( array $attempted, array $before_images, $missing_marker ) {
267 $restored = true;
268 foreach ( array_reverse( $attempted ) as $option_name ) {
269 $before_image = $before_images[ $option_name ];
270 if ( $before_image['exists'] ) {
271 update_bk_option( $option_name, $before_image['value'] );
272 $restored = $restored && (string) $before_image['value'] === (string) get_bk_option( $option_name, $missing_marker );
273 } else {
274 delete_bk_option( $option_name );
275 $restored = $restored && $missing_marker === get_bk_option( $option_name, $missing_marker );
276 }
277 }
278
279 return $restored;
280 }
281
282 /**
283 * Date Selection does not complete the route.
284 *
285 * @return bool False.
286 */
287 public function is_terminal() {
288 return false;
289 }
290 }
291