Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
13.01% |
16 / 123 |
|
25.00% |
2 / 8 |
CRAP | |
0.00% |
0 / 1 |
Sync | |
13.01% |
16 / 123 |
|
25.00% |
2 / 8 |
1040.30 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
init | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
scheduleSyncs | |
46.15% |
6 / 13 |
|
0.00% |
0 / 1 |
8.90 | |||
syncToWordPress | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
90 | |||
updateOptionsFromResponses | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
syncToDashboard | |
0.00% |
0 / 60 |
|
0.00% |
0 / 1 |
182 | |||
shouldSyncOptionToDashboard | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
syncOptionToDashboard | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | /** |
6 | * Setting: Sync |
7 | * |
8 | * @package Beyondwords\Wordpress |
9 | * @author Stuart McAlpine <stu@beyondwords.io> |
10 | * @since 5.0.0 |
11 | */ |
12 | |
13 | namespace Beyondwords\Wordpress\Component\Settings; |
14 | |
15 | use Beyondwords\Wordpress\Core\ApiClient; |
16 | use Beyondwords\Wordpress\Core\Environment; |
17 | use Symfony\Component\PropertyAccess\PropertyAccess; |
18 | |
19 | /** |
20 | * Sync |
21 | * |
22 | * @since 5.0.0 |
23 | * |
24 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) |
25 | * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
26 | * @SuppressWarnings(PHPMD.NPathComplexity) |
27 | */ |
28 | class Sync |
29 | { |
30 | /** |
31 | * Map settings. |
32 | * |
33 | * @since 5.0.0 |
34 | */ |
35 | public const MAP_SETTINGS = [ |
36 | // Player |
37 | 'beyondwords_player_style' => '[player_settings][player_style]', |
38 | 'beyondwords_player_theme' => '[player_settings][theme]', |
39 | 'beyondwords_player_theme_dark' => '[player_settings][dark_theme]', |
40 | 'beyondwords_player_theme_light' => '[player_settings][light_theme]', |
41 | 'beyondwords_player_theme_video' => '[player_settings][video_theme]', |
42 | 'beyondwords_player_call_to_action' => '[player_settings][call_to_action]', |
43 | 'beyondwords_player_widget_style' => '[player_settings][widget_style]', |
44 | 'beyondwords_player_widget_position' => '[player_settings][widget_position]', |
45 | 'beyondwords_player_skip_button_style' => '[player_settings][skip_button_style]', |
46 | 'beyondwords_player_clickable_sections' => '[player_settings][segment_playback_enabled]', |
47 | // Project |
48 | 'beyondwords_project_auto_publish_enabled' => '[project][auto_publish_enabled]', |
49 | 'beyondwords_project_language_code' => '[project][language]', |
50 | 'beyondwords_project_body_voice_id' => '[project][body][voice][id]', |
51 | 'beyondwords_project_body_voice_speaking_rate' => '[project][body][voice][speaking_rate]', |
52 | 'beyondwords_project_title_enabled' => '[project][title][enabled]', |
53 | 'beyondwords_project_title_voice_id' => '[project][title][voice][id]', |
54 | 'beyondwords_project_title_voice_speaking_rate' => '[project][title][voice][speaking_rate]', |
55 | // Video |
56 | 'beyondwords_video_enabled' => '[video_settings][enabled]', |
57 | ]; |
58 | |
59 | /** |
60 | * PropertyAccessor. |
61 | * |
62 | * @var PropertyAccessor |
63 | * |
64 | * @since 5.0.0 |
65 | */ |
66 | public $propertyAccessor; |
67 | |
68 | /** |
69 | * Constructor. |
70 | * |
71 | * @since 5.0.0 |
72 | */ |
73 | public function __construct() |
74 | { |
75 | $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() |
76 | ->disableExceptionOnInvalidPropertyPath() |
77 | ->getPropertyAccessor(); |
78 | } |
79 | |
80 | /** |
81 | * Init. |
82 | * |
83 | * @since 5.0.0 |
84 | */ |
85 | public function init() |
86 | { |
87 | add_action('load-settings_page_beyondwords', array($this, 'syncToWordPress'), 30); |
88 | |
89 | if (Environment::hasAutoSyncSettings()) { |
90 | add_action('load-settings_page_beyondwords', array($this, 'scheduleSyncs'), 20); |
91 | add_action('shutdown', array($this, 'syncToDashboard')); |
92 | } |
93 | } |
94 | |
95 | /** |
96 | * Should we schedule a sync on the current settings tab? |
97 | * |
98 | * @since 5.0.0 |
99 | * @since 5.2.0 Remove API creds validation. |
100 | * |
101 | * @return void |
102 | */ |
103 | public function scheduleSyncs() |
104 | { |
105 | $tab = Settings::getActiveTab(); |
106 | $endpoints = []; |
107 | |
108 | switch ($tab) { |
109 | case 'content': |
110 | $endpoints = ['project']; |
111 | break; |
112 | case 'voices': |
113 | $endpoints = ['project']; |
114 | break; |
115 | case 'player': |
116 | $endpoints = ['player_settings', 'video_settings']; |
117 | break; |
118 | } |
119 | |
120 | if (count($endpoints)) { |
121 | wp_cache_set('beyondwords_sync_to_wordpress', $endpoints, 'beyondwords', 60); |
122 | } |
123 | } |
124 | |
125 | /** |
126 | * Sync from the dashboard/BeyondWords REST API to WordPress. |
127 | * |
128 | * @since 5.0.0 Introduced. |
129 | * @since 5.4.0 Stop saving language ID – we only need the ISO code now. |
130 | * |
131 | * @return void |
132 | **/ |
133 | public function syncToWordPress() |
134 | { |
135 | $sync_to_wordpress = wp_cache_get('beyondwords_sync_to_wordpress', 'beyondwords'); |
136 | wp_cache_delete('beyondwords_sync_to_wordpress', 'beyondwords'); |
137 | |
138 | if (empty($sync_to_wordpress) || ! is_array($sync_to_wordpress)) { |
139 | return; |
140 | } |
141 | |
142 | $responses = []; |
143 | |
144 | if (! empty(array_intersect($sync_to_wordpress, ['all', 'project']))) { |
145 | $project = ApiClient::getProject(); |
146 | if (! empty($project)) { |
147 | $responses['project'] = $project; |
148 | } |
149 | } |
150 | |
151 | if (! empty(array_intersect($sync_to_wordpress, ['all', 'player_settings']))) { |
152 | $player_settings = ApiClient::getPlayerSettings(); |
153 | if (! empty($player_settings)) { |
154 | $responses['player_settings'] = $player_settings; |
155 | } |
156 | } |
157 | |
158 | if (! empty(array_intersect($sync_to_wordpress, ['all', 'video_settings']))) { |
159 | $video_settings = ApiClient::getVideoSettings(); |
160 | if (! empty($video_settings)) { |
161 | $responses['video_settings'] = $video_settings; |
162 | } |
163 | } |
164 | |
165 | // Update WordPress options using the REST API response data. |
166 | $this->updateOptionsFromResponses($responses); |
167 | } |
168 | |
169 | /** |
170 | * Update WordPress options from REST API responses. |
171 | * |
172 | * @since 5.0.0 |
173 | * |
174 | * @return boolean |
175 | **/ |
176 | public function updateOptionsFromResponses($responses) |
177 | { |
178 | if (empty($responses)) { |
179 | add_settings_error( |
180 | 'beyondwords_settings', |
181 | 'beyondwords_settings', |
182 | '<span class="dashicons dashicons-controls-volumeon"></span> Unexpected BeyondWords REST API response.', |
183 | 'error' |
184 | ); |
185 | return false; |
186 | } |
187 | |
188 | $updated = false; |
189 | |
190 | foreach (self::MAP_SETTINGS as $optionName => $path) { |
191 | $value = $this->propertyAccessor->getValue($responses, $path); |
192 | |
193 | if ($value !== null) { |
194 | update_option($optionName, $value, false); |
195 | $updated = true; |
196 | } |
197 | } |
198 | |
199 | return $updated; |
200 | } |
201 | |
202 | /** |
203 | * Sync from WordPress to the dashboard/BeyondWords REST API. |
204 | * |
205 | * @since 5.0.0 |
206 | * |
207 | * @return void |
208 | **/ |
209 | public function syncToDashboard() |
210 | { |
211 | $options = wp_cache_get('beyondwords_sync_to_dashboard', 'beyondwords'); |
212 | wp_cache_delete('beyondwords_sync_to_dashboard', 'beyondwords'); |
213 | |
214 | if (empty($options) || ! is_array($options)) { |
215 | return; |
216 | } |
217 | |
218 | $settings = []; |
219 | |
220 | foreach ($options as $option) { |
221 | if ($this->shouldSyncOptionToDashboard($option)) { |
222 | $this->propertyAccessor->setValue( |
223 | $settings, |
224 | self::MAP_SETTINGS[$option], |
225 | get_option($option) |
226 | ); |
227 | } |
228 | } |
229 | |
230 | // Sync player settings back to API |
231 | if (isset($settings['player_settings'])) { |
232 | ApiClient::updatePlayerSettings($settings['player_settings']); |
233 | |
234 | add_settings_error( |
235 | 'beyondwords_settings', |
236 | 'beyondwords_settings', |
237 | '<span class="dashicons dashicons-rest-api"></span> Player settings synced from WordPress to the BeyondWords dashboard.', // phpcs:ignore Generic.Files.LineLength.TooLong |
238 | 'success' |
239 | ); |
240 | } |
241 | |
242 | // Sync title voice back to API |
243 | if (in_array('beyondwords_project_title_voice_speaking_rate', $options)) { |
244 | $value = $this->propertyAccessor->getValue( |
245 | $settings, |
246 | self::MAP_SETTINGS['beyondwords_project_title_voice_speaking_rate'] |
247 | ); |
248 | |
249 | if ($value !== null) { |
250 | $titleVoiceId = get_option('beyondwords_project_title_voice_id'); |
251 | ApiClient::updateVoice($titleVoiceId, [ |
252 | 'speaking_rate' => (int)$value, |
253 | ]); |
254 | } |
255 | } |
256 | |
257 | // Sync body voice back to API |
258 | if (in_array('beyondwords_project_body_voice_speaking_rate', $options)) { |
259 | $value = $this->propertyAccessor->getValue( |
260 | $settings, |
261 | self::MAP_SETTINGS['beyondwords_project_body_voice_speaking_rate'] |
262 | ); |
263 | |
264 | if ($value !== null) { |
265 | $bodyVoiceId = get_option('beyondwords_project_body_voice_id'); |
266 | ApiClient::updateVoice($bodyVoiceId, [ |
267 | 'speaking_rate' => (int)$value, |
268 | ]); |
269 | } |
270 | } |
271 | |
272 | // Sync project settings back to API |
273 | if (isset($settings['project'])) { |
274 | // Don't send speaking rates back to /project endpoint |
275 | $titleSpeakingRate = $this->propertyAccessor->getValue( |
276 | $settings, |
277 | self::MAP_SETTINGS['beyondwords_project_title_voice_speaking_rate'] |
278 | ); |
279 | if ($titleSpeakingRate) { |
280 | unset($settings['project']['title']['voice']['speaking_rate']); |
281 | } |
282 | |
283 | // Don't send speaking rates back to /project endpoint |
284 | $bodySpeakingRate = $this->propertyAccessor->getValue( |
285 | $settings, |
286 | self::MAP_SETTINGS['beyondwords_project_body_voice_speaking_rate'] |
287 | ); |
288 | if ($bodySpeakingRate) { |
289 | unset($settings['project']['body']['voice']['speaking_rate']); |
290 | } |
291 | |
292 | ApiClient::updateProject($settings['project']); |
293 | |
294 | add_settings_error( |
295 | 'beyondwords_settings', |
296 | 'beyondwords_settings', |
297 | '<span class="dashicons dashicons-rest-api"></span> Project settings synced from WordPress to the BeyondWords dashboard.', // phpcs:ignore Generic.Files.LineLength.TooLong |
298 | 'success' |
299 | ); |
300 | } |
301 | } |
302 | |
303 | /** |
304 | * Should we sync this option to the dashboard? |
305 | * |
306 | * @since 5.0.0 |
307 | * |
308 | * @param string $option_name Option name. |
309 | * |
310 | * @return void |
311 | **/ |
312 | public function shouldSyncOptionToDashboard($option_name) |
313 | { |
314 | if (! array_key_exists($option_name, self::MAP_SETTINGS)) { |
315 | return false; |
316 | } |
317 | |
318 | // Check the option was updated without error |
319 | $hasErrors = get_settings_errors($option_name); |
320 | |
321 | return is_array($hasErrors) && count($hasErrors) === 0; |
322 | } |
323 | |
324 | /** |
325 | * Sync an option to the WordPress dashboard. |
326 | * |
327 | * Note that this DOES NOT make the API call, it instead flags the field |
328 | * as one to sync so that we can group fields and send them in a single |
329 | * request to the BeyondWords REST API. |
330 | * |
331 | * @since 5.0.0 |
332 | * |
333 | * @return void |
334 | **/ |
335 | public static function syncOptionToDashboard($optionName) |
336 | { |
337 | $options = wp_cache_get('beyondwords_sync_to_dashboard', 'beyondwords'); |
338 | |
339 | if (! is_array($options)) { |
340 | $options = []; |
341 | } |
342 | |
343 | $options[] = $optionName; |
344 | $options = array_unique($options); |
345 | |
346 | wp_cache_set('beyondwords_sync_to_dashboard', $options, 'beyondwords', 60); |
347 | } |
348 | } |