Completed
Push — master ( 047943...ada7af )
by Lee
05:25
created

AbstractAction   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 88.46%

Importance

Changes 14
Bugs 0 Features 1
Metric Value
wmc 51
c 14
b 0
f 1
lcom 1
cbo 12
dl 0
loc 362
ccs 138
cts 156
cp 0.8846
rs 8.3206

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getService() 0 4 1
A getMatchedRoute() 0 4 1
A getEntityManager() 0 4 1
A getEntityManagerRegistry() 0 4 1
A getResponse() 0 4 1
A getRequest() 0 4 1
A getRepresentation() 0 4 1
A handleError() 0 4 1
A registerExposeFromMetaData() 0 8 1
B registerExpose() 0 55 9
B getFilteredAssociations() 0 19 6
C createResultSet() 0 29 7
D removeAddedKeyFields() 0 25 10
B performDefaultUpdateAction() 0 40 5
A runHandle() 0 9 2
A getAlias() 0 16 2
A setService() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractAction 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 AbstractAction, 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\Service\Action;
14
15
use Doctrine\ORM;
16
use Drest\Mapping\RouteMetaData;
17
use Drest\Service;
18
use DrestCommon\Error\Response\ResponseInterface;
19
use DrestCommon\Representation\AbstractRepresentation;
20
use DrestCommon\Request\Request;
21
use DrestCommon\Response\Response;
22
use DrestCommon\ResultSet;
23
24
/**
25
 * abstract action class.
26
 * This should be extended for creating and registering custom service actions
27
 * @author Lee
28
 *
29
 */
