Completed
Push — feature/evo-2472-whoami ( b4d617...71332d )
by Jan
34:12
created

RestController::render()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 3
crap 2
1
<?php
2
/**
3
 * basic rest controller
4
 */
5
6
namespace Graviton\RestBundle\Controller;
7
8
use Graviton\DocumentBundle\Service\FormDataMapperInterface;
9
use Graviton\ExceptionBundle\Exception\DeserializationException;
10
use Graviton\ExceptionBundle\Exception\InvalidJsonPatchException;
11
use Graviton\ExceptionBundle\Exception\MalformedInputException;
12
use Graviton\ExceptionBundle\Exception\NotFoundException;
13
use Graviton\ExceptionBundle\Exception\SerializationException;
14
use Graviton\RestBundle\Validator\Form;
15
use Graviton\RestBundle\Model\DocumentModel;
16
use Graviton\RestBundle\Model\PaginatorAwareInterface;
17
use Graviton\SchemaBundle\SchemaUtils;
18
use Graviton\DocumentBundle\Form\Type\DocumentType;
19
use Graviton\RestBundle\Service\RestUtilsInterface;
20
use Knp\Component\Pager\Paginator;
21
use Symfony\Component\DependencyInjection\ContainerInterface;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpFoundation\Response;
24
use Symfony\Component\Routing\Exception\RouteNotFoundException;
25
use Symfony\Component\Form\FormFactory;
26
use Symfony\Bundle\FrameworkBundle\Routing\Router;
27
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
28
use Symfony\Component\Validator\Validator\ValidatorInterface;
29
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
30
use Rs\Json\Patch;
31
use Rs\Json\Patch\InvalidPatchDocumentJsonException;
32
use Rs\Json\Patch\InvalidTargetDocumentJsonException;
33
use Rs\Json\Patch\InvalidOperationException;
34
use Rs\Json\Patch\FailedTestException;
35
use Graviton\RestBundle\Service\JsonPatchValidator;
36
37
/**
38
 * This is a basic rest controller. It should fit the most needs but if you need to add some
39
 * extra functionality you can extend it and overwrite single/all actions.
40
 * You can also extend the model class to add some extra logic before save
41
 *
42
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
43
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
44
 * @link     http://swisscom.ch
45
 */
