Completed
Push — acf-option-pages-check-hooks ( 6ee41b...98b683 )
by Doğa
03:14
created

OptionPages::get()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 5
nop 4
dl 0
loc 25
rs 8.439
c 0
b 0
f 0
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
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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 %%NEXT_VERSION%% 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
    protected static function checkRequiredHooks($optionType, $optionCategory, $subPageName, $fieldName)
146
    {
147
        if (did_action('acf/init') < 1) {
148
            $parameters = "${optionType}, ${optionCategory}, ${subPageName}, ";
149
            $parameters .= isset($fieldName) ? $fieldName : 'NULL';
150
            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);
151
            return false;
152
        }
153
        return true;
154
    }
155
156
    // find and replace relevant keys, then return an array of all options for this Sub-Page
157
    protected static function collectOptionsWithPrefix($options, $prefix)
158
    {
159
        $optionKeys = is_array($options) ? array_keys($options) : [];
160
        return array_reduce($optionKeys, function ($carry, $key) use ($options, $prefix) {
161
            $count = 0;
162
            $option = $options[$key];
163
            $key = str_replace($prefix, '', $key, $count);
164
            if ($count > 0) {
165
                $carry[$key] = $option;
166
            }
167
            return $carry;
168
        }, []);
169
    }
170
171
    // ============
172
    // COMPONENTS
173
    // ============
174
175
    public static function addComponentData($data, $parentData, $config)
176
    {
177
        // get fields for this component
178
        $options = array_reduce(array_keys(self::$optionTypes), function ($carry, $optionType) use ($config) {
179
            return array_merge($carry, self::get($optionType, 'Component', $config['name']));
180
        }, []);
181
182
        // don't overwrite existing data
183
        return array_merge($options, $data);
184
    }
185
186
    public static function addComponentSubPages()
187
    {
188
        // load fields.json if it exists
189
        $componentManager = ComponentManager::getInstance();
190
        $components = $componentManager->getAll();
191
192
        foreach ($components as $name => $path) {
193
            self::createSubPage('component', $name);
194
        }
195
    }
196
197
    // ==================
198
    // CUSTOM POST TYPES
199
    // ==================
200
201
    public static function addCustomPostTypeSubPages()
202
    {
203
        $customPostTypes = CustomPostTypeRegister::getAll();
204
205
        foreach ($customPostTypes as $name => $config) {
206
            self::createSubPage('customPostType', $name);
207
        }
208
    }
209
210
    // ========
211
    // FEATURES
212
    // ========
213
214
    public static function addFeatureSubPages()
215
    {
216
        foreach (Feature::getFeatures() as $handle => $config) {
217
            self::createSubPage('feature', $config['name']);
218
        }
219
    }
220
221
    // ========
222
    // GENERAL
223
    // ========
224
225
    protected static function createOptionPages()
226
    {
227
228
        foreach (self::$optionTypes as $optionType => $option) {
229
            $title = _x($option['title'], 'title', 'flynt-starter-theme');
230
            $slug = ucfirst($optionType);
231
232
            acf_add_options_page([
233
                'page_title'  => $title,
234
                'menu_title'  => $title,
235
                'redirect'    => true,
236
                'menu_slug'   => $slug,
237
                'icon_url'    => $option['icon']
238
            ]);
239
240
            self::$optionPages[$optionType] = [
241
                'menu_slug' => $slug,
242
                'menu_title' => $title
243
            ];
244
        }
245
246
        add_action('current_screen', function ($currentScreen) {
247
            foreach (self::$optionTypes as $optionType => $option) {
248
                $isTranslatable = $option['translatable'];
249
                $toplevelPageId = 'toplevel_page_' . $optionType;
250
                $menuTitle = self::$optionPages[$optionType]['menu_title'];
251
                $subPageId = sanitize_title($menuTitle) . '_page_' . $optionType;
252
                $isCurrentPage = StringHelpers::startsWith($toplevelPageId, $currentScreen->id)
253
                || StringHelpers::startsWith($subPageId, $currentScreen->id);
254
255
                if (!$isTranslatable && $isCurrentPage) {
256
                    // set acf field values to default language
257
                    add_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 101);
258
259
                    // hide language selector in admin bar
260
                    add_action('wp_before_admin_bar_render', function () {
261
                        $adminBar = $GLOBALS['wp_admin_bar'];
262
                        $adminBar->remove_menu('WPML_ALS');
263
                    });
264
                }
265
            }
266
        });
267
    }
268
269
    protected static function createSubPage($type, $name)
270
    {
271
        $namespace = self::FILTER_NAMESPACES[$type];
272
        foreach (self::$optionTypes as $optionType => $option) {
273
            $filterName = "{$namespace}/{$name}/Fields/" . ucfirst($optionType);
274
            $fields = apply_filters($filterName, []);
275
            if (!empty($fields)) {
276
                self::addOptionSubPage(
277
                    $type,
278
                    ucfirst($name),
279
                    $optionType,
280
                    $fields
281
                );
282
            }
283
        }
284
    }
