Completed
Push — master ( 8cb42d...e9bfab )
by
unknown
19:56
created

RestController::serialize()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 0
cts 20
cp 0
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 14
nc 6
nop 1
crap 20

1 Method

Rating   Name   Duplication   Size   Complexity  
A RestController::getRouter() 0 4 1
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);
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Graviton\RestBundle\Serv...tils::validateRequest() does only seem to accept object|string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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
268
        $this->restUtils->checkJsonRequest($request, $response, $this->getModel());
269
270
        // Check and wait if another update is being processed
271
        $this->collectionCache->updateOperationCheck($model->getRepository(), $id);
272
        $this->collectionCache->addUpdateLock($model->getRepository(), $id);
273
274
        $record = $this->restUtils->validateRequest($request->getContent(), $model);
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Graviton\RestBundle\Serv...tils::validateRequest() does only seem to accept object|string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
275
276
        // handle missing 'id' field in input to a PUT operation
277
        // if it is settable on the document, let's set it and move on.. if not, inform the user..
278
        if ($record->getId() != $id) {
279
            // try to set it..
280
            if (is_callable(array($record, 'setId'))) {
281
                $record->setId($id);
282
            } else {
283
                $this->collectionCache->releaseUpdateLock($model->getRepository(), $id);
284
                throw new MalformedInputException('No ID was supplied in the request payload.');
285
            }
286
        }
287
288
        $repository = $model->getRepository();
289
290
        // And update the record, if everything is ok
291
        if (!$this->getModel()->recordExists($id)) {
292
            $this->getModel()->insertRecord($record, false);
293
        } else {
294
            $this->getModel()->updateRecord($id, $record, false);
295
        }
296
297
        $this->collectionCache->releaseUpdateLock($repository, $id);
298
299
        // Set status code
300
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
301
302
        // store id of new record so we dont need to reparse body later when needed
303
        $request->attributes->set('id', $record->getId());
304
305
        return $response;
306
    }
307
308
    /**
309
     * Patch a record
310
     *
311
     * @param Number  $id      ID of record
312
     * @param Request $request Current http request
313
     *
314
     * @throws MalformedInputException
315
     *
316
     * @return Response $response Result of action with data (if successful)
317
     */
318
    public function patchAction($id, Request $request)
319
    {
320
        $response = $this->getResponse();
321
        $model = $this->getModel();
322
323
        // Check JSON Patch request
324
        $this->restUtils->checkJsonRequest($request, $response, $model);
325
        $this->restUtils->checkJsonPatchRequest(json_decode($request->getContent(), 1));
326
327
        // Check and wait if another update is being processed
328
        $this->collectionCache->updateOperationCheck($model->getRepository(), $id);
329
330
        // Find record && apply $ref converter
331
        $jsonDocument = $model->getSerialised($id);
332
        $this->collectionCache->addUpdateLock($model->getRepository(), $id);
333
334
        try {
335
            // Check if valid
336
            $this->jsonPatchValidator->validate($jsonDocument, $request->getContent());
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Graviton\RestBundle\Serv...chValidator::validate() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $jsonDocument defined by $model->getSerialised($id) on line 331 can also be of type object; however, Graviton\RestBundle\Serv...chValidator::validate() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
337
338
            // Apply JSON patches
339
            $patch = new Patch($jsonDocument, $request->getContent());
0 ignored issues
show
Bug introduced by
It seems like $request->getContent() targeting Symfony\Component\HttpFo...n\Request::getContent() can also be of type resource; however, Rs\Json\Patch::__construct() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $jsonDocument defined by $model->getSerialised($id) on line 331 can also be of type object; however, Rs\Json\Patch::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
340
            $patchedDocument = $patch->apply();
341
        } catch (\Exception $e) {
342
            $this->collectionCache->releaseUpdateLock($model->getRepository(), $id);
343
            throw new InvalidJsonPatchException($e->getMessage());
344
        }
345
346
        // Validate result object
347
        $model = $this->getModel();
348
        $record = $this->restUtils->validateRequest($patchedDocument, $model);
349
350
        // Update object
351
        $this->getModel()->updateRecord($id, $record);
352
353
        $this->collectionCache->releaseUpdateLock($model->getRepository(), $id);
354
355
        // Set status code
356
        $response->setStatusCode(Response::HTTP_OK);
357
358
        // Set Content-Location header
359
        $response->headers->set(
360
            'Content-Location',
361
            $this->getRouter()->generate($this->restUtils->getRouteName($request), array('id' => $record->getId()))
362
        );
363
364
        return $response;
365
    }
