OptionPages   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 400
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 0
loc 400
rs 8.64
c 0
b 0
f 0
wmc 47
lcom 1
cbo 4

19 Methods

Rating   Name   Duplication   Size   Complexity  
A setup() 0 25 1
A init() 0 8 2
A get() 0 25 5
A addComponentData() 0 10 1
A addComponentSubPages() 0 10 2
A addCustomPostTypeSubPages() 0 8 2
A addFeatureSubPages() 0 6 2
B createOptionPages() 0 43 6
A createSubPage() 0 17 3
A addOptionSubPage() 0 27 3
A addFieldGroupToSubPage() 0 22 1
A checkFeature() 0 17 3
A prefixFields() 0 7 1
A checkRequiredHooks() 0 10 3
A getOptionFields() 0 25 3
A getCachedOptionFields() 0 16 3
A collectOptionsWithPrefix() 0 13 3
A combineArrayDefaults() 0 6 2
A getDefaultAcfLanguage() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like OptionPages often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OptionPages, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
// TODO adjust readme
4
// TODO [minor] Overview Page + setting for redirect
5
// TODO [minor] add custom post type label
6
// TODO [minor] add notice for meta keys that are too long (unsolved ACF / WordPress issue)
7
// TODO [minor] remove / don't create empty (parent) option pages
8
9
namespace Flynt\Features\Acf;
10
11
use ACFComposer;
12
use Flynt\ComponentManager;
13
use Flynt\Features\AdminNotices\AdminNoticeManager;
14
use Flynt\Features\CustomPostTypes\CustomPostTypeRegister;
15
use Flynt\Utils\Feature;
16
use Flynt\Utils\FileLoader;
17
use Flynt\Utils\StringHelpers;
18
19
class OptionPages
20
{
21
    const FIELD_GROUPS_DIR = '/config/fieldGroups';
22
23
    const OPTION_TYPES = [
24
        'translatableOptions' => [
25
            'title' => 'Translatable Options',
26
            'icon' => 'dashicons-translation',
27
            'translatable' => true
28
        ],
29
        'globalOptions' => [
30
            'title' => 'Global Options',
31
            'icon' => 'dashicons-admin-site',
32
            'translatable' => false
33
        ]
34
    ];
35
36
    const OPTION_CATEGORIES = [
37
        'component' => [
38
            'title' => 'Component',
39
            'icon' => 'dashicons-editor-table',
40
            'showType' => true
41
        ],
42
        'customPostType' => [
43
            'title' => 'Custom Post Type',
44
            'icon' => 'dashicons-palmtree',
45
            'showType' => true
46
            // 'label' => [ 'labels', 'menu_item' ], // TODO add this functionality
47
        ],
48
        'feature' => [
49
            'title' => 'Feature',
50
            'icon' => 'dashicons-carrot',
51
            'showType' => true
52
        ]
53
    ];
54
55
    const FILTER_NAMESPACES = [
56
        'component' => 'Flynt/Components',
57
        'customPostType' => 'Flynt/CustomPostTypes',
58
        'feature' => 'Flynt/Features'
59
    ];
60
61
    protected static $optionPages = [];
62
    protected static $optionTypes = [];
63
    protected static $optionCategories = [];
64
65
    public static function setup()
66
    {
67
68
        self::$optionTypes = self::OPTION_TYPES;
69
        self::$optionCategories = self::OPTION_CATEGORIES;
70
71
        self::createOptionPages();
72
73
        // Register Categories
74
        add_action('acf/init', function () {
75
            self::addComponentSubPages();
76
            self::addFeatureSubPages();
77
            self::addCustomPostTypeSubPages();
78
        });
79
80
        add_filter(
81
            'Flynt/addComponentData',
82
            ['Flynt\Features\Acf\OptionPages', 'addComponentData'],
83
            10,
84
            3
85
        );
86
87
        // Setup Flynt Non Persistent Cache
88
        wp_cache_add_non_persistent_groups('flynt');
89
    }
90
91
    public static function init()
92
    {
93
        // show (dismissible) Admin Notice if required feature is missing
94
        if (class_exists('Flynt\Features\AdminNotices\AdminNoticeManager')) {
95
            self::checkFeature('customPostType', 'flynt-custom-post-types');
96
            self::checkFeature('component', 'flynt-components');
97
        }
98
    }
99
100
    // ============
101
    // PUBLIC API
102
    // ============
103
104
    /**
105
     * Get option(s) from a sub page.
106
     *
107
     * Returns an option of a sub page. If no field name is provided it will get all option of that sub page.
108
     * Parameters are expected to be camelCase.
109
     *
110
     * @since 0.2.0 introduced as a replacement for OptionPages::getOption and OptionPages::getOptions
111
     * @since 0.2.2 added check for required hooks to have run to alert of timing issues when used incorrectly
112
     *
113
     * @param string $optionType Type of option page. Either globalOptions or translatableOptions.
114
     * @param string $optionCategory Category of option page. One of these three values: component, feature, customPostType.
115
     * @param string $subPageName Name of the sub page.
116
     * @param string $fieldName (optional) Name of the field to get.
117
     * @return mixed The value of the option or array of options. False if subpage doesn't exist or no option was found.
118
     **/
119
    public static function get($optionType, $optionCategory, $subPageName, $fieldName = null)
120
    {
121
        if (!self::checkRequiredHooks($optionType, $optionCategory, $subPageName, $fieldName)) {
122
            return false;
123
        }
124
125
        // convert parameters
126
        $optionType = lcfirst($optionType);
127
        $optionCategory = ucfirst($optionCategory);
128
        $subPageName = ucfirst($subPageName);
129
130
        if (!isset(self::$optionTypes[$optionType])) {
131
            return false;
132
        }
133
134
        $prefix = implode('', [$optionType, $optionCategory, $subPageName, '_']);
135
        $options = self::getOptionFields(self::$optionTypes[$optionType]['translatable']);
136
        $options = self::collectOptionsWithPrefix($options, $prefix);
137
138
        if (isset($fieldName)) {
139
            $fieldName = lcfirst($fieldName);
140
            return array_key_exists($fieldName, $options) ? $options[$fieldName] : false;
141
        }
142
        return $options;
143
    }
144
145
    // ============
146
    // COMPONENTS
147
    // ============
148
149
    public static function addComponentData($data, $parentData, $config)
150
    {
151
        // get fields for this component
152
        $options = array_reduce(array_keys(self::$optionTypes), function ($carry, $optionType) use ($config) {
153
            return array_merge($carry, self::get($optionType, 'Component', $config['name']));
154
        }, []);
155
156
        // don't overwrite existing data
157
        return array_merge($options, $data);
158
    }
159
160
    public static function addComponentSubPages()
161
    {
162
        // load fields.json if it exists
163
        $componentManager = ComponentManager::getInstance();
164
        $components = $componentManager->getAll();
165
166
        foreach ($components as $name => $path) {
167
            self::createSubPage('component', $name);
168
        }
169
    }
170
171
    // ==================
172
    // CUSTOM POST TYPES
173
    // ==================
174
175
    public static function addCustomPostTypeSubPages()
176
    {
177
        $customPostTypes = CustomPostTypeRegister::getAll();
178
179
        foreach ($customPostTypes as $name => $config) {
180
            self::createSubPage('customPostType', $name);
181
        }
182
    }
183
184
    // ========
185
    // FEATURES
186
    // ========
187
188
    public static function addFeatureSubPages()
189
    {
190
        foreach (Feature::getFeatures() as $handle => $config) {
191
            self::createSubPage('feature', $config['name']);
192
        }
193
    }
194
195
    // ========
196
    // GENERAL
197
    // ========
198
199
    protected static function createOptionPages()
200
    {
201
202
        foreach (self::$optionTypes as $optionType => $option) {
203
            $title = _x($option['title'], 'title', 'flynt-starter-theme');
204
            $slug = ucfirst($optionType);
205
206
            acf_add_options_page([
207
                'page_title'  => $title,
208
                'menu_title'  => $title,
209
                'redirect'    => true,
210
                'menu_slug'   => $slug,
211
                'icon_url'    => $option['icon']
212
            ]);
213
214
            self::$optionPages[$optionType] = [
215
                'menu_slug' => $slug,
216
                'menu_title' => $title
217
            ];
218
        }
219
220
        add_action('current_screen', function ($currentScreen) {
221
            foreach (self::$optionTypes as $optionType => $option) {
222
                $isTranslatable = $option['translatable'];
223
                $toplevelPageId = 'toplevel_page_' . $optionType;
224
                $menuTitle = self::$optionPages[$optionType]['menu_title'];
225
                $subPageId = sanitize_title($menuTitle) . '_page_' . $optionType;
226
                $isCurrentPage = StringHelpers::startsWith($toplevelPageId, $currentScreen->id)
227
                || StringHelpers::startsWith($subPageId, $currentScreen->id);
228
229
                if (!$isTranslatable && $isCurrentPage) {
230
                    // set acf field values to default language
231
                    add_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 101);
232
233
                    // hide language selector in admin bar
234
                    add_action('wp_before_admin_bar_render', function () {
235
                        $adminBar = $GLOBALS['wp_admin_bar'];
236
                        $adminBar->remove_menu('WPML_ALS');
237
                    });
238
                }
239
            }
240
        });
241
    }
