ExposeFields   C
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 83.33%

Importance

Changes 0
Metric Value
wmc 67
lcom 1
cbo 7
dl 0
loc 418
ccs 160
cts 192
cp 0.8333
rs 5.7097
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A create() 0 4 1
A configureExposeDepth() 0 16 2
A configurePushRequest() 0 14 4
D filterPushExpose() 0 27 9
C configurePullRequest() 0 29 8
A parseExposeString() 0 12 2
B recurseExposeString() 0 23 4
C parseStringParts() 0 57 14
C filterRequestedExpose() 0 25 8
C processExposeDepth() 0 27 7
A toArray() 0 8 2
A current() 0 4 1
A key() 0 4 1
A next() 0 4 1
A rewind() 0 4 1
A valid() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ExposeFields 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 ExposeFields, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the Drest package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @author Lee Davis
9
 * @copyright Copyright (c) Lee Davis <@leedavis81>
10
 * @link https://github.com/leedavis81/drest/blob/master/LICENSE
11
 * @license http://opensource.org/licenses/MIT The MIT X License (MIT)
12
 */
13
namespace Drest\Query;
14
15
use Drest\EntityManagerRegistry;
16
use Drest\Configuration;
17
use Drest\DrestException;
18
use Drest\Mapping\RouteMetaData;
19
use DrestCommon\Request\Request;
20
use DrestCommon\ResultSet;
21
22
/**
23
 * Class ExposeFields Handles processing logic for expose fields.
24
 * @package Drest\Query
25
 */