366
367
    /**
368
     * Deletes a record
369
     *
370
     * @param Number $id ID of record
371
     *
372
     * @return Response $response Result of the action
373
     */
374
    public function deleteAction($id)
375
    {
376
        $response = $this->getResponse();
377
        $this->model->deleteRecord($id);
378
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
379
380
        return $response;
381
    }
382
383
    /**
384
     * Return OPTIONS results.
385
     *
386
     * @param Request $request Current http request
387
     *
388
     * @throws SerializationException
389
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
390
     */
391
    public function optionsAction(Request $request)
392
    {
393
        list($app, $module, , $modelName) = explode('.', $request->attributes->get('_route'));
394
395
        $response = $this->response;
396
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
397
398
        // enabled methods for CorsListener
399
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
400
        try {
401
            $router = $this->getRouter();
402
            // if post route is available we assume everything is readable
403
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
404
        } catch (RouteNotFoundException $exception) {
405
            // only allow read methods
406
            $corsMethods = 'GET, OPTIONS';
407
        }
408
        $request->attributes->set('corsMethods', $corsMethods);
409
410
        return $response;
411
    }
412
413
414
    /**
415
     * Return schema GET results.
416
     *
417
     * @param Request $request Current http request
418
     * @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...
419
     *
420
     * @throws SerializationException
421
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
422
     */
423
    public function schemaAction(Request $request, $id = null)
424
    {
425
        $request->attributes->set('schemaRequest', true);
426
427
        list($app, $module, , $modelName, $schemaType) = explode('.', $request->attributes->get('_route'));
428
429
        $response = $this->response;
430
        $response->setStatusCode(Response::HTTP_OK);
431
        $response->setPublic();
432
433
        if (!$id && $schemaType != 'canonicalIdSchema') {
434
            $schema = $this->schemaUtils->getCollectionSchema($modelName, $this->getModel());
435
        } else {
436
            $schema = $this->schemaUtils->getModelSchema($modelName, $this->getModel());
437
        }
438
439
        // enabled methods for CorsListener
440
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
441
        try {
442
            $router = $this->getRouter();
443
            // if post route is available we assume everything is readable
444
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
445
        } catch (RouteNotFoundException $exception) {
446
            // only allow read methods
447
            $corsMethods = 'GET, OPTIONS';
448
        }
449
        $request->attributes->set('corsMethods', $corsMethods);
450
451
452
        return $this->render(
453
            'GravitonRestBundle:Main:index.json.twig',
454
            ['response' => $this->restUtils->serialize($schema)],
455
            $response
456
        );
457
    }
458
459
    /**
460
     * Renders a view.
461
     *
462
     * @param string   $view       The view name
463
     * @param array    $parameters An array of parameters to pass to the view
464
     * @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...
465
     *
466
     * @return Response A Response instance
467
     */
468
    public function render($view, array $parameters = array(), Response $response = null)
469
    {
470
        return $this->templating->renderResponse($view, $parameters, $response);
471
    }
472
473
474
    /**
475
     * Security needs to be enabled to get Object.
476
     *
477
     * @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...
478
     * @throws UsernameNotFoundException
479
     */
480
    public function getSecurityUser()
481
    {
482
        return $this->securityUtils->getSecurityUser();
483
    }
484
}
485