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