Test Failed
Push — master ( 894c40...e5d2d2 )
by Julien
11:34
created

Exposer   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 126
c 1
b 0
f 0
dl 0
loc 247
rs 4.08
wmc 59

5 Methods

Rating   Name   Duplication   Size   Complexity  
D checkExpose() 0 113 28
A getValue() 0 5 3
F parseColumnsRecursive() 0 48 15
A createBuilder() 0 12 1
C expose() 0 45 12

How to fix   Complexity   

Complex Class

Complex classes like Exposer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Exposer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit\Support\Exposer;
13
14
use Zemit\Mvc\Model;
15
16
/**
17
 * Class Expose
18
 * @todo rewrite this code
19
 * @todo write unit test for this
20
 *
21
 * Example
22
 *
23
 * Simple
24
 * ```php
25
 * $this->expose() // expose everything except protected properties
26
 * $this->expose(null, true, true); // expose everything including protected properties
27
 * $this->expose(array('Source.id' => false)) // expose everything except 'id' and protected properties
28
 * $this->expose(array('Source' => array('id', 'title')) // expose only 'id' and 'title'
29
 * $this->expose(array('Source' => true, false) // expose everything from Source except protected properties
30
 * $this->expose(array('Source.Sources' => true, false) // expose everything from Source->Sources except protected properties
31
 * $this->expose(array('Source.Sources' => array('id'), false) // expose only the 'id' field of the sub array "Sources"
32
 * $this->expose(array('Source.Sources' => array(true, 'id' => false), false) // expose everything from the sub array "Sources" except the 'id' field
33
 * $this->expose(array('Source' => array(false, 'Sources' => array(true, 'id' => false))) // expose everything from the sub array "Sources" except the 'id' field
34
 * ```
35
 * Complexe
36
 *
37
 *
38
 * @package Zemit\Mvc\Model\Expose
39
 */