30
abstract class AbstractAction implements ActionInterface
31
{
32
    /**
33
     * The service class this action is registered to
34
     * @var Service $service
35
     */
36
    protected $service;
37
38
    /**
39
     * additional key fields that are included in partial queries to make the DQL valid
40
     * These columns should be purged from the result set
41
     * @var array $addedKeyFields
42
     */
43
    protected $addedKeyFields;
44
45
    /**
46
     * Set the service object
47
     * @param Service $service
48
     */
49 25
    public function setService(Service $service)
50
    {
51 25
        $this->service = $service;
52 25
    }
53
54
    /**
55
     * Get the service class this action is registered against
56
     * @return Service
57
     */
58
    protected function getService()
59
    {
60
        return $this->service;
61
    }
62
63
    /**
64
     * get the matched route object
65
     * @return RouteMetaData $route
66
     */
67 25
    protected function getMatchedRoute()
68
    {
69 25
        return $this->service->getMatchedRoute();
70
    }
71
72
    /**
73
     * Get the entity manager from the service object (uses the default manager)
74
     * @return \Doctrine\ORM\EntityManager $em
75
     */
76 25
    protected function getEntityManager()
77
    {
78 25
        return $this->getEntityManagerRegistry()->getManager();
79
    }
80
81
    /**
82
     * Get the entity manager registry
83
     * @return \Drest\EntityManagerRegistry
84
     */
85 25
    protected function getEntityManagerRegistry()
86
    {
87 25
        return $this->service->getEntityManagerRegistry();
88
    }
89
90
    /**
91
     * Get the response object
92
     * @return Response $response
93
     */
94 6
    protected function getResponse()
95
    {
96 6
        return $this->service->getResponse();
97
    }
98
99
    /**
100
     * Get the request object
101
     * @return Request $request
102
     */
103 3
    protected function getRequest()
104
    {
105 3
        return $this->service->getRequest();
106
    }
107
108
    /**
109
     * Get the predetermined representation
110
     * @return AbstractRepresentation
111
     */
112 3
    public function getRepresentation()
113
    {
114 3
        return $this->service->getRepresentation();
115
    }
116
117
    /**
118
     * Handle an error - set the resulting error document to the response object
119
     * @param  \Exception        $e
120
     * @param  integer           $defaultResponseCode the default response code to use if no match on exception type occurs
121
     * @param  ResponseInterface $errorDocument
122
     * @return ResultSet         the error result set
123
     */
124 5
    public function handleError(\Exception $e, $defaultResponseCode = 500, ResponseInterface $errorDocument = null)
125
    {
126 5
        return $this->service->handleError($e, $defaultResponseCode, $errorDocument);
127
    }
128
129
    /**
130
     * Register the expose details from given metadata, typically used when setting up GET single/collection endpoints
131
     * @param ORM\QueryBuilder $qb
132
     * @param ORM\Mapping\ClassMetadata $classMetaData
133
     */
134 17
    protected function registerExposeFromMetaData(ORM\QueryBuilder $qb, ORM\Mapping\ClassMetadata $classMetaData)
135
    {
136 17
        $this->registerExpose(
137 17
            $this->getMatchedRoute()->getExpose(),
138 17
            $qb,
139
            $classMetaData
140 17
        );
141 17
    }
142
143
    /**
144
     * A recursive function to process the specified expose fields for a fetch request (GET)
145
     * @param  array                     $fields         - expose fields to process
146
     * @param  ORM\QueryBuilder $qb
147
     * @param  ORM\Mapping\ClassMetadata $classMetaData
148
     * @param $rootAlias - table alias to be used on SQL query
149
     * @param  array                     $addedKeyFields
150
     * @return ORM\QueryBuilder
151
     */
152 17
    protected function registerExpose(
153
        $fields,
154
        ORM\QueryBuilder $qb,
155
        ORM\Mapping\ClassMetadata $classMetaData,
156
        $rootAlias = null,
157
        &$addedKeyFields = []
158
    ) {
159 17
        if (empty($fields)) {
160
            return $qb;
161
        }
162
163 17
        $rootAlias = (is_null($rootAlias)) ? self::getAlias($classMetaData->getName()) : $rootAlias;
164
165 17
        $addedKeyFields = (array) $addedKeyFields;
166 17
        $ormAssociationMappings = $classMetaData->getAssociationMappings();
167
168
        // Process single fields into a partial set - Filter fields not available on class meta data
169 17
        $selectFields = $this->getFilteredAssociations($fields, $classMetaData, 'fields');
170
171
        // merge required identifier fields with select fields
172 17
        $keyFieldDiff = array_diff($classMetaData->getIdentifierFieldNames(), $selectFields);
173 17
        if (!empty($keyFieldDiff)) {
174 14
            $addedKeyFields = $keyFieldDiff;
175 14
            $selectFields = array_merge($selectFields, $keyFieldDiff);
176 14
        }
177
178 17
        if (!empty($selectFields)) {
179 17
            $qb->addSelect('partial ' . $rootAlias . '.{' . implode(', ', $selectFields) . '}');
180 17
        }
181
182
        // Process relational field with no deeper expose restrictions
183 17
        foreach ($this->getFilteredAssociations($fields, $classMetaData, 'association') as $relationalField) {
184
            $alias = self::getAlias($ormAssociationMappings[$relationalField]['targetEntity'], $relationalField);
185
            $qb->leftJoin($rootAlias . '.' . $relationalField, $alias);
186
            $qb->addSelect($alias);
187 17
        }
188
189 17
        foreach ($fields as $key => $value) {
190 17
            if (is_array($value) && isset($ormAssociationMappings[$key])) {
191 2
                $alias = self::getAlias($ormAssociationMappings[$key]['targetEntity'], $key);
192 2
                $qb->leftJoin($rootAlias . '.' . $key, $alias);
193 2
                $qb = $this->registerExpose(
194 2
                    $value,
195 2
                    $qb,
196 2
                    $this->getEntityManager()->getClassMetadata($ormAssociationMappings[$key]['targetEntity']),
197 2
                    $alias,
198 2
                    $addedKeyFields[$key]
199 2
                );
200 2
            }
201 17
        }
202
203 17
        $this->addedKeyFields = $addedKeyFields;
204
205 17
        return $qb;
206
    }
207
208
    /**
209
     * Get filtered associations
210
     * @param array $fields
211
     * @param ORM\Mapping\ClassMetadata $classMetaData
212
     * @param string $type 'fields' or 'association
213
     * @return array
214
     */
215 17
    protected function getFilteredAssociations(array $fields, ORM\Mapping\ClassMetadata $classMetaData, $type = 'fields')
216
    {
217
        // Process relational fields / association with no deeper expose restrictions
218 17
        if ($type !== 'fields' && $type !== 'associations')
219 17
        {
220 17
            return [];
221
        }
222
223 17
        $names = ($type === 'fields') ? $classMetaData->getFieldNames() : $classMetaData->getAssociationNames();
224 17
        return array_filter(
225 17
            $fields,
226 17
            function ($offset) use ($names) {
227 17
                if (!is_array($offset) && in_array($offset, $names)) {
228 17
                    return true;
229
                }
230 16
                return false;
231
            }
232 17
        );
233
    }
234
235
    /**
236
     * Method used to write to the $data array.
237
     * -    wraps results in a single entry array keyed by entity name.
238
     *        Eg array(user1, user2) becomes array('users' => array(user1, user2)) - this is useful for a more descriptive output of collection resources
239
     * -    Removes any addition expose fields required for a partial DQL query
240
     * @param  array     $data    - the data fetched from the database
241
     * @param  string    $keyName - the key name to use to wrap the data in. If null will attempt to pluralise the entity name on collection request, or singularize on single element request
242
     * @return ResultSet $data
243
     */
244 14
    public function createResultSet(array $data, $keyName = null)
245
    {
246 14
        $matchedRoute = $this->getMatchedRoute();
247 14
        $classMetaData = $matchedRoute->getClassMetaData();
248
249
        // Recursively remove any additionally added pk fields ($data must be a single record hierarchy. Iterate if we're getting a collection)
250 14
        if ($matchedRoute->isCollection()) {
251 13
            $dataSize = sizeof($data);
252 13
            for ($x = 0; $x < $dataSize; $x++) {
253 1
                $this->removeAddedKeyFields($this->addedKeyFields, $data[$x]);
254 1
            }
255 13
        } else {
256 1
            $this->removeAddedKeyFields($this->addedKeyFields, $data);
257
        }
258
259 14
        if (is_null($keyName)) {
260 14
            reset($data);
261 14
            if (sizeof($data) === 1 && is_string(key($data))) {
262
                // Use the single keyed array as the result set key
263
                $keyName = key($data);
264
                $data = $data[key($data)];
265
            } else {
266 14
                $keyName = ($matchedRoute->isCollection()) ? $classMetaData->getCollectionName(
267 14
                ) : $classMetaData->getElementName();
268
            }
269 14
        }
270
271 14
        return ResultSet::create($data, $keyName);
272
    }
273
274
275
    /**
276
     * Functional recursive method to remove any fields added to make the partial DQL work and remove the data
277
     * @param  array $addedKeyFields
278
     * @param  array $data           - pass by reference
279
     * @return array
280
     */
281 2
    protected function removeAddedKeyFields($addedKeyFields, &$data)
282
    {
283 2
        $addedKeyFields = (array) $addedKeyFields;
284 2
        foreach ($data as $key => $value) {
285 2
            if (is_array($value) && isset($addedKeyFields[$key])) {
286 1
                if (is_int($key)) {
287
                    $valueSize = sizeof($value);
288
                    for ($x = 0; $x <= $valueSize; $x++) {
289
                        if (isset($data[$x]) && is_array($data[$x])) {
290
                            $this->removeAddedKeyFields($addedKeyFields[$key], $data[$x]);
291
                        }
292
                    }
293
                } else {
294 1
                    $this->removeAddedKeyFields($addedKeyFields[$key], $data[$key]);
295
                }
296
297 1
            } else {
298 2
                if (is_array($addedKeyFields) && in_array($key, $addedKeyFields)) {
299 1
                    unset($data[$key]);
300 1
                }
301
            }
302 2
        }
303
304 2
        return $data;
305
    }
306
307
308
    /**
309
     * Perform a default update action. Logic between PUT and PATCH (not POST) are identical
310
     * If things change then we can start to break this function down
311
     * @return ResultSet
312
     */
313 2
    protected function performDefaultUpdateAction()
314
    {
315 2
        $matchedRoute = $this->getMatchedRoute();
316 2
        $classMetaData = $matchedRoute->getClassMetaData();
317 2
        $elementName = $classMetaData->getEntityAlias();
318
319 2
        $em = $this->getEntityManager();
320
321 2
        $qb = $em->createQueryBuilder()->select($elementName)->from($classMetaData->getClassName(), $elementName);
322 2
        foreach ($matchedRoute->getRouteParams() as $key => $value) {
323 2
            $qb->andWhere($elementName . '.' . $key . ' = :' . $key);
324 2
            $qb->setParameter($key, $value);
325 2
        }
326
327
        try {
328 2
            $object = $qb->getQuery()->getSingleResult(ORM\Query::HYDRATE_OBJECT);
329 2
        } catch (\Exception $e) {
330
            return $this->handleError($e, Response::STATUS_CODE_404);
331
        }
332
333 2
        $this->runHandle($object);
334
335
        // Attempt to save the modified resource
336
        try {
337 2
            $em->persist($object);
338 2
            $em->flush($object);
339
340 2
            $location = $matchedRoute->getOriginLocation(
341 2
                $object,
342 2
                $this->getRequest()->getUrl(),
343 2
                $this->getEntityManager()
344 2
            );
345 2
            $this->getResponse()->setStatusCode(Response::STATUS_CODE_200);
346 2
            $resultSet = ResultSet::create(['location' => ($location) ? $location : 'unknown'], 'response');
347 2
        } catch (\Exception $e) {
348
            return $this->handleError($e, Response::STATUS_CODE_500);
349
        }
350
351 2
        return $resultSet;
352
    }
353
354
355
    /**
356
     * Run the handle call on an entity object
357
     * @param object $object
358
     */
359 3
    protected function runHandle($object)
360
    {
361 3
        $matchedRoute = $this->getMatchedRoute();
362
        // Run any attached handle function
363 3
        if ($matchedRoute->hasHandleCall()) {
364 3
            $handleMethod = $matchedRoute->getHandleCall();
365 3
            $object->$handleMethod($this->getRepresentation()->toArray(false), $this->getRequest());
366 3
        }
367 3
    }
368
369
    /**
370
     * Get a unique alias name from an entity class name and relation field
371
     * @param  string $className - The class of the related entity
372
     * @param  string $fieldName - The field the relation is on. Typical to root when using top level.
373
     * @return string
374
     */
375 24
    public static function getAlias($className, $fieldName = 'rt')
376
    {
377 24
        $classNameParts = explode('\\', $className);
378 24
        if (sizeof($classNameParts) > 1) {
379 24
            $hash = preg_replace(
380 24
                '/[0-9_\/]+/',
381 24
                '',
382 24
                base64_encode(sha1(implode('', array_slice($classNameParts, 0, -1)) . $fieldName))
383 24
            );
384 24
            $className = array_pop($classNameParts);
385 24
        } else {
386 1
            $hash = preg_replace('/[0-9_\/]+/', '', base64_encode(sha1($fieldName)));
387
        }
388
389 24
        return strtolower(preg_replace("/[^a-zA-Z_\s]/", "", substr($hash, 0, 5) . '_' . $className));
390
    }
391
}
392