Passed
Push — master ( 50b6e9...8d46b5 )
by Julien
07:54 queued 02:57
created

Exposer   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 237
Duplicated Lines 0 %

Test Coverage

Coverage 70.25%

Importance

Changes 5
Bugs 1 Features 0
Metric Value
wmc 59
eloc 123
c 5
b 1
f 0
dl 0
loc 237
ccs 85
cts 121
cp 0.7025
rs 4.08

5 Methods

Rating   Name   Duplication   Size   Complexity  
A createBuilder() 0 11 1
A getValue() 0 5 3
F checkExpose() 0 114 30
F parseColumnsRecursive() 0 48 15
B expose() 0 35 10

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 2
    public static function createBuilder(mixed $object, ?array $columns = null, ?bool $expose = null, ?bool $protected = null): Builder
43
    {
44 2
        $expose ??= true;
45 2
        $protected ??= false;
46
        
47 2
        $builder = new Builder();
48 2
        $builder->setColumns(self::parseColumnsRecursive($columns));
49 2
        $builder->setExpose($expose);
50 2
        $builder->setProtected($protected);
51 2
        $builder->setValue($object);
52 2
        return $builder;
53
    }
54
    
55 1
    private static function getValue(string $string, mixed $value): string
56
    {
57 1
        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...
58
            ? 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

58
            ? sprintfn($string, /** @scrutinizer ignore-type */ $value)
Loading history...
59 1
            : mb_sprintf($string, $value);
60
    }
61
    
62 2
    private static function checkExpose(Builder $builder): void