26
class ExposeFields implements \Iterator
27
{
28
    /**
29
     * Current iteration position
30
     * @var integer $position
31
     */
32
    private $position = 0;
33
34
    /**
35
     * Expose fields to be used - multidimensional array
36
     * @var array $fields
37
     */
38
    private $fields;
39
40
    /**
41
     * The matched route
42
     * @var RouteMetaData $route
43
     */
44
    protected $route;
45
46
    /**
47
     * The route expose array - If explicitly set it overrides any default config settings
48
     * @var array $route_expose
49
     */
50
    protected $route_expose;
51
52
    /**
53
     * An array of classes that are registered on default expose depth.
54
     * Temporary used during processExposeDepth to prevent you traversing up and down bi-directional relations
55
     * @var array $registered_expose_classes ;
56
     */
57
    protected $registered_expose_classes = [];
58
59
    /**
60
     * Create an instance of ExposeFields - use create() method
61
     * @param RouteMetaData $route - requires a matched route
62
     */
63 30
    private function __construct(RouteMetaData $route)
64
    {
65 30
        $this->route = $route;
66 30
        $this->route_expose = $route->getExpose();
67 30
    }
68
69
    /**
70
     * Create an instance of ExposeFields
71
     * @param  RouteMetaData             $route - requires a matched route
72
     * @return \Drest\Query\ExposeFields
73
     */
74 30
    public static function create(RouteMetaData $route)
75
    {
76 30
        return new self($route);
77
    }
78
79
    /**
80
     * Set the default exposure fields using the configured exposure depth
81
     * @param  EntityManagerRegistry    $emr
82
     * @param  integer                  $exposureDepth
83
     * @param  integer                  $exposureRelationsFetchType
84
     * @return ExposeFields             $this object instance
85
     */
86 22
    public function configureExposeDepth(EntityManagerRegistry $emr, $exposureDepth = 0, $exposureRelationsFetchType = null)
87
    {
88 22
        if (!empty($this->route_expose)) {
89 18
            $this->fields = $this->route_expose;
90 18
        } else {
91 5
            $this->processExposeDepth(
92 5
                $this->fields,
93 5
                $this->route->getClassMetaData()->getClassName(),
94 5
                $emr,
95 5
                $exposureDepth,
96
                $exposureRelationsFetchType
97 5
            );
98
        }
99
100 22
        return $this;
101
    }
102
103
    /**
104
     * Configure the expose object to filter out fields that are not allowed to be use by the client.
105
     * Unlike the configuring of the Pull request, this function will return the formatted array in a ResultSet object
106
     * This is only applicable for a HTTP push (POST/PUT/PATCH) call
107
     * @param  array                  $pushed - the data push on the request
108
     * @throws \Drest\DrestException
109
     * @return \DrestCommon\ResultSet
110
     *
111
     * @todo: this should follow the same pattern as configurePullRequest
112
     */
113 3
    public function configurePushRequest($pushed)
114
    {
115
        // This is horrible and silly, we need to remove wrapping
116 3
        if (sizeof($pushed) == 1 && is_string(key($pushed)) && is_array($pushed[key($pushed)])) {
117
            // We're assumming that if there's only one element, and it's and array, then we've wrapped data in a key
118
            // @todo: This assumption is silly, and needs to be removed
119 3
            $rootKey = key($pushed);
120 3
            $pushed = $this->filterPushExpose($pushed[key($pushed)], $this->fields);
121
122 3
            return ResultSet::create($pushed, $rootKey);
123
        } else {
124
            return ResultSet::create($pushed);
125
        }
126
    }
127
128
    /**
129
     * Filter out requested expose fields against what's allowed
130
     * @param  array $requested - The requested expose definition
131
     * @param  array $actual    - current allowed expose definition
132
     * @return array $request - The requested expose data with non-allowed data stripped off
133
     */
134 3
    protected function filterPushExpose($requested, $actual)
135
    {
136 3
        $requested = (array) $requested;
137 3
        $actual = (array) $actual;
138 3
        foreach ($requested as $requestedKey => $requestedValue) {
139 3
            if ($requestedKey !== 0 && in_array($requestedKey, $actual)) {
140 3
                continue;
141
            }
142
143 1
            if (is_array($requestedValue)) {
144 1
                if (is_string($requestedKey) && isset($actual[$requestedKey])) {
145 1
                    $requested[$requestedKey] = $this->filterPushExpose($requestedValue, $actual[$requestedKey]);
146 1
                    continue;
147 1
                } elseif (is_int($requestedKey)) {
148 1
                    $requested[$requestedKey] = $this->filterPushExpose($requestedValue, $actual);
149 1
                    continue;
150
                }
151
            } else {
152 1
                if (in_array($requestedKey, $actual)) {
153
                    continue;
154
                }
155
            }
156 1
            unset($requested[$requestedKey]);
157 3
        }
158
159 3
        return $requested;
160
    }
161
162
163
    /**
164
     * Configure the expose object to filter out fields that have been explicitly requested by the client.
165
     * This is only applicable for a HTTP pull (GET) call. For configuring
166
     * @param  array        $requestOptions
167
     * @param  Request      $request
168
     * @return ExposeFields $this object instance
169
     */
170 20
    public function configurePullRequest(array $requestOptions, Request $request)
171
    {
172 20
        if (empty($this->route_expose)) {
173 5
            $exposeString = '';
174 5
            foreach ($requestOptions as $requestOption => $requestValue) {
175
                switch ($requestOption) {
176 1
                    case Configuration::EXPOSE_REQUEST_HEADER:
177
                        $exposeString = $request->getHeaders($requestValue);
178
                        break;
179 1
                    case Configuration::EXPOSE_REQUEST_PARAM:
180
                        $exposeString = $request->getParams($requestValue);
181
                        break;
182 1
                    case Configuration::EXPOSE_REQUEST_PARAM_GET:
183
                        $exposeString = $request->getQuery($requestValue);
184
                        break;
185 1
                    case Configuration::EXPOSE_REQUEST_PARAM_POST:
186 1
                        $exposeString = $request->getPost($requestValue);
187 1
                        break;
188
                }
189 5
            }
190 5
            if (!empty($exposeString)) {
191 1
                $requestedExposure = $this->parseExposeString($exposeString);
192 1
                $this->filterRequestedExpose($requestedExposure, $this->fields);
193 1
                $this->fields = $requestedExposure;
194 1
            }
195 5
        }
196
197 20
        return $this;
198
    }
199
200
201
    /**
202
     * An awesome solution was posted on (link below) to parse these using a regex
203
     * http://stackoverflow.com/questions/16415558/regex-top-level-contents-from-a-string
204
     *
205
     *   preg_match_all(
206
     *       '/(?<=\[)     # Assert that the previous characters is a [
207
     *         (?:         # Match either...
208
     *          [^[\]]*    # any number of characters except brackets
209
     *         |           # or
210
     *          \[         # an opening bracket
211
     *          (?R)       # containing a match of this very regex
212
     *          \]         # followed by a closing bracket
213
     *         )*          # Repeat as needed
214
     *         (?=\])      # Assert the next character is a ]/x',
215
     *       $string, $result, PREG_PATTERN_ORDER);
216
     *
217
     * @todo: Adapt the parser to use the regex above (will also need alter it to grab parent keys)
218
     *
219
     * Parses an expose string into an array
220
     * Example: "username|email_address|profile[id|lastname|addresses[id]]|phone_numbers"
221
     * @param  string                       $string
222
     * @return array                        $result
223
     * @throws InvalidExposeFieldsException - if any syntax error occurs, or unable to parse the string
224
     */
225 6
    protected function parseExposeString($string)
226
    {
227 6
        $string = trim($string);
228 6
        if (preg_match("/[^a-zA-Z0-9\[\]\|_]/", $string) === 1) {
229 1
            throw InvalidExposeFieldsException::invalidExposeFieldsString();
230
        }
231
232 5
        $results = [];
233 5
        $this->recurseExposeString(trim($string, '|'), $results);
234
235 4
        return $results;
236
    }
237
238
    /**
239
     * Recursively process the passed expose string
240
     * @param  string                       $string  - the string to be processed
241
     * @param  array                        $results - passed by reference
242
     * @throws InvalidExposeFieldsException if unable to correctly parse the square brackets.
243
     */
244 5
    protected function recurseExposeString($string, &$results)
245
    {
246 5
        if (substr_count($string, '[') !== substr_count($string, ']')) {
247 1
            throw InvalidExposeFieldsException::unableToParseExposeString($string);
248
        }
249
250 4
        $results = (array) $results;
251
252 4
        $parts = $this->parseStringParts($string);
253 4
        foreach ($parts->parts as $part) {
254 2
            $this->recurseExposeString($part['contents'], $results[$part['tagName']]);
255 4
        }
256
257 4
        $results = array_merge(
258 4
            array_filter(
259 4
                explode('|', $parts->remaining_string),
260 4
                function ($item) {
261 4
                    return (empty($item)) ? false : true;
262
                }
263 4
            ),
264
            $results
265 4
        );
266 4
    }
267
268
    /**
269
     * Get information on parsed (top-level) brackets
270
     * @param  string    $string
271
     * @return \stdClass $information contains parse information object containing a $parts array eg array(
272
     *                          'openBracket' => xx,        - The position of the open bracket
273
     *                          'closeBracket' => xx        - The position of the close bracket
274
     *                          'contents' => xx            - The contents of the bracket
275
     *                          'tagName' => xx                - The name of the accompanying tag
276
     *                          )
277
     */
278 4
    private function parseStringParts($string)
279
    {
280 4
        $information = new \stdClass();
281 4
        $information->parts = [];
282 4
        $openPos = null;
283 4
        $closePos = null;
284 4
        $bracketCounter = 0;
285 4
        foreach (str_split($string) as $key => $char) {
286 4
            if ($char === '[') {
287 2
                if (is_null($openPos) && $bracketCounter === 0) {
288 2
                    $openPos = $key;
289 2
                }
290 2
                $bracketCounter++;
291 2
            }
292 4
            if ($char === ']') {
293 2
                if (is_null($closePos) && $bracketCounter === 1) {
294 2
                    $closePos = $key;
295 2
                }
296 2
                $bracketCounter--;
297 2
            }
298
299 4
            if (is_numeric($openPos) && is_numeric($closePos)) {
300
                // Work backwards from openPos until we hit [|]
301 2
                $stopPos = 0;
302 2
                foreach (['|', '[', ']', '%'] as $stopChar) {
303 2
                    if (($pos = strrpos(substr($string, 0, $openPos), $stopChar)) !== false) {
304 2
                        $stopPos = (++$pos > $stopPos) ? $pos : $stopPos;
305 2
                    }
306 2
                }
307
308 2
                if (($openPos + 1 === $closePos)) {
309
                    // Where no offset has been defined, blank out the [] characters
310
                    $rangeSize = ($closePos - $openPos) + 1;
311
                    $string = substr_replace($string, str_repeat('%', $rangeSize), $openPos, $rangeSize);
312
                } else {
313 2
                    $information->parts[] = [
314 2
                        'openBracket' => $openPos,
315 2
                        'closeBracket' => $closePos,
316 2
                        'contents' => substr($string, $openPos + 1, ($closePos - $openPos) - 1),
317 2
                        'tagName' => substr($string, $stopPos, ($openPos - $stopPos)),
318 2
                        'tagStart' => $stopPos,
319 2
                        'tagEnd' => ($openPos - 1)
320 2
                    ];
321 2
                    $rangeSize = ($closePos - $stopPos) + 1;
322 2
                    $string = substr_replace($string, str_repeat('%', $rangeSize), $stopPos, $rangeSize);
323
                }
324 2
                $openPos = $closePos = null;
325 2
            }
326 4
        }
327
328 4
        $string = str_replace('%', '', $string);
329 4
        $string = str_replace('||', '|', $string);
330
331 4
        $information->remaining_string = trim($string, '|');
332
333 4
        return $information;
334
    }
335
336
    /**
337
     * Filter out requested expose fields against what's allowed
338
     * @param array $requested - The requested expose definition - invalid / not allowed data is stripped off
339
     * @param array $actual    - current allowed expose definition
340
     */
341 1
    protected function filterRequestedExpose(&$requested, &$actual)
342
    {
343 1
        $actual = (array) $actual;
344 1
        foreach ($requested as $requestedKey => $requestedValue) {
345 1
            if (in_array($requestedValue, $actual)) {
346
                continue;
347
            }
348
349 1
            if (is_array($requestedValue)) {
350
                if (isset($actual[$requestedKey])) {
351
                    $this->filterRequestedExpose($requested[$requestedKey], $actual[$requestedKey]);
352
                    continue;
353
                }
354
            } else {
355 1
                if (isset($actual[$requestedValue]) && is_array($actual[$requestedValue]) && array_key_exists(
356
                        $requestedValue,
357
                        $actual
358
                    )
359 1
                ) {
360
                    continue;
361
                }
362
            }
363 1
            unset($requested[$requestedKey]);
364 1
        }
365 1
    }
366
367
    /**
368
     * Recursive function to generate default expose columns
369
     *
370
     * @param array                     $fields    - array to be populated recursively (referenced)
371
     * @param string                    $class     - name of the class to process
372
     * @param EntityManagerRegistry     $emr       - entity manager registry used to fetch class information
373
     * @param integer                   $depth     - maximum depth you want to travel through the relations
374
     * @param integer                   $fetchType - The fetch type to be used
375
     * @param integer|null              $fetchType - The required fetch type of the relation
376
     */
377 5
    protected function processExposeDepth(&$fields, $class, EntityManagerRegistry $emr, $depth = 0, $fetchType = null)
378
    {
379 5
        $this->registered_expose_classes[] = $class;
380 5
        if ($depth > 0) {
381
            /** @var \Doctrine\ORM\Mapping\ClassMetaData $metaData */
382 5
            $metaData = $emr->getManagerForClass($class)->getClassMetadata($class);
383 5
            $fields = $metaData->getColumnNames();
384
385 5
            if (($depth - 1) > 0) {
386 5
                --$depth;
387 5
                foreach ($metaData->getAssociationMappings() as $key => $assocMapping) {
388 4
                    if (!in_array($assocMapping['targetEntity'], $this->registered_expose_classes) && (is_null(
389
                                $fetchType
390 4
                            ) || ($assocMapping['fetch'] == $fetchType))
391 4
                    ) {
392 4
                        $this->processExposeDepth(
393 4
                            $fields[$key],
394 4
                            $assocMapping['targetEntity'],
395 4
                            $emr,
396 4
                            $depth,
397
                            $fetchType
398 4
                        );
399 4
                    }
400 5
                }
401 5
            }
402 5
        }
403 5
    }
404
405
    /**
406
     * Get the expose fields
407
     * @return array $fields
408
     */
409 21
    public function toArray()
410
    {
411 21
        if (!empty($this->route_expose)) {
412 16
            return $this->route_expose;
413
        }
414
415 6
        return $this->fields;
416
    }
417
418
    public function current()
419
    {
420
        return $this->fields[$this->position];
421
    }
422
423
    public function key()
424
    {
425
        return $this->position;
426
    }
427
428
    public function next()
429
    {
430
        ++$this->position;
431
    }
432
433
    public function rewind()
434
    {
435
        $this->position = 0;
436
    }
437
438
    public function valid()
439
    {
440
        return isset($this->fields[$this->position]);
441
    }
442
443
}
444