Test Failed
Push — develop ( 425fc0...4f2be8 )
by Paul
13:11
created

DefaultsAbstract::guard()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Defaults;
4
5
use GeminiLabs\SiteReviews\Contracts\DefaultsContract;
6
use GeminiLabs\SiteReviews\Contracts\PluginContract;
7
use GeminiLabs\SiteReviews\Helper;
8
use GeminiLabs\SiteReviews\Helpers\Arr;
9
use GeminiLabs\SiteReviews\Helpers\Cast;
10
use GeminiLabs\SiteReviews\Helpers\Str;
11
use GeminiLabs\SiteReviews\Modules\Sanitizer;
12
13
/**
14
 * Run order:
15
 * 1. map keys
16
 * 2. normalize()
17
 * 3. merge/restrict/filter with defaults (and concatenate values)
18
 * 4. cast values
19
 * 5. sanitize values
20
 * 6. enum values
21
 * 7. guard values
22
 * 8. finalize()
23
 *
24
 * @method array dataAttributes(array $values = [])
25
 * @method array defaults()
26
 * @method array filter(array $values = [])
27
 * @method array merge(array $values = [])
28
 * @method array restrict(array $values = [])
29
 * @method array unguardedDataAttributes(array $values = [])
30
 * @method array unguardedDefaults():
31
 * @method array unguardedFilter(array $values = [])
32
 * @method array unguardedMerge(array $values = [])
33
 * @method array unguardedRestrict(array $values = [])
34
 */
