Completed
Push — develop ( 519aee...e125f2 )
by
unknown
16s
created

RestController::patchAction()   B

Complexity

Conditions 5
Paths 84

Size

Total Lines 62
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 62
ccs 0
cts 43
cp 0
rs 8.6652
cc 5
eloc 36
nc 84
nop 2
crap 30

How to fix   Long Method   

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
 * basic rest controller
4
 */
5
6
namespace Graviton\RestBundle\Controller;
7
8
use Graviton\DocumentBundle\Service\CollectionCache;
9
use Graviton\ExceptionBundle\Exception\InvalidJsonPatchException;
10
use Graviton\ExceptionBundle\Exception\MalformedInputException;
11
use Graviton\ExceptionBundle\Exception\SerializationException;
12
use Graviton\RestBundle\Model\DocumentModel;
13
use Graviton\RestBundle\Service\RestUtils;
14
use Graviton\SchemaBundle\SchemaUtils;
15
use Graviton\SecurityBundle\Service\SecurityUtils;
16
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\HttpFoundation\Response;
20
use Symfony\Component\Routing\Exception\RouteNotFoundException;
21
use Symfony\Bundle\FrameworkBundle\Routing\Router;
22
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
23
use Rs\Json\Patch;
24
use Graviton\RestBundle\Service\JsonPatchValidator;
25
26
/**
27
 * This is a basic rest controller. It should fit the most needs but if you need to add some
28
 * extra functionality you can extend it and overwrite single/all actions.
29
 * You can also extend the model class to add some extra logic before save
30
 *
31
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
32
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
33
 * @link     http://swisscom.ch
34
 */