242
243
    protected static function createSubPage($type, $name)
244
    {
245
        $namespace = self::FILTER_NAMESPACES[$type];
246
        $name = ucfirst($name);
247
        foreach (self::$optionTypes as $optionType => $option) {
248
            $filterName = "{$namespace}/{$name}/Fields/" . ucfirst($optionType);
249
            $fields = apply_filters($filterName, []);
250
            if (!empty($fields)) {
251
                self::addOptionSubPage(
252
                    $type,
253
                    $name,
254
                    $optionType,
255
                    $fields
256
                );
257
            }
258
        }
259
    }
260
261
    protected static function addOptionSubPage($optionCategoryName, $subPageName, $optionType, $fields)
262
    {
263
        $prettySubPageName = StringHelpers::splitCamelCase($subPageName);
264
        $optionCategorySettings = self::$optionCategories[$optionCategoryName];
265
        $iconClasses = 'flynt-submenu-item dashicons-before ' . $optionCategorySettings['icon'];
266
267
        $appendCategory = '';
268
        if (isset($optionCategorySettings['showType']) && true === $optionCategorySettings['showType']) {
269
            $appendCategory = ' (' . $optionCategorySettings['title'] . ')';
270
        }
271
272
        $subPageConfig = [
273
            'page_title'  => $prettySubPageName . $appendCategory,
274
            'menu_title'  => "<span class='{$iconClasses}'>{$prettySubPageName}</span>",
275
            'parent_slug' => self::$optionPages[$optionType]['menu_slug'],
276
            'menu_slug'   => $optionType . ucfirst($optionCategoryName) . $subPageName
277
        ];
278
279
        acf_add_options_sub_page($subPageConfig);
280
281
        self::addFieldGroupToSubPage(
282
            $subPageConfig['parent_slug'],
283
            $subPageConfig['menu_slug'],
284
            $subPageConfig['menu_title'],
285
            $fields
286
        );
287
    }
