Completed
Push — master ( 4e3a1b...06a217 )
by Narcotic
22:41
created

RestController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 21
cp 0
rs 9.3142
c 0
b 0
f 0
cc 1
eloc 19
nc 1
nop 9
crap 2

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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