Test Failed
Push — master ( 8a9607...6de795 )
by Paul
05:45
created

DefaultsAbstract::normalize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 1
b 1
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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
        return $this->app()->filterArray($this->hook, $values, $this->method);
137
    }
138
139
    /**
140
     * @return string
141
     */
142 25
    protected function currentHook()
143
    {
144 25
        $hookName = (new ReflectionClass($this))->getShortName();
145 25
        $hookName = Str::replaceLast('Defaults', '', $hookName);
146 25
        return Str::dashCase($hookName);
147
    }
148
149
    /**
150
     * @return string
151
     */
152 18
    protected function concatenate($key, $value)
153
    {
154 18
        if (in_array($key, $this->property('concatenated'))) {
155
            $default = glsr()->args($this->defaults)->$key;
156
            return trim($default.$this->glue.$value);
157
        }
158 18
        return $value;
159
    }
160
161
    /**
162
     * Restrict provided values to defaults, remove empty and unchanged values,
163
     * and return data attribute keys with JSON encoded values.
164
     * @return array
165
     */
166
    protected function dataAttributes(array $values = [])
167
    {
168
        $defaults = $this->flattenArrayValues($this->defaults);
169
        $values = $this->flattenArrayValues(shortcode_atts($defaults, $values));
170
        $filtered = array_filter(array_diff_assoc($values, $defaults));  // remove all empty values
171
        $filtered = $this->sanitize($filtered);
172
        $filtered = $this->guard($filtered); // this after sanitize for a more unique id
173
        $filteredJson = [];
174
        foreach ($filtered as $key => $value) {
175
            $filteredJson['data-'.$key] = !is_scalar($value)
176
                ? json_encode((array) $value, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
177
                : $value;
178
        }
179
        return $filteredJson;
180
    }
181
182
    /**
183
     * The default values.
184
     * @return array
185
     */
186 14
    protected function defaults()
187
    {
188 14
        return [];
189
    }
190
191
    /**
192
     * Remove empty values from the provided values and merge with the defaults.
193
     * @return array
194
     */
195 14
    protected function filter(array $values = [])
196
    {
197 14
        return $this->merge(array_filter($values, Helper::class.'::isNotEmpty'));
198
    }
199
200
    /**
201
     * @return array
202
     */
203
    protected function flattenArrayValues(array $values)
204
    {
205
        array_walk($values, function (&$value) {
206
            if (is_array($value)) {
207
                $value = implode(',', array_filter($value, 'is_scalar'));
208
            }
209
        });
210
        return $values;
211
    }
212
213
    /**
214
     * Remove guarded keys from the provided values.
215
     * @return array
216
     */
217 25
    protected function guard(array $values)
218
    {
219 25
        if (!Str::startsWith('unguarded', $this->called)) {
220 25
            return array_diff_key($values, array_flip($this->property('guarded')));
221
        }
222 1
        return $values;
223
    }
224
225
    /**
226
     * Map old or deprecated keys to new keys.
227
     * @return array
228
     */
229 25
    protected function mapKeys(array $args)
230
    {
231 25
        foreach ($this->property('mapped') as $old => $new) {
232 17
            if (!empty($args[$old])) { // old always takes precedence
233 16
                $args[$new] = $args[$old];
234
            }
235 17
            unset($args[$old]);
236
        }
237 25
        return $args;
238
    }
239
240
    /**
241
     * Merge provided values with the defaults.
242
     * @return array
243
     */
244 14
    protected function merge(array $values = [])
245
    {
246 14
        return $this->parse($values, $this->defaults);
247
    }
248
249
    /**
250
     * Normalize provided values, this always runs first.
251
     * @return array
252
     */
253 25
    protected function normalize(array $values = [])
254
    {
255 25
        return $values;
256
    }
257
258
    /**
259
     * @param mixed $values
260
     * @param mixed $defaults
261
     * @return array
262
     */
263 16
    protected function parse($values, $defaults)
264
    {
265 16
        $values = Cast::toArray($values);
266 16
        if (!is_array($defaults)) {
267 1
            return $values;
268
        }
269 16
        $parsed = $defaults;
270 16
        foreach ($values as $key => $value) {
271 15
            if (!is_scalar($value) && isset($parsed[$key])) {
272 13
                $parsed[$key] = Arr::unique($this->parse($value, $parsed[$key]));
273 13
                continue;
274
            }
275 15
            $parsed[$key] = $this->concatenate($key, $value);
276
        }
277 16
        return $parsed;
278
    }
279
280
    /**
281
     * @param mixed $values
282
     * @return array
283
     */
284 18
    protected function parseRestricted($values)
285
    {
286 18
        $values = Cast::toArray($values);
287 18
        $parsed = [];
288 18
        foreach ($this->defaults as $key => $default) {
289 18
            if (!array_key_exists($key, $values)) {
290 18
                $parsed[$key] = $default;
291 18
                continue;
292
            }
293 18
            if (is_array($default)) { // if the default value is supposed to be an array
294 9
                $parsed[$key] = $this->parse($values[$key], $default);
295 9
                continue;
296
            }
297 18
            $parsed[$key] = $this->concatenate($key, $values[$key]);
298
        }
299 18
        return $parsed;
300
    }
301
302
    /**
303
     * @return array|void
304
     */
305 25
    protected function property($key)
306
    {
307
        try {
308 25
            $reflection = new ReflectionClass($this);
309 25
            $property = $reflection->getProperty($key);
310 25
            $value = $property->getValue($this);
311 25
            if ($property->isPublic()) { // all public properties are expected to be an array
312 25
                $hook = 'defaults/'.$this->hook.'/'.$key;
313 25
                return $this->app()->filterArray($hook, $value, $this->method);
314
            }
315
        } catch (ReflectionException $e) {
316
            glsr_log()->error("Invalid or protected property [$key].");
317
        }
318
    }
319
320
    /**
321
     * Merge the provided values with the defaults and remove any non-default keys.
322
     * @return array
323
     */
324 18
    protected function restrict(array $values = [])
325
    {
326 18
        return $this->parseRestricted($values);
327
    }
328
329
    /**
330
     * @return array
331
     */
332 25
    protected function sanitize(array $values = [])
333
    {
334 25
        foreach ($this->property('casts') as $key => $cast) {
335 16
            if (array_key_exists($key, $values)) {
336 16
                $values[$key] = Cast::to($cast, $values[$key]);
337
            }
338
        }
339 25
        return (new Sanitizer($values, $this->property('sanitize')))->run();
340
    }
341
342
    /**
343
     * @return array
344
     */
345
    protected function unmapKeys(array $args)
346
    {
347
        foreach ($this->property('mapped') as $old => $new) {
348
            if (array_key_exists($new, $args)) {
349
                $args[$old] = $args[$new];
350
                unset($args[$new]);
351
            }
352
        }
353
        return $args;
354
    }
355
}
356