288
289
    protected static function addFieldGroupToSubPage($parentMenuSlug, $menuSlug, $prettySubPageName, $fields)
0 ignored issues
show
Unused Code introduced by
The parameter $parentMenuSlug is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
290
    {
291
        $fieldGroup = ACFComposer\ResolveConfig::forFieldGroup(
292
            [
293
                'name' => $menuSlug,
294
                'title' => $prettySubPageName,
295
                'fields' => $fields,
296
                'style' => 'seamless',
297
                'location' => [
298
                    [
299
                        [
300
                            'param' => 'options_page',
301
                            'operator' => '==',
302
                            'value' => $menuSlug
303
                        ]
304
                    ]
305
                ]
306
            ]
307
        );
308
        $fieldGroup['fields'] = self::prefixFields($fieldGroup['fields'], $menuSlug);
309
        acf_add_local_field_group($fieldGroup);
310
    }
311
312
    protected static function checkFeature($optionCategory, $feature)
313
    {
314
        if (array_key_exists($optionCategory, self::$optionCategories) && !Feature::isRegistered($feature)) {
315
            $title = self::$optionCategories[$optionCategory]['title'];
316
            $noticeManager = AdminNoticeManager::getInstance();
317
            $noticeManager->addNotice(
318
                [
319
                    "Could not add Option Pages for {$title} because the \"{$feature}\" feature is missing."
320
                ],
321
                [
322
                    'title' => 'Acf Option Pages Feature',
323
                    'dismissible' => true,
324
                    'type' => 'info'
325
                ]
326
            );
327
        }
328
    }
