Test Failed
Push — master ( be437e...866f6c )
by Julien
07:25
created

Exposer   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Test Coverage

Coverage 70.34%

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 119
c 4
b 0
f 1
dl 0
loc 231
ccs 83
cts 118
cp 0.7034
rs 4.5599
wmc 58

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getValue() 0 4 1
F parseColumnsRecursive() 0 47 14
A createBuilder() 0 11 1
B expose() 0 35 10
F checkExpose() 0 113 32

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
 */
36
class Exposer
37
{
38 2
    public static function createBuilder(mixed $object, ?array $columns = null, ?bool $expose = null, ?bool $protected = null): Builder
39
    {
40 2
        $expose ??= true;
41 2
        $protected ??= false;
42
        
43 2
        $builder = new Builder();
44 2
        $builder->setColumns(self::parseColumnsRecursive($columns));
45 2
        $builder->setExpose($expose);
46 2
        $builder->setProtected($protected);
47 2
        $builder->setValue($object);
48 2
        return $builder;
49
    }
50
    
51 1
    private static function getValue(string $string, mixed $value): string
52
    {
53
        // @todo maybe we should remove the sprintf manipulation
54 1
        return mb_vsprintf($string, [$value]);
55
    }
56
    
57 2
    private static function checkExpose(Builder $builder): void
58
    {
59 2
        $columns = $builder->getColumns();
60 2
        $fullKey = $builder->getFullKey();
61 2
        $value = $builder->getValue();
62
        
63
        // Check if the key itself exists at first
64 2
        if (isset($fullKey, $columns[$fullKey])) {
65 2
            $column = $columns[$fullKey];
66
            
67
            // If boolean, set expose to the boolean value
68 2
            if (is_bool($column)) {
69 2
                $builder->setExpose($column);
70
            }
71
            
72
            // If callable, set the expose to true, and run the method and passes the builder as parameter
73 1
            elseif (is_callable($column)) {
74
                $builder->setExpose(true);
75
                $callbackReturn = $column($builder);
76
                
77
                // If builder is returned, no nothing
78
                if ($callbackReturn instanceof BuilderInterface) {
79
                    $builder = $callbackReturn;
80
                }
81
                
82
                // If string is returned, set expose to true and parse with mb_sprintfn or mb_sprintf
83
                elseif (is_string($callbackReturn)) {
84
                    $builder->setExpose(true);
85
                    $builder->setValue(self::getValue($callbackReturn, $value));
86
                }
87
                
88
                // If bool is returned, set expose to boolean value
89
                elseif (is_bool($callbackReturn)) {
90
                    $builder->setExpose($callbackReturn);
91
                }
92
                
93
                // If array is returned, parse the columns from the current context key and merge it with the builder
94
                elseif (is_iterable($callbackReturn)) {
95
                    $columns = self::parseColumnsRecursive($callbackReturn, $fullKey) ?? [];
96
                    
97
                    // If not set, set expose to false by default
98
                    if (!isset($columns[$fullKey])) {
99
                        $columns[$fullKey] = false;
100
                    }
101
                    
102
                    //@TODO handle with array_merge_recursive and handle array inside the columns parameters ^^
103
                    $builder->setColumns(array_merge($builder->getColumns() ?? [], $columns));
104
                }
105
            }
106
            
107
            // If is string, set expose to true and parse with mb_sprintfn or mb_sprintf
108 1
            elseif (is_string($column)) {
109 1
                $builder->setExpose(true);
110 1
                $builder->setValue(self::getValue($column, $value));
111
            }
112
        }
113
        
114
        // Otherwise, check if a parent key exists
115
        else {
116 2
            $parentKey = $fullKey;
117 2
            $parentIndex = isset($parentKey)? strrpos($parentKey, '.') : false;
118
            do {
119 2
                $parentKey = $parentIndex && isset($parentKey) ? substr($parentKey, 0, $parentIndex) : '';
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentIndex of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
120 2
                if (isset($columns[$parentKey])) {
121 2
                    $column = $columns[$parentKey];
122 2
                    if (is_bool($column)) {
123 2
                        $builder->setExpose($column);
124
                    }
125
                    elseif (is_callable($column)) {
126
                        $builder->setExpose(true);
127
                        $callbackReturn = $column($builder);
128
                        if ($callbackReturn instanceof BuilderInterface) {
129
                            $builder = $callbackReturn;
130
                        }
131
                        elseif (is_string($callbackReturn)) {
132
                            $builder->setExpose(true);
133
                            $builder->setValue(self::getValue($callbackReturn, $value));
134
                        }
135
                        elseif (is_bool($callbackReturn)) {
136
                            $builder->setExpose($callbackReturn);
137
                        }
138
                        elseif (is_iterable($callbackReturn)) {
139
                            // Since it is a parent, we're not supposed to re-re-merge the existing columns
140
                        }
141
                    }
142
                    elseif (is_string($column)) {
143
                        $builder->setExpose(false);
144
                        $builder->setValue(self::getValue($column, $value));
145
                    }
146 2
                    break;
147
                    // break at the first parent found
148
                }
149
            }
150 1
            while ($parentIndex = strrpos($parentKey, '.'));
151
        }
152
        
153 2
        if (isset($fullKey) && !empty($columns)) {
154
            // Try to find a subentity, or field that has the true value
155 2
            $value = $builder->getValue();
156 2
            if (is_iterable($value) || is_callable($value)) {
157 2
                foreach ($columns as $columnKey => $columnValue) {
158 2
                    if ($columnValue === true && str_starts_with($columnKey, $fullKey)) {
159
                        // expose the current instance (which is the parent of the sub column)
160 1
                        $builder->setExpose(true);
161
                    }
162
                }
163
            }
164
        }
165
        
166
        // check for protected setting
167 2
        $key = $builder->getKey();
168 2
        if (!$builder->getProtected() && is_string($key) && str_starts_with($key, '_')) {
169
            $builder->setExpose(false);
170
        }
171
    }
172
    
173 2
    public static function expose(Builder $builder): mixed
174
    {
175 2
        $columns = $builder->getColumns();
176 2
        $value = $builder->getValue();
177
        
178 2
        if (is_iterable($value) || is_object($value)) {
179 2
            $toParse = is_object($value) && method_exists($value, 'toArray')
180
                ? $value->toArray()
181 2
                : (array)$value;
182
            
183
            // si accused column demandé et que l'expose est à false
184 2
            if (is_null($columns) && !$builder->getExpose()) {
185
                return [];
186
            }
187
            
188
            // Prépare l'array de retour des fields de l'instance
189 2
            $ret = [];
190 2
            $currentContextKey = $builder->getContextKey();
191 2
            $builder->setContextKey($builder->getFullKey());
192 2
            foreach ($toParse as $fieldKey => $fieldValue) {
193 2
                $builder->setParent($value);
194 2
                $builder->setKey($fieldKey);
195 2
                $builder->setValue($fieldValue);
196 2
                self::checkExpose($builder);
197 2
                if ($builder->getExpose()) {
198 2
                    $ret [$fieldKey] = self::expose($builder);
199
                }
200
            }
201 2
            $builder->setContextKey($currentContextKey);
202
        }
203
        else {
204 2
            $ret = $builder->getExpose() ? $value : null;
205
        }
206
        
207 2
        return $ret;
208
    }
209
    
210
    /**
211
     * Here to parse the columns parameter into some kind of flatten array with
212
     * the key path separated by dot "my.path" and the value true, false or a callback function
213
     * including the ExposeBuilder object
214
     *
215
     * @param iterable|null $columns
216
     * @param string|null $context
217
     *
218
     * @return array|null
219
     */
220 2
    public static function parseColumnsRecursive(?iterable $columns = null, ?string $context = null): ?array
221
    {
222 2
        if (!isset($columns)) {
223 1
            return null;
224
        }
225 2
        $ret = [];
226 2
        foreach ($columns as $key => $value) {
227 2
            if (is_bool($key)) {
228
                $value = $key;
229
                $key = null;
230
            }
231
            
232 2
            if (is_int($key)) {
233 2
                if (is_string($value)) {
234 1
                    $key = $value;
235 1
                    $value = true;
236
                }
237
                else {
238 2
                    $key = null;
239
                }
240
            }
241
            
242 2
            if (is_string($key)) {
243 2
                $key = trim(mb_strtolower($key));
244
            }
245
            
246 2
            if (is_string($value) && empty($value)) {
247
                $value = true;
248
            }
249
            
250 2
            $currentKey = (!empty($context) ? $context . (!empty($key) ? '.' : null) : null) . $key;
251 2
            if (is_callable($value)) {
252
                $ret[$currentKey] = $value;
253
            }
254 2
            else if (is_iterable($value)) {
255 1
                $subRet = self::parseColumnsRecursive($value, $currentKey);
256 1
                $ret = array_merge_recursive($ret, $subRet ?? []);
257 1
                if (!isset($ret[$currentKey])) {
258 1
                    $ret[$currentKey] = false;
259
                }
260
            }
261
            else {
262 2
                $ret[$currentKey] = $value;
263
            }
264
        }
265
        
266 2
        return $ret;
267
    }
268
}
269