Completed
Push — master ( 0f0cd8...55e6de )
by Lee
06:12
created

ExposeFields   C

Complexity

Total Complexity 66

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 83.33%

Importance

Changes 13
Bugs 1 Features 1
Metric Value
wmc 66
c 13
b 1
f 1
lcom 1
cbo 8
dl 0
loc 416
ccs 160
cts 192
cp 0.8333
rs 5.7474

17 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 4 1
A __construct() 0 5 1
A configureExposeDepth() 0 16 2
A configurePushRequest() 0 12 3
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
        // Offset the array by one of it has a string key and is size of 1
116 3
        if (sizeof($pushed) == 1 && is_string(key($pushed))) {
117 3
            $rootKey = key($pushed);
118 3
            $pushed = $this->filterPushExpose($pushed[key($pushed)], $this->fields);
119
120 3
            return ResultSet::create($pushed, $rootKey);
121
        } else {
122
            throw DrestException::unableToHandleACollectionPush();
123
        }
124
    }
125
126
    /**
127
     * Filter out requested expose fields against what's allowed
128
     * @param  array $requested - The requested expose definition
129
     * @param  array $actual    - current allowed expose definition
130
     * @return array $request - The requested expose data with non-allowed data stripped off
131
     */
132 3
    protected function filterPushExpose($requested, $actual)
133
    {
134 3
        $requested = (array) $requested;
135 3
        $actual = (array) $actual;
136 3
        foreach ($requested as $requestedKey => $requestedValue) {
137 3
            if ($requestedKey !== 0 && in_array($requestedKey, $actual)) {
138 3
                continue;
139
            }
140
141 1
            if (is_array($requestedValue)) {
142 1
                if (is_string($requestedKey) && isset($actual[$requestedKey])) {
143 1
                    $requested[$requestedKey] = $this->filterPushExpose($requestedValue, $actual[$requestedKey]);
144 1
                    continue;
145 1
                } elseif (is_int($requestedKey)) {
146 1
                    $requested[$requestedKey] = $this->filterPushExpose($requestedValue, $actual);
147 1
                    continue;
148
                }
149
            } else {
150 1
                if (in_array($requestedKey, $actual)) {
151
                    continue;
152
                }
153
            }
154 1
            unset($requested[$requestedKey]);
155 3
        }
156
157 3
        return $requested;
158
    }
159
160
161
    /**
162
     * Configure the expose object to filter out fields that have been explicitly requested by the client.
163
     * This is only applicable for a HTTP pull (GET) call. For configuring
164
     * @param  array        $requestOptions
165
     * @param  Request      $request
166
     * @return ExposeFields $this object instance
167
     */
168 20
    public function configurePullRequest(array $requestOptions, Request $request)
169
    {
170 20
        if (empty($this->route_expose)) {
171 5
            $exposeString = '';
172 5
            foreach ($requestOptions as $requestOption => $requestValue) {
173
                switch ($requestOption) {
174 1
                    case Configuration::EXPOSE_REQUEST_HEADER:
175
                        $exposeString = $request->getHeaders($requestValue);
176
                        break;
177 1
                    case Configuration::EXPOSE_REQUEST_PARAM:
178
                        $exposeString = $request->getParams($requestValue);
179
                        break;
180 1
                    case Configuration::EXPOSE_REQUEST_PARAM_GET:
181
                        $exposeString = $request->getQuery($requestValue);
182
                        break;
183 1
                    case Configuration::EXPOSE_REQUEST_PARAM_POST:
184 1
                        $exposeString = $request->getPost($requestValue);
185 1
                        break;
186
                }
187 5
            }
188 5
            if (!empty($exposeString)) {
189 1
                $requestedExposure = $this->parseExposeString($exposeString);
190 1
                $this->filterRequestedExpose($requestedExposure, $this->fields);
191 1
                $this->fields = $requestedExposure;
192 1
            }
193 5
        }
194
195 20
        return $this;
196
    }