329
330
    protected static function prefixFields($fields, $prefix)
331
    {
332
        return array_map(function ($field) use ($prefix) {
333
            $field['name'] = $prefix . '_' . $field['name'];
334
            return $field;
335
        }, $fields);
336
    }
337
338
    protected static function checkRequiredHooks($optionType, $optionCategory, $subPageName, $fieldName)
339
    {
340
        if (did_action('acf/init') < 1) {
341
            $parameters = "${optionType}, ${optionCategory}, ${subPageName}, ";
342
            $parameters .= isset($fieldName) ? $fieldName : 'NULL';
343
            trigger_error("Could not get option/s for [${parameters}]. Required hooks have not yet been executed! Please make sure to run `OptionPages::get()` after the `acf/init` action is finished.", E_USER_WARNING);
344
            return false;
345
        }
346
        return true;
347
    }
348
349
    protected static function getOptionFields($translatable)
350
    {
351
        global $sitepress;
352
353
        if (!isset($sitepress)) {
354
            $options = self::getCachedOptionFields();
355
        } else if ($translatable) {
356
            // get options from cache with language namespace
357
            $options = self::getCachedOptionFields(ICL_LANGUAGE_CODE);
358
        } else {
359
            // switch to default language to get global options
360
            $sitepress->switch_lang(acf_get_setting('default_language'));
361
362
            add_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 100);
363
364
            // get optios from cache with global namespace
365
            $options = self::getCachedOptionFields('global');
366
367
            remove_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 100);
368
369
            $sitepress->switch_lang(ICL_LANGUAGE_CODE);
370
        }
371
372
        return $options;
373
    }
374
375
    protected static function getCachedOptionFields($namespace = '')
376
    {
377
        // get cached options
378
        $found = false;
379
        $suffix = !empty($namespace) ? "_${namespace}" : '';
380
        $cacheKey = "Features/Acf/OptionPages/ID=options${suffix}";
381
382
        $options = wp_cache_get($cacheKey, 'flynt', null, $found);
383
384
        if (!$found) {
385
            $options = get_fields('options');
386
            wp_cache_set($cacheKey, $options, 'flynt');
387
        }
388
389
        return $options;
390
    }
391
392
    // find and replace relevant keys, then return an array of all options for this Sub-Page
393
    protected static function collectOptionsWithPrefix($options, $prefix)
394
    {
395
        $optionKeys = is_array($options) ? array_keys($options) : [];
396
        return array_reduce($optionKeys, function ($carry, $key) use ($options, $prefix) {
397
            $count = 0;
398
            $option = $options[$key];
399
            $key = str_replace($prefix, '', $key, $count);
400
            if ($count > 0) {
401
                $carry[$key] = $option;
402
            }
403
            return $carry;
404
        }, []);
405
    }
406
407
    protected static function combineArrayDefaults(array $array, array $defaults)
408
    {
409
        return array_map(function ($value) use ($defaults) {
410
            return is_array($value) ? array_merge($defaults, $value) : [];
411
        }, $array);
412
    }
413
414
    public static function getDefaultAcfLanguage()
415
    {
416
        return acf_get_setting('default_language');
417
    }
418
}
419