46
class RestController
47
{
48
    /**
49
     * @var DocumentModel
50
     */
51
    private $model;
52
53
    /**
54
     * @var ContainerInterface service_container
55
     */
56
    private $container;
57
58
    /**
59
     * @var Response
60
     */
61
    private $response;
62
63
    /**
64
     * @var FormFactory
65
     */
66
    private $formFactory;
67
68
    /**
69
     * @var DocumentType
70
     */
71
    private $formType;
72
73
    /**
74
     * @var RestUtilsInterface
75
     */
76
    private $restUtils;
77
78
    /**
79
     * @var SchemaUtils
80
     */
81
    private $schemaUtils;
82
83
    /**
84
     * @var FormDataMapperInterface
85
     */
86
    protected $formDataMapper;
87
88
    /**
89
     * @var Router
90
     */
91
    private $router;
92
93
    /**
94
     * @var ValidatorInterface
95
     */
96
    private $validator;
97
98
    /**
99
     * @var EngineInterface
100
     */
101
    private $templating;
102
103
    /**
104
     * @var JsonPatchValidator
105
     */
106
    private $jsonPatchValidator;
107
108
    /**
109
     * @var Form
110
     */
111
    protected $formValidator;
112
113
    /**
114
     * @var TokenStorage
115
     */
116
    protected $tokenStorage;
117
118
    /**
119
     * @param Response           $response    Response
120
     * @param RestUtilsInterface $restUtils   Rest utils
121
     * @param Router             $router      Router
122
     * @param ValidatorInterface $validator   Validator
123 2
     * @param EngineInterface    $templating  Templating
124
     * @param FormFactory        $formFactory form factory
125
     * @param DocumentType       $formType    generic form
126
     * @param ContainerInterface $container   Container
127
     * @param SchemaUtils        $schemaUtils Schema utils
128
     */
129
    public function __construct(
130
        Response $response,
131
        RestUtilsInterface $restUtils,
132
        Router $router,
133
        ValidatorInterface $validator,
134 2
        EngineInterface $templating,
135 2
        FormFactory $formFactory,
136 2
        DocumentType $formType,
137 2
        ContainerInterface $container,
138 2
        SchemaUtils $schemaUtils
139 2
    ) {
140 2
        $this->response = $response;
141 2
        $this->restUtils = $restUtils;
142 2
        $this->router = $router;
143 2
        $this->validator = $validator;
144
        $this->templating = $templating;
145
        $this->formFactory = $formFactory;
146
        $this->formType = $formType;
147
        $this->container = $container;
148
        $this->schemaUtils = $schemaUtils;
149
    }
150
151 2
    /**
152
     * @param TokenStorage $tokenStorage
153 2
     */
154 2
    public function setTokenStorage(TokenStorage $tokenStorage)
155
    {
156
        $this->tokenStorage = $tokenStorage;
157
    }
158
159
    /**
160 2
     * Set form data mapper
161
     *
162 2
     * @param FormDataMapperInterface $formDataMapper Form data mapper
163 2
     * @return void
164
     */
165
    public function setFormDataMapper(FormDataMapperInterface $formDataMapper)
166
    {
167
        $this->formDataMapper = $formDataMapper;
168
    }
169
170
    /**
171
     * @param JsonPatchValidator $jsonPatchValidator Service for validation json patch
172 2
     * @return void
173
     */
174 2
    public function setJsonPatchValidator(JsonPatchValidator $jsonPatchValidator)
175 2
    {
176
        $this->jsonPatchValidator = $jsonPatchValidator;
177
    }
178
179
    /**
180
     * Defines the Form validator to be used.
181
     *
182
     * @param Form $validator Validator to be used
183
     *
184
     * @return void
185
     */
186
    public function setFormValidator(Form $validator)
187
    {
188
        $this->formValidator = $validator;
189
    }
190
191
    /**
192
     * Get the container object
193
     *
194
     * @return \Symfony\Component\DependencyInjection\ContainerInterface
195
     *
196
     * @obsolete
197 1
     */
198
    public function getContainer()
199 1
    {
200 1
        return $this->container;
201
    }
202 1
203
    /**
204 1
     * Returns a single record
205 1
     *
206 1
     * @param Request $request Current http request
207
     * @param string  $id      ID of record
208 1
     *
209
     * @return \Symfony\Component\HttpFoundation\Response $response Response with result or error
210
     */
211
    public function getAction(Request $request, $id)
212
    {
213
        $response = $this->getResponse()
214
            ->setStatusCode(Response::HTTP_OK);
215
216 2
        $record = $this->findRecord($id);
217
218 2
        return $this->render(
219
            'GravitonRestBundle:Main:index.json.twig',
220
            ['response' => $this->serialize($record)],
221
            $response
222
        );
223
    }
224
225
    /**
226
     * Get the response object
227
     *
228
     * @return \Symfony\Component\HttpFoundation\Response $response Response object
229
     */
230 1
    public function getResponse()
231
    {
232 1
        return $this->response;
233
    }
234 1
235
    /**
236
     * Get a single record from database or throw an exception if it doesn't exist
237
     *
238
     * @param mixed $id Record id
239
     *
240 1
     * @throws \Graviton\ExceptionBundle\Exception\NotFoundException
241
     *
242
     * @return object $record Document object
243
     */
244
    protected function findRecord($id)
245
    {
246
        $response = $this->getResponse();
247
248
        if (!($record = $this->getModel()->find($id))) {
249
            $e = new NotFoundException("Entry with id " . $id . " not found!");
250 2
            $e->setResponse($response);
251
            throw $e;
252 2
        }
253
254
        return $record;
255
    }
256 2
257
    /**
258
     * Return the model
259
     *
260
     * @throws \Exception in case no model was defined.
261
     *
262
     * @return DocumentModel $model Model
263
     */
264
    public function getModel()
265
    {
266 2
        if (!$this->model) {
267
            throw new \Exception('No model is set for this controller');
268 2
        }
269
270 2
        return $this->model;
271
    }
272
273
    /**
274
     * Set the model class
275
     *
276
     * @param DocumentModel $model Model class
277
     *
278
     * @return self
279
     */
280
    public function setModel(DocumentModel $model)
281
    {
282 2
        $this->model = $model;
283
284 2
        return $this;
285
    }
286
287
    /**
288
     * Serialize the given record and throw an exception if something went wrong
289 2
     *
290 1
     * @param object|object[] $result Record(s)
291 1
     *
292 1
     * @throws \Graviton\ExceptionBundle\Exception\SerializationException
293 1
     *
294
     * @return string $content Json content
295 1
     */
296 1
    protected function serialize($result)
297
    {
298
        $response = $this->getResponse();
299 1
300
        try {
301
            // array is serialized as an object {"0":{...},"1":{...},...} when data contains an empty objects
302
            // we serialize each item because we can assume this bug affects only root array element
303
            if (is_array($result) && array_keys($result) === range(0, count($result) - 1)) {
304
                $result = array_map(
305
                    function ($item) {
306
                        return $this->getRestUtils()->serializeContent($item);
307
                    },
308
                    $result
309
                );
310
                return '['.implode(',', $result).']';
311
            }
312 2
313
            return $this->getRestUtils()->serializeContent($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by parameter $result on line 296 can also be of type array; however, Graviton\RestBundle\Serv...ils::serializeContent() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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...
314 2
        } catch (\Exception $e) {
315
            $exception = new SerializationException($e);
316
            $exception->setResponse($response);
317
            throw $exception;
318
        }
319
    }
320
321
    /**
322
     * Get RestUtils service
323
     *
324 1
     * @return \Graviton\RestBundle\Service\RestUtils
325
     */
326 1
    public function getRestUtils()
327
    {
328 1
        return $this->restUtils;
329
    }
330
331
    /**
332
     * Returns all records
333 1
     *
334 1
     * @param Request $request Current http request
335
     *
336 1
     * @return \Symfony\Component\HttpFoundation\Response $response Response with result or error
337 1
     */
338 1
    public function allAction(Request $request)
339
    {
340 1
        $model = $this->getModel();
341
342
        if ($model instanceof PaginatorAwareInterface && !$model->hasPaginator()) {
343
            $paginator = new Paginator();
344
            $model->setPaginator($paginator);
345
        }
346
347
        $response = $this->getResponse()
348
            ->setStatusCode(Response::HTTP_OK);
349
350
        return $this->render(
351
            'GravitonRestBundle:Main:index.json.twig',
352
            ['response' => $this->serialize($model->findAll($request))],
353
            $response
354
        );
355
    }
356
357
    /**
358
     * Writes a new Entry to the database
359
     *
360
     * @param Request $request Current http request
361
     *
362
     * @return \Symfony\Component\HttpFoundation\Response $response Result of action with data (if successful)
363
     */
364
    public function postAction(Request $request)
365
    {
366
        // Get the response object from container
367
        $response = $this->getResponse();
368
        $model = $this->getModel();
369
370
        $this->formValidator->checkJsonRequest($request, $response);
371
        $record = $this->formValidator->checkForm(
372
            $this->formValidator->getForm($request, $model),
373
            $model,
374
            $this->formDataMapper,
375
            $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\Validator\Form::checkForm() 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...
376
        );
377
378
        // Insert the new record
379
        $record = $this->getModel()->insertRecord($record);
380
381
        // store id of new record so we dont need to reparse body later when needed
382
        $request->attributes->set('id', $record->getId());
383
384
        // Set status code
385
        $response->setStatusCode(Response::HTTP_CREATED);
386
387
        $response->headers->set(
388
            'Location',
389
            $this->getRouter()->generate($this->getRouteName($request), array('id' => $record->getId()))
390
        );
391
392
        return $response;
393
    }
394
395
    /**
396
     * Deserialize the given content throw an exception if something went wrong
397
     *
398
     * @param string $content       Request content
399
     * @param string $documentClass Document class
400
     *
401
     * @throws DeserializationException
402
     *
403
     * @return object $record Document
404
     */
405
    protected function deserialize($content, $documentClass)
406
    {
407
        $response = $this->getResponse();
408
409
        try {
410
            $record = $this->getRestUtils()->deserializeContent(
411
                $content,
412
                $documentClass
413
            );
414
        } catch (\Exception $e) {
415
            // pass the previous exception in this case to get the error message in the handler
416
            // http://php.net/manual/de/exception.getprevious.php
417
            $exception = new DeserializationException("Deserialization failed", $e);
418
419 1
            // at the moment, the response has to be set on the exception object.
420
            // try to refactor this and return the graviton.rest.response if none is set...
421 1
            $exception->setResponse($response);
422
            throw $exception;
423
        }
424
425
        return $record;
426
    }
427
428
    /**
429
     * Get the router from the dic
430
     *
431
     * @return Router
432
     */
433
    public function getRouter()
434
    {
435
        return $this->router;
436
    }
437
438
    /**
439
     * Update a record
440
     *
441
     * @param Number  $id      ID of record
442
     * @param Request $request Current http request
443
     *
444
     * @throws MalformedInputException
445
     *
446
     * @return Response $response Result of action with data (if successful)
447
     */
448
    public function putAction($id, Request $request)
449
    {
450
        $response = $this->getResponse();
451
        $model = $this->getModel();
452
453
        $this->formValidator->checkJsonRequest($request, $response);
454
455
        $record = $this->formValidator->checkForm(
456
            $this->formValidator->getForm($request, $model),
457
            $model,
458
            $this->formDataMapper,
459
            $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\Validator\Form::checkForm() 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...
460
        );
461
462
        // does it really exist??
463
        $upsert = false;
464
        try {
465
            $this->findRecord($id);
466
        } catch (NotFoundException $e) {
467
            // who cares, we'll upsert it
468
            $upsert = true;
469
        }
470
471
        // handle missing 'id' field in input to a PUT operation
472
        // if it is settable on the document, let's set it and move on.. if not, inform the user..
473
        if ($record->getId() != $id) {
474
            // try to set it..
475
            if (is_callable(array($record, 'setId'))) {
476
                $record->setId($id);
477
            } else {
478
                throw new MalformedInputException('No ID was supplied in the request payload.');
479
            }
480
        }
481
482
        // And update the record, if everything is ok
483
        if ($upsert) {
484
            $this->getModel()->insertRecord($record);
485
        } else {
486
            $this->getModel()->updateRecord($id, $record);
487
        }
488
489
        // Set status code
490
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
491
492
        // store id of new record so we dont need to reparse body later when needed
493
        $request->attributes->set('id', $record->getId());
494
495
        return $response;
496
    }
497
498
    /**
499
     * Patch a record
500
     *
501
     * @param Number  $id      ID of record
502
     * @param Request $request Current http request
503
     *
504
     * @throws MalformedInputException
505
     *
506
     * @return Response $response Result of action with data (if successful)
507
     */
508
    public function patchAction($id, Request $request)
509
    {
510
        $response = $this->getResponse();
511
        $this->formValidator->checkJsonRequest($request, $response);
512
513
        // Check JSON Patch request
514
        $this->formValidator->checkJsonPatchRequest(json_decode($request->getContent(), 1));
515
516
        // Find record && apply $ref converter
517
        $record = $this->findRecord($id);
518
        $jsonDocument = $this->serialize($record);
519
520
        // Check/validate JSON Patch
521
        if (!$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...
522
            throw new InvalidJsonPatchException($this->jsonPatchValidator->getException()->getMessage());
523
        }
524
525
        try {
526
            // Apply JSON patches
527
            $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...
528
            $patchedDocument = $patch->apply();
529
        } catch (InvalidPatchDocumentJsonException $e) {
530
            throw new InvalidJsonPatchException($e->getMessage());
531
        } catch (InvalidTargetDocumentJsonException $e) {
532
            throw new InvalidJsonPatchException($e->getMessage());
533
        } catch (InvalidOperationException $e) {
534
            throw new InvalidJsonPatchException($e->getMessage());
535
        } catch (FailedTestException $e) {
536
            throw new InvalidJsonPatchException($e->getMessage());
537
        }
538
539
        // Validate result object
540
        $model = $this->getModel();
541
        $record = $this->formValidator->checkForm(
542
            $this->formValidator->getForm($request, $model),
543
            $model,
544
            $this->formDataMapper,
545
            $patchedDocument
546
        );
547
548
        // Update object
549
        $this->getModel()->updateRecord($id, $record);
550
551
        // Set status code
552
        $response->setStatusCode(Response::HTTP_OK);
553
554
        // Set Content-Location header
555
        $response->headers->set(
556 1
            'Content-Location',
557
            $this->getRouter()->generate($this->getRouteName($request), array('id' => $record->getId()))
558 1
        );
559
560
        return $response;
561 1
    }
562
563 1
    /**
564 1
     * Deletes a record
565
     *
566 1
     * @param Number $id ID of record
567
     *
568
     * @return Response $response Result of the action
569
     */
570
    public function deleteAction($id)
571
    {
572
        $response = $this->getResponse();
573
574
        // does this record exist?
575
        $this->findRecord($id);
576
577
        $this->getModel()->deleteRecord($id);
578
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
579
580
        return $response;
581
    }
582
583
    /**
584
     * Return OPTIONS results.
585
     *
586
     * @param Request $request Current http request
587
     *
588
     * @throws SerializationException
589
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
590
     */
591
    public function optionsAction(Request $request)
592
    {
593
        list($app, $module, , $modelName) = explode('.', $request->attributes->get('_route'));
594
595
        $response = $this->response;
596
        $response->setStatusCode(Response::HTTP_OK);
597
598
        // enabled methods for CorsListener
599
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
600
        try {
601
            $router = $this->getRouter();
602
            // if post route is available we assume everything is readable
603
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
604
        } catch (RouteNotFoundException $exception) {
605
            // only allow read methods
606
            $corsMethods = 'GET, OPTIONS';
607
        }
608
        $request->attributes->set('corsMethods', $corsMethods);
609
610
        return $response;
611
    }
612
613
614
    /**
615
     * Return schema GET results.
616
     *
617
     * @param Request $request Current http request
618
     * @param string  $id      ID of record
619
     *
620
     * @throws SerializationException
621
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
622
     */
623
    public function schemaAction(Request $request, $id = null)
624
    {
625
        $request->attributes->set('schemaRequest', true);
626
627
        list($app, $module, , $modelName, $schemaType) = explode('.', $request->attributes->get('_route'));
628
629
        $response = $this->response;
630
        $response->setStatusCode(Response::HTTP_OK);
631
        $response->setPublic();
632
633
        if (!$id && $schemaType != 'canonicalIdSchema') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
634
            $schema = $this->schemaUtils->getCollectionSchema($modelName, $this->getModel());
635
        } else {
636
            $schema = $this->schemaUtils->getModelSchema($modelName, $this->getModel());
637
        }
638
639
        // enabled methods for CorsListener
640
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
641
        try {
642
            $router = $this->getRouter();
643
            // if post route is available we assume everything is readable
644
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
645
        } catch (RouteNotFoundException $exception) {
646
            // only allow read methods
647
            $corsMethods = 'GET, OPTIONS';
648
        }
649
        $request->attributes->set('corsMethods', $corsMethods);
650
651
        return $this->render(
652
            'GravitonRestBundle:Main:index.json.twig',
653
            ['response' => $this->serialize($schema)],
654
            $response
655
        );
656
    }
657
658
    /**
659
     * Get the validator
660
     *
661
     * @return ValidatorInterface
662
     */
663 2
    public function getValidator()
664
    {
665 2
        return $this->validator;
666
    }
667
668
    /**
669
     * Renders a view.
670
     *
671
     * @param string   $view       The view name
672
     * @param array    $parameters An array of parameters to pass to the view
673
     * @param Response $response   A response instance
674
     *
675
     * @return Response A Response instance
676
     */
677
    public function render($view, array $parameters = array(), Response $response = null)
678
    {
679
        return $this->templating->renderResponse($view, $parameters, $response);
680
    }
681
682
    /**
683
     * @param Request $request request
684
     * @return string
685
     */
686
    private function getRouteName(Request $request)
687
    {
688
        $routeName = $request->get('_route');
689
        $routeParts = explode('.', $routeName);
690
        $routeType = end($routeParts);
691
692
        if ($routeType == 'post') {
693
            $routeName = substr($routeName, 0, -4) . 'get';
694
        }
695
696
        return $routeName;
697
    }
698
699
    /**
700
     * @return Mixed | false
701
     */
702
    public function getUser()
703
    {
704
        if($token = $this->tokenStorage->getToken()){
705
            return $token->getUser();
706
        }
707
708
        return false;
709
    }
710
}
711