197
198
199
    /**
200
     * An awesome solution was posted on (link below) to parse these using a regex
201
     * http://stackoverflow.com/questions/16415558/regex-top-level-contents-from-a-string
202
     *
203
     *   preg_match_all(
204
     *       '/(?<=\[)     # Assert that the previous characters is a [
205
     *         (?:         # Match either...
206
     *          [^[\]]*    # any number of characters except brackets
207
     *         |           # or
208
     *          \[         # an opening bracket
209
     *          (?R)       # containing a match of this very regex
210
     *          \]         # followed by a closing bracket
211
     *         )*          # Repeat as needed
212
     *         (?=\])      # Assert the next character is a ]/x',
213
     *       $string, $result, PREG_PATTERN_ORDER);
214
     *
215
     * @todo: Adapt the parser to use the regex above (will also need alter it to grab parent keys)
216
     *
217
     * Parses an expose string into an array
218
     * Example: "username|email_address|profile[id|lastname|addresses[id]]|phone_numbers"
219
     * @param  string                       $string
220
     * @return array                        $result
221
     * @throws InvalidExposeFieldsException - if any syntax error occurs, or unable to parse the string
222
     */
223 6
    protected function parseExposeString($string)
224
    {
225 6
        $string = trim($string);
226 6
        if (preg_match("/[^a-zA-Z0-9\[\]\|_]/", $string) === 1) {
227 1
            throw InvalidExposeFieldsException::invalidExposeFieldsString();
228
        }
229
230 5
        $results = [];
231 5
        $this->recurseExposeString(trim($string, '|'), $results);
232
233 4
        return $results;
234
    }
235
236
    /**
237
     * Recursively process the passed expose string
238
     * @param  string                       $string  - the string to be processed
239
     * @param  array                        $results - passed by reference
240
     * @throws InvalidExposeFieldsException if unable to correctly parse the square brackets.
241
     */
242 5
    protected function recurseExposeString($string, &$results)
243
    {
244 5
        if (substr_count($string, '[') !== substr_count($string, ']')) {
245 1
            throw InvalidExposeFieldsException::unableToParseExposeString($string);
246
        }
247
248 4
        $results = (array) $results;
249
250 4
        $parts = $this->parseStringParts($string);
251 4
        foreach ($parts->parts as $part) {
252 2
            $this->recurseExposeString($part['contents'], $results[$part['tagName']]);
253 4
        }
254
255 4
        $results = array_merge(
256 4
            array_filter(
257 4
                explode('|', $parts->remaining_string),
258 4
                function ($item) {
259 4
                    return (empty($item)) ? false : true;
260
                }
261 4
            ),
262
            $results
263 4
        );
264 4
    }
265
266
    /**
267
     * Get information on parsed (top-level) brackets
268
     * @param  string    $string
269
     * @return \stdClass $information contains parse information object containing a $parts array eg array(
270
     *                          'openBracket' => xx,        - The position of the open bracket
271
     *                          'closeBracket' => xx        - The position of the close bracket
272
     *                          'contents' => xx            - The contents of the bracket
273
     *                          'tagName' => xx                - The name of the accompanying tag
274
     *                          )
275
     */
276 4
    private function parseStringParts($string)
277
    {
278 4
        $information = new \stdClass();
279 4
        $information->parts = [];
280 4
        $openPos = null;
281 4
        $closePos = null;
282 4
        $bracketCounter = 0;
283 4
        foreach (str_split($string) as $key => $char) {
284 4
            if ($char === '[') {
285 2
                if (is_null($openPos) && $bracketCounter === 0) {
286 2
                    $openPos = $key;
287 2
                }
288 2
                $bracketCounter++;
289 2
            }
290 4
            if ($char === ']') {
291 2
                if (is_null($closePos) && $bracketCounter === 1) {
292 2
                    $closePos = $key;
293 2
                }
294 2
                $bracketCounter--;
295 2
            }
296
297 4
            if (is_numeric($openPos) && is_numeric($closePos)) {
298
                // Work backwards from openPos until we hit [|]
299 2
                $stopPos = 0;
300 2
                foreach (['|', '[', ']', '%'] as $stopChar) {
301 2
                    if (($pos = strrpos(substr($string, 0, $openPos), $stopChar)) !== false) {
302 2
                        $stopPos = (++$pos > $stopPos) ? $pos : $stopPos;
303 2
                    }
304 2
                }
305
306 2
                if (($openPos + 1 === $closePos)) {
307
                    // Where no offset has been defined, blank out the [] characters
308
                    $rangeSize = ($closePos - $openPos) + 1;
309
                    $string = substr_replace($string, str_repeat('%', $rangeSize), $openPos, $rangeSize);
310
                } else {
311 2
                    $information->parts[] = [
312 2
                        'openBracket' => $openPos,
313 2
                        'closeBracket' => $closePos,
314 2
                        'contents' => substr($string, $openPos + 1, ($closePos - $openPos) - 1),
315 2
                        'tagName' => substr($string, $stopPos, ($openPos - $stopPos)),
316 2
                        'tagStart' => $stopPos,
317 2
                        'tagEnd' => ($openPos - 1)
318 2
                    ];
319 2
                    $rangeSize = ($closePos - $stopPos) + 1;
320 2
                    $string = substr_replace($string, str_repeat('%', $rangeSize), $stopPos, $rangeSize);
321
                }
322 2
                $openPos = $closePos = null;
323 2
            }
324 4
        }
325
326 4
        $string = str_replace('%', '', $string);
327 4
        $string = str_replace('||', '|', $string);
328
329 4
        $information->remaining_string = trim($string, '|');
330
331 4
        return $information;
332
    }