35
class RestController
36
{
37
    /**
38
     * @var DocumentModel
39
     */
40
    private $model;
41
42
    /**
43
     * @var ContainerInterface service_container
44
     */
45
    private $container;
46
47
    /**
48
     * @var Response
49
     */
50
    private $response;
51
52
    /**
53
     * @var SchemaUtils
54
     */
55
    private $schemaUtils;
56
57
    /**
58
     * @var RestUtils
59
     */
60
    protected $restUtils;
61
62
    /**
63
     * @var Router
64
     */
65
    private $router;
66
67
    /**
68
     * @var EngineInterface
69
     */
70
    private $templating;
71
72
    /**
73
     * @var JsonPatchValidator
74
     */
75
    private $jsonPatchValidator;
76
77
    /**
78
     * @var SecurityUtils
79
     */
80
    protected $securityUtils;
81
82
    /** @var CollectionCache */
83
    protected $collectionCache;
84
85
    /**
86
     * @param Response           $response    Response
87
     * @param RestUtils          $restUtils   Rest Utils
88
     * @param Router             $router      Router
89
     * @param EngineInterface    $templating  Templating
90
     * @param ContainerInterface $container   Container
91
     * @param SchemaUtils        $schemaUtils Schema utils
92
     * @param CollectionCache    $cache       Cache service
93
     * @param JsonPatchValidator $jsonPatch   Service for validation json patch
94
     * @param SecurityUtils      $security    The securityUtils service
95
     */
96
    public function __construct(
97
        Response $response,
98
        RestUtils $restUtils,
99
        Router $router,
100
        EngineInterface $templating,
101
        ContainerInterface $container,
102
        SchemaUtils $schemaUtils,
103
        CollectionCache $cache,
104
        JsonPatchValidator $jsonPatch,
105
        SecurityUtils $security
106
    ) {
107
        $this->response = $response;
108
        $this->restUtils = $restUtils;
109
        $this->router = $router;
110
        $this->templating = $templating;
111
        $this->container = $container;
112
        $this->schemaUtils = $schemaUtils;
113
        $this->collectionCache = $cache;
114
        $this->jsonPatchValidator = $jsonPatch;
115
        $this->securityUtils = $security;
116
    }
117
118
    /**
119
     * Get the container object
120
     *
121
     * @return \Symfony\Component\DependencyInjection\ContainerInterface
122
     *
123
     * @obsolete
124
     */
125
    public function getContainer()
126
    {
127
        return $this->container;
128
    }
129
130
    /**
131
     * Returns a single record
132
     *
133
     * @param Request $request Current http request
134
     * @param string  $id      ID of record
135
     *
136
     * @return \Symfony\Component\HttpFoundation\Response $response Response with result or error
137
     */
138
    public function getAction(Request $request, $id)
139
    {
140
        $document = $this->getModel()->getSerialised($id, $request);
141
142
        $response = $this->getResponse()
143
            ->setStatusCode(Response::HTTP_OK)
144
            ->setContent($document);
145
146
        return $response;
147
    }
148
149
    /**
150
     * Get the response object
151
     *
152
     * @return \Symfony\Component\HttpFoundation\Response $response Response object
153
     */
154
    public function getResponse()
155
    {
156
        return $this->response;
157
    }
158
159
    /**
160
     * Return the model
161
     *
162
     * @throws \Exception in case no model was defined.
163
     *
164
     * @return DocumentModel $model Model
165
     */
166
    public function getModel()
167
    {
168
        if (!$this->model) {
169
            throw new \Exception('No model is set for this controller');
170
        }
171
172
        return $this->model;
173
    }
174
175
    /**
176
     * Set the model class
177
     *
178
     * @param DocumentModel $model Model class
179
     *
180
     * @return self
181
     */
182
    public function setModel(DocumentModel $model)
183
    {
184
        $this->model = $model;
185
186
        return $this;
187
    }
188
189
    /**
190
     * Returns all records
191
     *
192
     * @param Request $request Current http request
193
     *
194
     * @return \Symfony\Component\HttpFoundation\Response $response Response with result or error
195
     */
196
    public function allAction(Request $request)
197
    {
198
        $model = $this->getModel();
199
200
        $content = $this->restUtils->serialize($model->findAll($request));
201
202
        $response = $this->getResponse()
203
            ->setStatusCode(Response::HTTP_OK)
204
            ->setContent($content);
205
206
        return $response;
207
    }
208
209
    /**
210
     * Writes a new Entry to the database
211
     *
212
     * @param Request $request Current http request
213
     *
214
     * @return \Symfony\Component\HttpFoundation\Response $response Result of action with data (if successful)
215
     */
216
    public function postAction(Request $request)
217
    {
218
        // Get the response object from container
219
        $response = $this->getResponse();
220
        $model = $this->getModel();
221
222
        $this->restUtils->checkJsonRequest($request, $response, $this->getModel());
223
224
        $record = $this->restUtils->validateRequest($request->getContent(), $model);
225
226
        // Insert the new record
227
        $record = $model->insertRecord($record);
228
229
        // store id of new record so we dont need to reparse body later when needed
230
        $request->attributes->set('id', $record->getId());
231
232
        // Set status code
233
        $response->setStatusCode(Response::HTTP_CREATED);
234
235
        $response->headers->set(
236
            'Location',
237
            $this->getRouter()->generate($this->restUtils->getRouteName($request), array('id' => $record->getId()))
238
        );
239
240
        return $response;
241
    }
242
243
    /**
244
     * Get the router from the dic
245
     *
246
     * @return Router
247
     */
248
    public function getRouter()
249
    {
250
        return $this->router;
251
    }
252
253
    /**
254
     * Update a record
255
     *
256
     * @param Number  $id      ID of record
257
     * @param Request $request Current http request
258
     *
259
     * @throws MalformedInputException
260
     *
261
     * @return Response $response Result of action with data (if successful)
262
     */
263
    public function putAction($id, Request $request)
264
    {
265
        $response = $this->getResponse();
266
        $model = $this->getModel();
267
        $repository = $model->getRepository();
268
269
        // Check and wait if another update is being processed
270
        $this->collectionCache->updateOperationCheck($repository, $id);
271
        $this->collectionCache->addUpdateLock($repository, $id);
272
273
        // Validate received data. On failure release the lock.
274
        try {
275
            $this->restUtils->checkJsonRequest($request, $response, $this->getModel());
276
            $record = $this->restUtils->validateRequest($request->getContent(), $model);
277
        } catch (\Exception $e) {
278
            $this->collectionCache->releaseUpdateLock($repository, $id);
279
            throw $e;
280
        }
281
282
        // handle missing 'id' field in input to a PUT operation
283
        // if it is settable on the document, let's set it and move on.. if not, inform the user..
284
        if ($record->getId() != $id) {
285
            // try to set it..
286
            if (is_callable(array($record, 'setId'))) {
287
                $record->setId($id);
288
            } else {
289
                $this->collectionCache->releaseUpdateLock($repository, $id);
290
                throw new MalformedInputException('No ID was supplied in the request payload.');
291
            }
292
        }
293
294
        // And update the record, if everything is ok
295
        if (!$this->getModel()->recordExists($id)) {
296
            $this->getModel()->insertRecord($record, false);
297
        } else {
298
            $this->getModel()->updateRecord($id, $record, false);
299
        }
300
301
        $this->collectionCache->releaseUpdateLock($repository, $id);
302
303
        // Set status code
304
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
305
306
        // store id of new record so we dont need to reparse body later when needed
307
        $request->attributes->set('id', $record->getId());
308
309
        return $response;
310
    }
311
312
    /**
313
     * Patch a record
314
     *
315
     * @param Number  $id      ID of record
316
     * @param Request $request Current http request
317
     *
318
     * @throws MalformedInputException
319
     *
320
     * @return Response $response Result of action with data (if successful)
321
     */
322
    public function patchAction($id, Request $request)
323
    {
324
        $response = $this->getResponse();
325
        $model = $this->getModel();
326
        $repository = $model->getRepository();
327
328
        // Check and wait if another update is being processed
329
        $this->collectionCache->updateOperationCheck($repository, $id);
330
        $this->collectionCache->addUpdateLock($repository, $id);
331
332
        // Validate received data. On failure release the lock.
333
        try {
334
            // Check JSON Patch request
335
            $this->restUtils->checkJsonRequest($request, $response, $model);
336
            $this->restUtils->checkJsonPatchRequest(json_decode($request->getContent(), 1));
337
338
            // Find record && apply $ref converter
339
            $jsonDocument = $model->getSerialised($id, null, true);
340
341
            try {
342
                // Check if valid
343
                $this->jsonPatchValidator->validate($jsonDocument, $request->getContent());
344
                // Apply JSON patches
345
                $patch = new Patch($jsonDocument, $request->getContent());
346
                $patchedDocument = $patch->apply();
347
            } catch (\Exception $e) {
348
                throw new InvalidJsonPatchException($e->getMessage());
349
            }
350
        } catch (\Exception $e) {
351
            throw $e;
352
        } finally {
353
            $this->collectionCache->releaseUpdateLock($repository, $id);
354
        }
355
356
        // if document hasn't changed, pass HTTP_NOT_MODIFIED and exit
357
        if ($jsonDocument == $patchedDocument) {
358
            $this->collectionCache->releaseUpdateLock($repository, $id);
359
            $response->setStatusCode(Response::HTTP_NOT_MODIFIED);
360
            return $response;
361
        }
362
363
        // Validate result object
364
        try {
365
            $record = $this->restUtils->validateRequest($patchedDocument, $model);
366
        } catch (\Exception $e) {
367
            $this->collectionCache->releaseUpdateLock($repository, $id);
368
            throw $e;
369
        }
370
371
        // Update object
372
        $this->getModel()->updateRecord($id, $record);
373
        $this->collectionCache->releaseUpdateLock($repository, $id);
374
375
        // Set status response code
376
        $response->setStatusCode(Response::HTTP_OK);
377
        $response->headers->set(
378
            'Content-Location',
379
            $this->getRouter()->generate($this->restUtils->getRouteName($request), array('id' => $record->getId()))
380
        );
381
382
        return $response;
383
    }
384
385
    /**
386
     * Deletes a record
387
     *
388
     * @param Number $id ID of record
389
     *
390
     * @return Response $response Result of the action
391
     */
392
    public function deleteAction($id)
393
    {
394
        $response = $this->getResponse();
395
        $this->model->deleteRecord($id);
396
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
397
398
        return $response;
399
    }
400
401
    /**
402
     * Return OPTIONS results.
403
     *
404
     * @param Request $request Current http request
405
     *
406
     * @throws SerializationException
407
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
408
     */
409
    public function optionsAction(Request $request)
410
    {
411
        list($app, $module, , $modelName) = explode('.', $request->attributes->get('_route'));
412
413
        $response = $this->response;
414
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
415
416
        // enabled methods for CorsListener
417
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
418
        try {
419
            $router = $this->getRouter();
420
            // if post route is available we assume everything is readable
421
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
422
        } catch (RouteNotFoundException $exception) {
423
            // only allow read methods
424
            $corsMethods = 'GET, OPTIONS';
425
        }
426
        $request->attributes->set('corsMethods', $corsMethods);
427
428
        return $response;
429
    }
430
431
432
    /**
433
     * Return schema GET results.
434
     *
435
     * @param Request $request Current http request
436
     * @param string  $id      ID of record
0 ignored issues
show
Documentation introduced by
Should the type for parameter $id not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
437
     *
438
     * @throws SerializationException
439
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
440
     */
441
    public function schemaAction(Request $request, $id = null)
442
    {
443
        $request->attributes->set('schemaRequest', true);
444
445
        list($app, $module, , $modelName, $schemaType) = explode('.', $request->attributes->get('_route'));
446
447
        $response = $this->response;
448
        $response->setStatusCode(Response::HTTP_OK);
449
        $response->setVary(['Origin', 'Accept-Encoding']);
450
        $response->setPublic();
451
452
        if (!$id && $schemaType != 'canonicalIdSchema') {
453
            $schema = $this->schemaUtils->getCollectionSchema($modelName, $this->getModel());
454
        } else {
455
            $schema = $this->schemaUtils->getModelSchema($modelName, $this->getModel());
456
        }
457
458
        // enabled methods for CorsListener
459
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
460
        try {
461
            $router = $this->getRouter();
462
            // if post route is available we assume everything is readable
463
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
464
        } catch (RouteNotFoundException $exception) {
465
            // only allow read methods
466
            $corsMethods = 'GET, OPTIONS';
467
        }
468
        $request->attributes->set('corsMethods', $corsMethods);
469
470
471
        return $this->render(
472
            'GravitonRestBundle:Main:index.json.twig',
473
            ['response' => $this->restUtils->serialize($schema)],
474
            $response
475
        );
476
    }
477
478
    /**
479
     * Renders a view.
480
     *
481
     * @param string   $view       The view name
482
     * @param array    $parameters An array of parameters to pass to the view
483
     * @param Response $response   A response instance
0 ignored issues
show
Documentation introduced by
Should the type for parameter $response not be null|Response?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
484
     *
485
     * @return Response A Response instance
486
     */
487
    public function render($view, array $parameters = array(), Response $response = null)
488
    {
489
        return $this->templating->renderResponse($view, $parameters, $response);
490
    }
491
492
493
    /**
494
     * Security needs to be enabled to get Object.
495
     *
496
     * @return String
0 ignored issues
show
Documentation introduced by
Should the return type not be \Graviton\SecurityBundle\Entities\SecurityUser?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
497
     * @throws UsernameNotFoundException
498
     */
499
    public function getSecurityUser()
500
    {
501
        return $this->securityUtils->getSecurityUser();
502
    }
503
}
504