Test Failed
Push — master ( 31c635...ef6440 )
by Paul
07:17 queued 01:32
created

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