333
334
    /**
335
     * Filter out requested expose fields against what's allowed
336
     * @param array $requested - The requested expose definition - invalid / not allowed data is stripped off
337
     * @param array $actual    - current allowed expose definition
338
     */
339 1
    protected function filterRequestedExpose(&$requested, &$actual)
340
    {
341 1
        $actual = (array) $actual;
342 1
        foreach ($requested as $requestedKey => $requestedValue) {
343 1
            if (in_array($requestedValue, $actual)) {
344
                continue;
345
            }
346
347 1
            if (is_array($requestedValue)) {
348
                if (isset($actual[$requestedKey])) {
349
                    $this->filterRequestedExpose($requested[$requestedKey], $actual[$requestedKey]);
350
                    continue;
351
                }
352
            } else {
353 1
                if (isset($actual[$requestedValue]) && is_array($actual[$requestedValue]) && array_key_exists(
354
                        $requestedValue,
355
                        $actual
356
                    )
357 1
                ) {
358
                    continue;
359
                }
360
            }
361 1
            unset($requested[$requestedKey]);
362 1
        }
363 1
    }
364
365
    /**
366
     * Recursive function to generate default expose columns
367
     *
368
     * @param array                     $fields    - array to be populated recursively (referenced)
369
     * @param string                    $class     - name of the class to process
370
     * @param EntityManagerRegistry     $emr       - entity manager registry used to fetch class information
371
     * @param integer                   $depth     - maximum depth you want to travel through the relations
372
     * @param integer                   $fetchType - The fetch type to be used
373
     * @param integer|null              $fetchType - The required fetch type of the relation
374
     */
375 5
    protected function processExposeDepth(&$fields, $class, EntityManagerRegistry $emr, $depth = 0, $fetchType = null)
376
    {
377 5
        $this->registered_expose_classes[] = $class;
378 5
        if ($depth > 0) {
379
            /** @var \Doctrine\ORM\Mapping\ClassMetaData $metaData */
380 5
            $metaData = $emr->getManagerForClass($class)->getClassMetadata($class);
381 5
            $fields = $metaData->getColumnNames();
382
383 5
            if (($depth - 1) > 0) {
384 5
                --$depth;
385 5
                foreach ($metaData->getAssociationMappings() as $key => $assocMapping) {
386 4
                    if (!in_array($assocMapping['targetEntity'], $this->registered_expose_classes) && (is_null(
387
                                $fetchType
388 4
                            ) || ($assocMapping['fetch'] == $fetchType))
389 4
                    ) {
390 4
                        $this->processExposeDepth(
391 4
                            $fields[$key],
392 4
                            $assocMapping['targetEntity'],
393 4
                            $emr,
394 4
                            $depth,
395
                            $fetchType
396 4
                        );
397 4
                    }
398 5
                }
399 5
            }
400 5
        }
401 5
    }
402
403
    /**
404
     * Get the expose fields
405
     * @return array $fields
406
     */
407 21
    public function toArray()
408
    {
409 21
        if (!empty($this->route_expose)) {
410 16
            return $this->route_expose;
411
        }
412
413 6
        return $this->fields;
414
    }
415
416
    public function current()
417
    {
418
        return $this->fields[$this->position];
419
    }
420
421
    public function key()
422
    {
423
        return $this->position;
424
    }
425
426
    public function next()
427
    {
428
        ++$this->position;
429
    }
430
431
    public function rewind()
432
    {
433
        $this->position = 0;
434
    }
435
436
    public function valid()
437
    {
438
        return isset($this->fields[$this->position]);
439
    }
440
441
}
442