35
abstract class DefaultsAbstract implements DefaultsContract
36
{
37
    /**
38
     * The values that should be cast before sanitization is run.
39
     * This is done before $sanitize and $enums.
40
     */
41
    public array $casts = [];
42
43
    /**
44
     * The values that should be concatenated.
45
     *
46
     * @var string[]
47
     */
48
    public array $concatenated = [];
49
50
    /**
51
     * The values that should be constrained after sanitization is run.
52
     * This is done after $casts and $sanitize.
53
     */
54
    public array $enums = [];
55
56
    /**
57
     * The values that should be guarded.
58
     *
59
     * @var string[]
60
     */
61
    public array $guarded = [];
62
63
    /**
64
     * The keys that should be mapped to other keys.
65
     * Keys are mapped before the values are cast, normalized, and sanitized.
66
     * Note: Mapped keys should not be included in the defaults!
67
     */
68
    public array $mapped = [];
69
70
    /**
71
     * The values that should be sanitized.
72
     * This is done after $casts and before $enums.
73
     */
74
    public array $sanitize = [];
75
76
    /**
77
     * The methods that are callable.
78
     */
79
    protected array $callable = [
80
        'dataAttributes', 'defaults', 'filter', 'merge', 'restrict',
81
    ];
82
83
    /**
84
     * The method being called.
85
     */
86
    protected string $called = '';
87
88
    /**
89
     * The default data.
90
     */
91
    protected array $defaults = [];
92
93
    /**
94
     * The string used for concatenation.
95
     */
96
    protected string $glue = ' ';
97
98
    /**
99
     * The current filter hook name.
100
     */
101
    protected string $hook = '';
102
103
    /**
104
     * The unprefixed method being called.
105
     */
106
    protected string $method = '';
107
108 175
    public function __call(string $name, array $args = []): array
109
    {
110 175
        return $this->call($name, Arr::consolidate($args[0] ?? []));
111
    }
112
113
    /**
114
     * Use this if you need to call a method from within another Defaults class.
115
     */
116 175
    public function call(string $name, array $values = []): array
117
    {
118 175
        $this->called = $name;
119 175
        $this->hook = $this->currentHook();
120 175
        $this->method = Helper::buildMethodName(Str::removePrefix($name, 'unguarded'));
121 175
        if (!in_array($this->method, $this->callable)) { // this also means that the callable method exists
122
            glsr_log()->error("Invalid method [$this->method].");
123
            return $values;
124
        }
125 175
        $this->defaults = $this->app()->filterArray("defaults/{$this->hook}/defaults", $this->defaults(), $this->hook);
126 175
        $mapped = $this->mapKeys($values);
127 175
        $normalized = $this->normalize($mapped);
128 175
        $values = $this->callMethod($normalized);
129 175
        return $this->app()->filterArray("defaults/{$this->hook}", $values, $this->method, $normalized, $this->hook);
130
    }
131
132
    /**
133
     * Use this to get the filtered value of a public class property.
134
     */
135 175
    public function property($key): array
136
    {
137
        try {
138 175
            $reflection = new \ReflectionClass($this);
139 175
            $property = $reflection->getProperty($key);
140 175
            $value = $property->getValue($this);
141 175
            if ($property->isPublic()) { // all public properties are expected to be an array
142 175
                $this->hook = $this->hook ?: $this->currentHook();
143 175
                return $this->app()->filterArray("defaults/{$this->hook}/{$key}", $value, $this->method, $this->hook);
144
            }
145
        } catch (\ReflectionException $e) {
146
            glsr_log()->error("Invalid or protected property [$key].");
147
        }
148
        return [];
149
    }
150
151 175
    protected function app(): PluginContract
152
    {
153 175
        return glsr();
154
    }
155
156 175
    protected function callMethod(array $normalized): array
157
    {
158 175
        $this->app()->action('defaults', $this, $this->hook, $this->method, $normalized);
159 175
        $values = 'defaults' === $this->method
160 41
            ? $this->defaults // use the filtered defaults instead of the normalized values
161 175
            : call_user_func([$this, $this->method], $normalized);
162 175
        if ('dataAttributes' !== $this->method) {
163 175
            $sanitized = $this->sanitize($values);
164 175
            $guarded = $this->guard($sanitized);
165 175
            $values = $this->finalize($guarded);
166
        }
167 175
        return $values;
168
    }
169
170 175
    protected function currentHook(): string
171
    {
172 175
        $hookName = (new \ReflectionClass($this))->getShortName();
173 175
        $hookName = Str::replaceLast('Defaults', '', $hookName);
174 175
        return Str::dashCase($hookName);
175
    }
176
177
    /**
178
     * @param mixed $value
179
     *
180
     * @return mixed
181
     */
182 166
    protected function concatenate(string $key, $value)
183
    {
184 166
        if (!in_array($key, $this->property('concatenated'))) {
185 166
            return $value;
186
        }
187
        if (!is_string($value)) {
188
            return $value;
189
        }
190
        $default = glsr()->args($this->defaults)->$key;
191
        return trim($default.$this->glue.$value);
192
    }
193
194
    /**
195
     * Restrict provided values to defaults, remove empty and unchanged values,
196
     * and return data attribute keys with JSON encoded values.
197
     */
198
    protected function dataAttributes(array $values = []): array
199
    {
200
        $values = shortcode_atts($this->defaults, $values);
201
        $sanitized = $this->sanitize($values);
202
        $guarded = $this->guard($sanitized); // this after sanitize for a more unique id
203
        $values = $this->finalize($guarded);
204
        $filtered = array_filter(array_diff_assoc(
205
            $this->flattenArrayValues($values),
206
            $this->flattenArrayValues($this->defaults)
207
        ));
208
        $filteredJson = [];
209
        foreach ($filtered as $key => $value) {
210
            $filteredJson["data-{$key}"] = !is_scalar($value)
211
                ? wp_json_encode((array) $value, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
212
                : $value;
213
        }
214
        return $filteredJson;
215
    }
216
217
    /**
218
     * The default values.
219
     */
220 18
    protected function defaults(): array
221
    {
222 18
        return [];
223
    }
224
225
    /**
226
     * Remove empty values from the provided values and merge with the defaults.
227
     */
228 18
    protected function filter(array $values = []): array
229
    {
230 18
        return $this->merge(array_filter($values, Helper::class.'::isNotEmpty'));
231
    }
232
233
    /**
234
     * Finalize provided values, this always runs last.
235
     */
236 159
    protected function finalize(array $values = []): array
237
    {
238 159
        return $values;
239
    }
240
241
    protected function flattenArrayValues(array $values): array
242
    {
243
        array_walk($values, function (&$value) {
244
            if (is_array($value)) {
245
                $value = implode(',', array_filter($value, 'is_scalar'));
246
            }
247
        });
248
        return $values;
249
    }
250
251
    /**
252
     * Remove guarded keys from the provided values.
253
     */
254 175
    protected function guard(array $values): array
255
    {
256 175
        if (!str_starts_with($this->called, 'unguarded')) {
257 175
            return array_diff_key($values, array_flip($this->property('guarded')));
258
        }
259 1
        return $values;
260
    }
261
262
    /**
263
     * Map old or deprecated keys to new keys.
264
     */
265 175
    protected function mapKeys(array $values): array
266
    {
267 175
        foreach ($this->property('mapped') as $old => $new) {
268 45
            if (empty($values[$new]) && !empty($values[$old])) { // new always takes precedence
269 21
                $values[$new] = $values[$old];
270
            }
271 45
            unset($values[$old]);
272
        }
273 175
        return $values;
274
    }
275
276
    /**
277
     * Merge provided values with the defaults.
278
     */
279 158
    protected function merge(array $values = []): array
280
    {
281 158
        return $this->parse($values, $this->defaults);
282
    }
283
284
    /**
285
     * Normalize provided values, this always runs first.
286
     */
287 159
    protected function normalize(array $values = []): array
288
    {
289 159
        return $values;
290
    }
291
292
    /**
293
     * @param mixed $values
294
     * @param mixed $defaults
295
     */
296 175
    protected function parse($values, $defaults): array
297
    {
298 175
        $values = Cast::toArray($values);
299 175
        if (!is_array($defaults)) {
300 20
            return $values;
301
        }
302 175
        $parsed = $defaults;
303 175
        foreach ($values as $key => $value) {
304 159
            if (!is_scalar($value) && isset($parsed[$key])) {
305 149
                $parsed[$key] = Arr::unique($this->parse($value, $parsed[$key])); // does not reindex
306 149
                continue;
307
            }
308 159
            $parsed[$key] = $this->concatenate((string) $key, $value);
309
        }
310 175
        return $parsed;
311
    }
312
313
    /**
314
     * @param mixed $values
315
     */
316 46
    protected function parseRestricted($values): array
317
    {
318 46
        $values = Cast::toArray($values);
319 46
        $parsed = [];
320 46
        foreach ($this->defaults as $key => $default) {
321 46
            if (!array_key_exists($key, $values)) {
322 46
                $parsed[$key] = $default;
323 46
                continue;
324
            }
325 45
            if (is_array($default)) { // if the default value is supposed to be an array
326 45
                $parsed[$key] = $this->parse($values[$key], $default);
327 45
                continue;
328
            }
329 36
            $parsed[$key] = $this->concatenate((string) $key, $values[$key]);
330
        }
331 46
        return $parsed;
332
    }
333
334
    /**
335
     * Merge the provided values with the defaults and remove any non-default keys.
336
     */
337 46
    protected function restrict(array $values = []): array
338
    {
339 46
        return $this->parseRestricted($values);
340
    }
341
342 175
    protected function sanitize(array $values = []): array
343
    {
344 175
        foreach ($this->property('casts') as $key => $cast) {
345 174
            if (array_key_exists($key, $values)) {
346 174
                $values[$key] = Cast::to($cast, $values[$key]);
347
            }
348
        }
349 175
        $values = (new Sanitizer($values, $this->property('sanitize')))->run();
350 175
        foreach ($this->property('enums') as $key => $enums) {
351 44
            if (array_key_exists($key, $values) && !in_array($values[$key], $enums, true)) {
352 44
                $values[$key] = $this->defaults[$key] ?? '';
353
            }
354
        }
355 175
        return $values;
356
    }
357
358
    protected function unmapKeys(array $args): array
359
    {
360
        foreach ($this->property('mapped') as $old => $new) {
361
            if (array_key_exists($new, $args)) {
362
                $args[$old] = $args[$new];
363
                unset($args[$new]);
364
            }
365
        }
366
        return $args;
367
    }
368
}
369