PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 All 162 releases
woocommerce-pos / includes / Services / Settings / Abstract_Section.php

Abstract_Section.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.18, at includes/Services/Settings/Abstract_Section.php

278 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abstract option-backed Settings Section.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\Services\Settings;
9
10 use WCPOS\WooCommercePOS\Interfaces\Settings_Section_Interface;
11 use WP_Error;
12
13 /**
14 * Base class for option-backed Settings Sections.
15 *
16 * Owns the read/write template: read = option + migrate + defaults merge +
17 * compose + filter + redact (pure, no DB writes); write = sanitize + stamp +
18 * pre_save filter + update_option + saved action. Sections override the
19 * protected hooks; sections with bespoke behaviour (access, license,
20 * cloud_print) override read()/write() wholesale.
21 */
22 abstract class Abstract_Section implements Settings_Section_Interface {
23 /**
24 * Prefix for the wp_options table, identical to the legacy
25 * Services\Settings::$db_prefix. Persisted option names are frozen
26 * public interface — never change this.
27 *
28 * @var string
29 */
30 const DB_PREFIX = 'woocommerce_pos_settings_';
31
32 /**
33 * Section id.
34 *
35 * @return string
36 */
37 abstract public function id(): string;
38
39 /**
40 * The full default shape for this section.
41 *
42 * @return array
43 */
44 abstract public function defaults(): array;
45
46 /**
47 * Migrate legacy stored shapes, in memory only. Runs on the raw option
48 * value BEFORE defaults are merged. Must be idempotent and must not
49 * write to the database.
50 *
51 * @param array $raw Raw option value.
52 *
53 * @return array
54 */
55 protected function migrate( array $raw ): array {
56 return $raw;
57 }
58
59 /**
60 * Sanitize settings before persisting.
61 *
62 * @param array $settings Settings about to be saved.
63 *
64 * @return array
65 */
66 protected function sanitize( array $settings ): array {
67 return $settings;
68 }
69
70 /**
71 * Append computed, read-only view fields (e.g. resolved fallbacks) after
72 * defaults merge and before the section filter.
73 *
74 * @param array $settings Merged settings.
75 *
76 * @return array
77 */
78 protected function compose( array $settings ): array {
79 return $settings;
80 }
81
82 /**
83 * Strip secrets from the public view. Runs last in read().
84 *
85 * @param array $settings Filtered settings.
86 *
87 * @return array
88 */
89 protected function redact( array $settings ): array {
90 return $settings;
91 }
92
93 /**
94 * Default PATCH merge for REST updates.
95 *
96 * @param array $existing Existing settings view.
97 * @param array $patch Incoming partial payload.
98 *
99 * @return array
100 */
101 public function merge( array $existing, array $patch ): array {
102 return array_replace_recursive( $existing, $patch );
103 }
104
105 /**
106 * REST endpoint args. Default: none.
107 *
108 * @return array
109 */
110 public function endpoint_args(): array {
111 return array();
112 }
113
114 /**
115 * The wp_options key backing this section.
116 *
117 * @return string
118 */
119 protected function option_name(): string {
120 return self::DB_PREFIX . $this->id();
121 }
122
123 /**
124 * The wp_options key the upgrade-time autoload flip must target.
125 *
126 * A public accessor rather than a public option_name(): Pro's License
127 * section overrides option_name() at protected visibility (its key is
128 * Pro-prefixed), and a child cannot narrow a public parent method — making
129 * option_name() public fatalled every Pro site (#1846/#1849). Deriving the
130 * key from id() instead flipped the wrong row for that section and seeded a
131 * stray `woocommerce_pos_settings_license` (measured 2026-09-03 on dev-pro:
132 * one query per page for the license row).
133 *
134 * @return string
135 */
136 public function autoload_option_name(): string {
137 return $this->option_name();
138 }
139
140 /**
141 * Whether this section's option rides in alloptions.
142 *
143 * Off by default: byte-compatible with the legacy save path, and most
144 * sections are only read on POS/admin requests. A section that is read on
145 * EVERY request (General, via the Settings service during init) overrides
146 * this, because without an object cache a non-autoloaded option costs one
147 * query per page load (measured 2026-09-03 on dev-next). Declaring it here
148 * is the whole contract: write() honours it for new writes, and
149 * Activator::autoload_request_latches() flips existing rows on upgrade and
150 * reactivation for every registered section that returns true — core's
151 * update_option() never flips autoload on an unchanged value, so the
152 * writer alone cannot repair an existing row. Keep it off for sections that
153 * can hold unbounded lists (Visibility's product ids).
154 *
155 * @return bool
156 */
157 public function autoload(): bool {
158 return false;
159 }
160
161 /**
162 * Read the raw option value, coerced to array.
163 *
164 * @return array
165 */
166 protected function read_raw(): array {
167 $raw = get_option( $this->option_name(), array() );
168
169 return \is_array( $raw ) ? $raw : array();
170 }
171
172 /**
173 * Read the section's public view. Pure — never writes to the database.
174 *
175 * @return array
176 */
177 public function read(): array {
178 $settings = $this->migrate( $this->read_raw() );
179
180 foreach ( $this->defaults() as $key => $value ) {
181 if ( ! \array_key_exists( $key, $settings ) ) {
182 $settings[ $key ] = $value;
183 }
184 }
185
186 $settings = $this->compose( $settings );
187
188 /**
189 * Filters a Settings Section's read view.
190 *
191 * The dynamic portion of the hook name, `$this->id()`, refers to the
192 * section id, e.g. 'general' or 'checkout'.
193 *
194 * @since 1.0.0
195 *
196 * @param array $settings The section settings.
197 *
198 * @hook woocommerce_pos_{$id}_settings
199 */
200 $settings = apply_filters( "woocommerce_pos_{$this->id()}_settings", $settings );
201
202 return $this->redact( $settings );
203 }
204
205 /**
206 * Persist a full settings array.
207 *
208 * Behaviour is byte-compatible with the legacy
209 * Services\Settings::save_settings(): sanitize, stamp date_modified_gmt,
210 * apply the pre-save filter, update_option (autoload per {@see autoload()}), detect
211 * unchanged-value no-ops, fire the saved action, return the post-save
212 * read.
213 *
214 * @param array $settings The full settings array to persist.
215 *
216 * @return array|WP_Error
217 */
218 public function write( array $settings ) {
219 $settings = $this->sanitize( $settings );
220
221 $settings = array_merge(
222 $settings,
223 array( 'date_modified_gmt' => current_time( 'mysql', true ) )
224 );
225
226 /**
227 * Filters the settings before they are saved.
228 *
229 * @since 1.4.12
230 *
231 * @param array $settings The settings array about to be saved.
232 * @param string $id The ID of the settings section being saved.
233 *
234 * @hook woocommerce_pos_pre_save_{$id}_settings
235 */
236 $settings = apply_filters( "woocommerce_pos_pre_save_{$this->id()}_settings", $settings, $this->id() );
237
238 $option_name = $this->option_name();
239 $previous_value = get_option( $option_name, null );
240 $success = update_option( $option_name, $settings, $this->autoload() );
241
242 if ( ! $success ) {
243 // update_option() returns false both when the value is unchanged (no DB
244 // write) and on actual failure. Use the value read *before* the write
245 // attempt to avoid a post-write race.
246 $is_noop = null !== $previous_value
247 && maybe_serialize( $previous_value ) === maybe_serialize( $settings );
248
249 if ( ! $is_noop ) {
250 return new WP_Error(
251 'woocommerce_pos_settings_error',
252 // translators: %s: Settings group id, ie: 'general' or 'checkout'.
253 \sprintf( __( 'Can not save settings with id %s', 'woocommerce-pos' ), $this->id() ),
254 array( 'status' => 400 )
255 );
256 }
257 }
258
259 $saved_settings = $this->read();
260
261 if ( $success ) {
262 /**
263 * Fires after settings for a specific section are successfully saved.
264 *
265 * @since 1.4.12
266 *
267 * @param array $saved_settings The settings array that was just saved.
268 * @param string $id The ID of the settings section that was saved.
269 *
270 * @hook woocommerce_pos_saved_{$id}_settings
271 */
272 do_action( "woocommerce_pos_saved_{$this->id()}_settings", $saved_settings, $this->id() );
273 }
274
275 return $saved_settings;
276 }
277 }
278