285
286
    protected static function addOptionSubPage($optionCategoryName, $subPageName, $optionType, $fields)
287
    {
288
        $prettySubPageName = StringHelpers::splitCamelCase($subPageName);
289
        $optionCategorySettings = self::$optionCategories[$optionCategoryName];
290
        $iconClasses = 'flynt-submenu-item dashicons-before ' . $optionCategorySettings['icon'];
291
292
        $appendCategory = '';
293
        if (isset($optionCategorySettings['showType']) && true === $optionCategorySettings['showType']) {
294
            $appendCategory = ' (' . $optionCategorySettings['title'] . ')';
295
        }
296
297
        $subPageConfig = [
298
            'page_title'  => $prettySubPageName . $appendCategory,
299
            'menu_title'  => "<span class='{$iconClasses}'>{$prettySubPageName}</span>",
300
            'parent_slug' => self::$optionPages[$optionType]['menu_slug'],
301
            'menu_slug'   => $optionType . ucfirst($optionCategoryName) . $subPageName
302
        ];
303
304
        acf_add_options_sub_page($subPageConfig);
305
306
        self::addFieldGroupToSubPage(
307
            $subPageConfig['parent_slug'],
308
            $subPageConfig['menu_slug'],
309
            $subPageConfig['menu_title'],
310
            $fields
311
        );
312
    }
313
314
    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...
315
    {
316
        $fieldGroup = ACFComposer\ResolveConfig::forFieldGroup(
317
            [
318
                'name' => $menuSlug,
319
                'title' => $prettySubPageName,
320
                'fields' => $fields,
321
                'style' => 'seamless',
322
                'location' => [
323
                    [
324
                        [
325
                            'param' => 'options_page',
326
                            'operator' => '==',
327
                            'value' => $menuSlug
328
                        ]
329
                    ]
330
                ]
331
            ]
332
        );
333
        $fieldGroup['fields'] = self::prefixFields($fieldGroup['fields'], $menuSlug);
334
        acf_add_local_field_group($fieldGroup);
335
    }
336
337
    protected static function checkFeature($optionCategory, $feature)
338
    {
339
        if (array_key_exists($optionCategory, self::$optionCategories) && !Feature::isRegistered($feature)) {
340
            $title = self::$optionCategories[$optionCategory]['title'];
341
            $noticeManager = AdminNoticeManager::getInstance();
342
            $noticeManager->addNotice(
343
                [
344
                    "Could not add Option Pages for {$title} because the \"{$feature}\" feature is missing."
345
                ],
346
                [
347
                    'title' => 'Acf Option Pages Feature',
348
                    'dismissible' => true,
349
                    'type' => 'info'
350
                ]
351
            );
352
        }
353
    }
354
355
    protected static function prefixFields($fields, $prefix)
356
    {
357
        return array_map(function ($field) use ($prefix) {
358
            $field['name'] = $prefix . '_' . $field['name'];
359
            return $field;
360
        }, $fields);
361
    }
362
363
    protected static function getOptionFields($translatable)
364
    {
365
        global $sitepress;
366
367
        if (!isset($sitepress)) {
368
            $options = self::getCachedOptionFields();
369
        } else if ($translatable) {
370
            // get options from cache with language namespace
371
            $options = self::getCachedOptionFields(ICL_LANGUAGE_CODE);
372
        } else {
373
            // switch to default language to get global options
374
            $sitepress->switch_lang(acf_get_setting('default_language'));
375
376
            add_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 100);
377
378
            // get optios from cache with global namespace
379
            $options = self::getCachedOptionFields('global');
380
381
            remove_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 100);
382
383
            $sitepress->switch_lang(ICL_LANGUAGE_CODE);
384
        }
385
386
        return $options;
387
    }
388
389
    protected static function getCachedOptionFields($namespace = '')
390
    {
391
        // get cached options
392
        $found = false;
393
        $suffix = !empty($namespace) ? "_${namespace}" : '';
394
        $cacheKey = "Features/Acf/OptionPages/ID=options${suffix}";
395
396
        $options = wp_cache_get($cacheKey, 'flynt', null, $found);
397
398
        if (!$found) {
399
            $options = get_fields('options');
400
            wp_cache_set($cacheKey, $options, 'flynt');
401
        }
402
403
        return $options;
404
    }
405
406
    protected static function combineArrayDefaults(array $array, array $defaults)
407
    {
408
        return array_map(function ($value) use ($defaults) {
409
            return is_array($value) ? array_merge($defaults, $value) : [];
410
        }, $array);
411
    }
412
413
    public static function getDefaultAcfLanguage()
414
    {
415
        return acf_get_setting('default_language');
416
    }
417
}
418