Passed
Push — master ( 4aa208...f1ad34 )
by Paul
05:58
created

DefaultsAbstract::unmapKeys()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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