40
class Exposer
41
{
42
    public static function createBuilder($object, ?array $columns = null, ?bool $expose = null, ?bool $protected = null): Builder
43
    {
44
        $expose ??= true;
45
        $protected ??= false;
46
        
47
        $builder = new Builder();
48
        $builder->setColumns(self::parseColumnsRecursive($columns));
49
        $builder->setExpose($expose);
50
        $builder->setProtected($protected);
51
        $builder->setParent($object);
52
        $builder->setValue($object);
53
        return $builder;
54
    }
55
    
56
    private static function getValue(string $string, $value): string
57
    {
58
        return (is_array($value) || is_object($value))
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_array($value) ...printf($string, $value) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
59
            ? sprintfn($string, $value)
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type object; however, parameter $args of sprintfn() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

59
            ? sprintfn($string, /** @scrutinizer ignore-type */ $value)
Loading history...
60
            : mb_sprintf($string, $value);
61
    }
62
    
63
    private static function checkExpose(Builder $builder): void
64
    {
65
        $columns = $builder->getColumns();
66
        $fullKey = $builder->getFullKey();
67
        $value = $builder->getValue();
68
        
69
        // Check if the key itself exists at first
70
        if (isset($columns[$fullKey])) {
71
            $column = $columns[$fullKey];
72
            
73
            // If boolean, set expose to the boolean value
74
            if (is_bool($column)) {
75
                $builder->setExpose($column);
76
            }
77
            
78
            // If callable, set the expose to true, and run the method and passes the builder as parameter
79
            elseif (is_callable($column)) {
80
                $builder->setExpose(true);
81
                $callbackReturn = $column($builder);
82
                
83
                // If builder is returned, no nothing
84
                if ($callbackReturn instanceof BuilderInterface) {
85
                    $builder = $callbackReturn;
86
                }
87
                
88
                // If string is returned, set expose to true and parse with mb_sprintfn or mb_sprintf
89
                elseif (is_string($callbackReturn)) {
90
                    $builder->setExpose(true);
91
                    $builder->setValue(self::getValue($callbackReturn, $value));
92
                }
93
                
94
                // If bool is returned, set expose to boolean value
95
                elseif (is_bool($callbackReturn)) {
96
                    $builder->setExpose($callbackReturn);
97
                }
98
                
99
                // If array is returned, parse the columns from the current context key and merge it with the builder
100
                elseif (is_array($callbackReturn)) {
101
                    $columns = self::parseColumnsRecursive($callbackReturn, $builder->getFullKey());
102
                    
103
                    // If not set, set expose to false by default
104
                    if (!isset($columns[$builder->getFullKey()])) {
105
                        $columns[$builder->getFullKey()] = false;
106
                    }
107
                    
108
                    //@TODO handle with array_merge_recursive and handle array inside the columns parameters ^^
109
                    $builder->setColumns(array_merge($builder->getColumns(), $columns));
0 ignored issues
show
Bug introduced by
It seems like $builder->getColumns() can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

109
                    $builder->setColumns(array_merge(/** @scrutinizer ignore-type */ $builder->getColumns(), $columns));
Loading history...
110
                }
111
            }
112
            
113
            // If is string, set expose to true and parse with mb_sprintfn or mb_sprintf
114
            elseif (is_string($column)) {
115
                $builder->setExpose(true);
116
                $builder->setValue(self::getValue($column, $value));
117
            }
118
        }
119
        
120
        // Otherwise, check if a parent key exists
121
        else {
122
            $parentKey = $fullKey;
123
            while ($parentIndex = strrpos($parentKey, '.')) {
124
                $parentKey = substr($parentKey, 0, $parentIndex);
125
                if (isset($columns[$parentKey])) {
126
                    $column = $columns[$parentKey];
127
                    if (is_bool($column)) {
128
                        $builder->setExpose($column);
129
                    }
130
                    elseif (is_callable($column)) {
131
                        $builder->setExpose(true);
132
                        $callbackReturn = $column($builder);
133
                        if ($callbackReturn instanceof BuilderInterface) {
134
                            $builder = $callbackReturn;
135
                        }
136
                        elseif (is_string($callbackReturn)) {
137
                            $builder->setExpose(true);
138
                            $builder->setValue(self::getValue($callbackReturn, $value));
139
                        }
140
                        elseif (is_bool($callbackReturn)) {
141
                            $builder->setExpose($callbackReturn);
142
                        }
143
                        elseif (is_array($callbackReturn)) {
144
                            // Since it is a parent, we're not supposed to re-re-merge the existing columns
145
                        }
146
                    }
147
                    elseif (is_string($column)) {
148
                        $builder->setExpose(false);
149
                        $builder->setValue(self::getValue($column, $value));
150
                    }
151
                    break;
152
                    // break at the first parent found
153
                }
154
            }
155
        }
156
        
157
        // Try to find a subentity, or field that has the true value
158
        $value = $builder->getValue();
159
        if ((is_array($value) || is_object($value) || is_callable($value))) {
160
            $subColumns = is_null($columns) ? $columns : array_filter($columns, function ($columnValue, $columnKey) use ($builder) {
0 ignored issues
show
Unused Code introduced by
The assignment to $subColumns is dead and can be removed.
Loading history...
161
                
162
                $ret = strpos($columnKey, $builder->getFullKey()) === 0;
0 ignored issues
show
Bug introduced by
It seems like $builder->getFullKey() can also be of type null; however, parameter $needle of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
                $ret = strpos($columnKey, /** @scrutinizer ignore-type */ $builder->getFullKey()) === 0;
Loading history...
163
                if ($ret && $columnValue === true) {
164
                    // expose the current instance (which is the parent of the sub column)
165
                    $builder->setExpose(true);
166
                }
167
                
168
                return $ret;
169
            }, ARRAY_FILTER_USE_BOTH);
170
        }
171
        
172
        // check for protected setting
173
        $key = $builder->getKey();
174
        if (!$builder->getProtected() && is_string($key) && strpos($key, '_') === 0) {
175
            $builder->setExpose(false);
176
        }
177
    }
178
    
179
    /**
180
     * @return array|false|Model
181
     */
182
    public static function expose(Builder $builder)
183
    {
184
        $columns = $builder->getColumns();
185
        
186
        $value = $builder->getValue();
187
        
188
        if (is_array($value) || is_object($value)) {
189
            $toParse = [];
190
            if (is_array($value)) {
191
                $toParse = $value;
192
            }
193
            elseif (method_exists($value, 'toArray')) {
194
                $toParse = $value->toArray();
195
            }
196
            
197
            // @TODO fix / refactor this
198
            if (isset($value->dirtyRelated)) {
199
                foreach ($value->dirtyRelated as $dirtyRelatedKey => $dirtyRelated) {
200
                    $toParse[$dirtyRelatedKey] = $dirtyRelated ?? false;
201
                }
202
            }
203
            
204
            // si aucune column demandé et que l'expose est à false
205
            if (is_null($columns) && !$builder->getExpose()) {
206
                return [];
207
            }
208
            
209
            // Prépare l'array de retour des fields de l'instance
210
            $ret = [];
211
            $builder->setContextKey($builder->getFullKey());
212
            foreach ($toParse as $fieldKey => $fieldValue) {
213
                $builder->setParent($value);
214
                $builder->setKey($fieldKey);
215
                $builder->setValue($fieldValue);
216
                self::checkExpose($builder);
217
                if ($builder->getExpose()) {
218
                    $ret [$fieldKey] = self::expose($builder);
219
                }
220
            }
221
        }
222
        else {
223
            $ret = $builder->getExpose() ? $value : false;
224
        }
225
        
226
        return $ret;
227
    }
228
    
229
    /**
230
     * Here to parse the columns parameter into some kind of flatten array with
231
     * the key path separated by dot "my.path" and the value true, false or a callback function
232
     * including the ExposeBuilder object
233
     *
234
     * @param array|null $columns
235
     * @param string|null $context
236
     *
237
     * @return array|null
238
     */
239
    public static function parseColumnsRecursive(?array $columns = null, ?string $context = null): ?array
240
    {
241
        if (!isset($columns)) {
242
            return null;
243
        }
244
        $ret = [];
245
        foreach ($columns as $key => $value) {
246
            if (is_bool($key)) {
247
                $value = $key;
248
                $key = null;
249
            }
250
            
251
            if (is_int($key)) {
252
                if (is_string($value)) {
253
                    $key = $value;
254
                    $value = true;
255
                }
256
                else {
257
                    $key = null;
258
                }
259
            }
260
            
261
            if (is_string($key)) {
262
                $key = trim(mb_strtolower($key));
263
            }
264
            
265
            if (is_string($value) && empty($value)) {
266
                $value = true;
267
            }
268
            $currentKey = (!empty($context) ? $context . (!empty($key) ? '.' : null) : null) . $key;
269
            if (is_array($value) || is_object($value)) {
270
                if (is_callable($value)) {
271
                    $ret[$currentKey] = $value;
272
                }
273
                else {
274
                    $subRet = self::parseColumnsRecursive($value, $currentKey);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type object; however, parameter $columns of Zemit\Support\Exposer\Ex...parseColumnsRecursive() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

274
                    $subRet = self::parseColumnsRecursive(/** @scrutinizer ignore-type */ $value, $currentKey);
Loading history...
275
                    $ret = array_merge_recursive($ret, $subRet);
0 ignored issues
show
Bug introduced by
It seems like $subRet can also be of type null; however, parameter $arrays of array_merge_recursive() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

275
                    $ret = array_merge_recursive($ret, /** @scrutinizer ignore-type */ $subRet);
Loading history...
276
                    if (!isset($ret[$currentKey])) {
277
                        $ret[$currentKey] = false;
278
                    }
279
                }
280
            }
281
            else {
282
                $ret[$currentKey] = $value;
283
            }
284
        }
285
        
286
        return $ret;
287
    }
288
}
289