Exposer::expose()   B
last analyzed

Complexity

Conditions 10
Paths 10

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 10.0751

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 10
eloc 22
c 1
b 0
f 1
nc 10
nop 1
dl 0
loc 35
ccs 20
cts 22
cp 0.9091
crap 10.0751
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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