Passed
Push — master ( 817cab...57b58d )
by Paul
09:30 queued 02:21
created

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