63
    {
64 2
        $columns = $builder->getColumns();
65 2
        $fullKey = $builder->getFullKey();
66 2
        $value = $builder->getValue();
67
        
68
        // Check if the key itself exists at first
69 2
        if (isset($columns[$fullKey])) {
70 2
            $column = $columns[$fullKey];
71
            
72
            // If boolean, set expose to the boolean value
73 2
            if (is_bool($column)) {
74 2
                $builder->setExpose($column);
75
            }
76
            
77
            // If callable, set the expose to true, and run the method and passes the builder as parameter
78 1
            elseif (is_callable($column)) {
79
                $builder->setExpose(true);
80
                $callbackReturn = $column($builder);
81
                
82
                // If builder is returned, no nothing
83
                if ($callbackReturn instanceof BuilderInterface) {
84
                    $builder = $callbackReturn;
85
                }
86
                
87
                // If string is returned, set expose to true and parse with mb_sprintfn or mb_sprintf
88
                elseif (is_string($callbackReturn)) {
89
                    $builder->setExpose(true);
90
                    $builder->setValue(self::getValue($callbackReturn, $value));
91
                }
92
                
93
                // If bool is returned, set expose to boolean value
94
                elseif (is_bool($callbackReturn)) {
95
                    $builder->setExpose($callbackReturn);
96
                }
97
                
98
                // If array is returned, parse the columns from the current context key and merge it with the builder
99
                elseif (is_array($callbackReturn)) {
100
                    $columns = self::parseColumnsRecursive($callbackReturn, $builder->getFullKey());
101
                    
102
                    // If not set, set expose to false by default
103
                    if (!isset($columns[$builder->getFullKey()])) {
104
                        $columns[$builder->getFullKey()] = false;
105
                    }
106
                    
107
                    //@TODO handle with array_merge_recursive and handle array inside the columns parameters ^^
108
                    $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

108
                    $builder->setColumns(array_merge(/** @scrutinizer ignore-type */ $builder->getColumns(), $columns));
Loading history...
109
                }
110
            }
111
            
112
            // If is string, set expose to true and parse with mb_sprintfn or mb_sprintf
113 1
            elseif (is_string($column)) {
114 1
                $builder->setExpose(true);
115 1
                $builder->setValue(self::getValue($column, $value));
116
            }
117
        }
118
        
119
        // Otherwise, check if a parent key exists
120
        else {
121 2
            $parentKey = $fullKey;
122 2
            $parentIndex = strrpos($parentKey, '.');
123
            do {
124 2
                $parentKey = $parentIndex ? substr($parentKey, 0, $parentIndex) : '';
125 2
                if (isset($columns[$parentKey])) {
126 2
                    $column = $columns[$parentKey];
127 2
                    if (is_bool($column)) {
128 2
                        $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 2
                    break;
152
                    // break at the first parent found
153
                }
154
            }
155 1
            while ($parentIndex = strrpos($parentKey, '.'));
156
        }
157
        
158 2
        if (!empty($columns)) {
159
            // Try to find a subentity, or field that has the true value
160 2
            $value = $builder->getValue();
161 2
            if (is_array($value) || is_object($value) || is_callable($value)) {
162 2
                foreach ($columns as $columnKey => $columnValue) {
163 2
                    $ret = str_starts_with($columnKey, $builder->getFullKey());
0 ignored issues
show
Bug introduced by
It seems like $builder->getFullKey() can also be of type null; however, parameter $needle of str_starts_with() 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

163
                    $ret = str_starts_with($columnKey, /** @scrutinizer ignore-type */ $builder->getFullKey());
Loading history...
164 2
                    if ($ret && $columnValue === true) {
165
                        // expose the current instance (which is the parent of the sub column)
166 1
                        $builder->setExpose(true);
167
                    }
168
                }
169
            }
170
        }
171
        
172
        // check for protected setting
173 2
        $key = $builder->getKey();
174 2
        if (!$builder->getProtected() && is_string($key) && str_starts_with($key, '_')) {
175
            $builder->setExpose(false);
176
        }
177
    }
178
    
179
    /**
180
     * @return array|false|Model
181
     */
182 2
    public static function expose(Builder $builder)
183
    {
184 2
        $columns = $builder->getColumns();
185 2
        $value = $builder->getValue();
186
        
187 2
        if (is_array($value) || is_object($value)) {
188 2
            $toParse = is_object($value) && method_exists($value, 'toArray')
189
                ? $value->toArray()
190 2
                : $value;
191
            
192
            // si accused column demandé et que l'expose est à false
193 2
            if (is_null($columns) && !$builder->getExpose()) {
194
                return [];
195
            }
196
            
197
            // Prépare l'array de retour des fields de l'instance
198 2
            $ret = [];
199 2
            $currentContextKey = $builder->getContextKey();
200 2
            $builder->setContextKey($builder->getFullKey());
201 2
            foreach ($toParse as $fieldKey => $fieldValue) {
202 2
                $builder->setParent($value);
203 2
                $builder->setKey($fieldKey);
204 2
                $builder->setValue($fieldValue);
205 2
                self::checkExpose($builder);
206 2
                if ($builder->getExpose()) {
207 2
                    $ret [$fieldKey] = self::expose($builder);
208
                }
209
            }
210 2
            $builder->setContextKey($currentContextKey);
211
        }
212
        else {
213 2
            $ret = $builder->getExpose() ? $value : null;
214
        }
215
        
216 2
        return $ret;
217
    }
218
    
219
    /**
220
     * Here to parse the columns parameter into some kind of flatten array with
221
     * the key path separated by dot "my.path" and the value true, false or a callback function
222
     * including the ExposeBuilder object
223
     *
224
     * @param array|null $columns
225
     * @param string|null $context
226
     *
227
     * @return array|null
228
     */
229 2
    public static function parseColumnsRecursive(?array $columns = null, ?string $context = null): ?array
230
    {
231 2
        if (!isset($columns)) {
232 1
            return null;
233
        }
234 2
        $ret = [];
235 2
        foreach ($columns as $key => $value) {
236 2
            if (is_bool($key)) {
237
                $value = $key;
238
                $key = null;
239
            }
240
            
241 2
            if (is_int($key)) {
242 2
                if (is_string($value)) {
243 1
                    $key = $value;
244 1
                    $value = true;
245
                }
246
                else {
247 2
                    $key = null;
248
                }
249
            }
250
            
251 2
            if (is_string($key)) {
252 2
                $key = trim(mb_strtolower($key));
253
            }
254
            
255 2
            if (is_string($value) && empty($value)) {
256
                $value = true;
257
            }
258 2
            $currentKey = (!empty($context) ? $context . (!empty($key) ? '.' : null) : null) . $key;
259 2
            if (is_array($value) || is_object($value)) {
260 1
                if (is_callable($value)) {
261
                    $ret[$currentKey] = $value;
262
                }
263
                else {
264 1
                    $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

264
                    $subRet = self::parseColumnsRecursive(/** @scrutinizer ignore-type */ $value, $currentKey);
Loading history...
265 1
                    $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

265
                    $ret = array_merge_recursive($ret, /** @scrutinizer ignore-type */ $subRet);
Loading history...
266 1
                    if (!isset($ret[$currentKey])) {
267 1
                        $ret[$currentKey] = false;
268
                    }
269
                }
270
            }
271
            else {
272 2
                $ret[$currentKey] = $value;
273
            }
274
        }
275
        
276 2
        return $ret;
277
    }
278
}
279