Completed
Push — feature/evo-2472-whoami ( 39cc08...5611b4 )
by Jan
14:59
created

RestController::setFormDataMapper()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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
     * @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 2
    public function __construct(
130
        Response $response,
131
        RestUtilsInterface $restUtils,
132
        Router $router,
133
        ValidatorInterface $validator,
134
        EngineInterface $templating,
135
        FormFactory $formFactory,
136
        DocumentType $formType,
137
        ContainerInterface $container,
138
        SchemaUtils $schemaUtils
139
    ) {
140 2
        $this->response = $response;
141 2
        $this->restUtils = $restUtils;
142 2
        $this->router = $router;
143 2
        $this->validator = $validator;
144 2
        $this->templating = $templating;
145 2
        $this->formFactory = $formFactory;
146 2
        $this->formType = $formType;
147 2
        $this->container = $container;
148 2
        $this->schemaUtils = $schemaUtils;
149 2
    }
150
151
    /**
152
     * Setter for the tokenStorage
153
     *
154
     * @param TokenStorage $tokenStorage The token storage
155
     */
156 2
    public function setTokenStorage(TokenStorage $tokenStorage)
157
    {
158 2
        $this->tokenStorage = $tokenStorage;
159 2
    }
160
161
    /**
162
     * Set form data mapper
163
     *
164
     * @param FormDataMapperInterface $formDataMapper Form data mapper
165
     * @return void
166
     */
167 2
    public function setFormDataMapper(FormDataMapperInterface $formDataMapper)
168
    {
169 2
        $this->formDataMapper = $formDataMapper;
170 2
    }
171
172
    /**
173
     * @param JsonPatchValidator $jsonPatchValidator Service for validation json patch
174
     * @return void
175
     */
176 2
    public function setJsonPatchValidator(JsonPatchValidator $jsonPatchValidator)
177
    {
178 2
        $this->jsonPatchValidator = $jsonPatchValidator;
179 2
    }
180
181
    /**
182
     * Defines the Form validator to be used.
183
     *
184
     * @param Form $validator Validator to be used
185
     *
186
     * @return void
187
     */
188 2
    public function setFormValidator(Form $validator)
189
    {
190 2
        $this->formValidator = $validator;
191 2
    }
192
193
    /**
194
     * Get the container object
195
     *
196
     * @return \Symfony\Component\DependencyInjection\ContainerInterface
197
     *
198
     * @obsolete
199
     */
200
    public function getContainer()
201
    {
202
        return $this->container;
203
    }
204
205
    /**
206
     * Returns a single record
207
     *
208
     * @param Request $request Current http request
209
     * @param string  $id      ID of record
210
     *
211
     * @return \Symfony\Component\HttpFoundation\Response $response Response with result or error
212
     */
213 1
    public function getAction(Request $request, $id)
214
    {
215 1
        $response = $this->getResponse()
216 1
            ->setStatusCode(Response::HTTP_OK);
217
218 1
        $record = $this->findRecord($id);
219
220 1
        return $this->render(
221 1
            'GravitonRestBundle:Main:index.json.twig',
222 1
            ['response' => $this->serialize($record)],
223
            $response
224 1
        );
225
    }
226
227
    /**
228
     * Get the response object
229
     *
230
     * @return \Symfony\Component\HttpFoundation\Response $response Response object
231
     */
232 2
    public function getResponse()
233
    {
234 2
        return $this->response;
235
    }
236
237
    /**
238
     * Get a single record from database or throw an exception if it doesn't exist
239
     *
240
     * @param mixed $id Record id
241
     *
242
     * @throws \Graviton\ExceptionBundle\Exception\NotFoundException
243
     *
244
     * @return object $record Document object
245
     */
246 1
    protected function findRecord($id)
247
    {
248 1
        $response = $this->getResponse();
249
250 1
        if (!($record = $this->getModel()->find($id))) {
251
            $e = new NotFoundException("Entry with id " . $id . " not found!");
252
            $e->setResponse($response);
253
            throw $e;
254
        }
255
256 1
        return $record;
257
    }
258
259
    /**
260
     * Return the model
261
     *
262
     * @throws \Exception in case no model was defined.
263
     *
264
     * @return DocumentModel $model Model
265
     */
266 2
    public function getModel()
267
    {
268 2
        if (!$this->model) {
269
            throw new \Exception('No model is set for this controller');
270
        }
271
272 2
        return $this->model;
273
    }
274
275
    /**
276
     * Set the model class
277
     *
278
     * @param DocumentModel $model Model class
279
     *
280
     * @return self
281
     */
282 2
    public function setModel(DocumentModel $model)
283
    {
284 2
        $this->model = $model;
285
286 2
        return $this;
287
    }
288
289
    /**
290
     * Serialize the given record and throw an exception if something went wrong
291
     *
292
     * @param object|object[] $result Record(s)
293
     *
294
     * @throws \Graviton\ExceptionBundle\Exception\SerializationException
295
     *
296
     * @return string $content Json content
297
     */
298 2
    protected function serialize($result)
299
    {
300 2
        $response = $this->getResponse();
301
302
        try {
303
            // array is serialized as an object {"0":{...},"1":{...},...} when data contains an empty objects
304
            // we serialize each item because we can assume this bug affects only root array element
305 2
            if (is_array($result) && array_keys($result) === range(0, count($result) - 1)) {
306 1
                $result = array_map(
307 1
                    function ($item) {
308 1
                        return $this->getRestUtils()->serializeContent($item);
309 1
                    },
310
                    $result
311 1
                );
312 1
                return '['.implode(',', $result).']';
313
            }
314
315 1
            return $this->getRestUtils()->serializeContent($result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by parameter $result on line 298 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...
316
        } catch (\Exception $e) {
317
            $exception = new SerializationException($e);
318
            $exception->setResponse($response);
319
            throw $exception;
320
        }
321
    }
322
323
    /**
324
     * Get RestUtils service
325
     *
326
     * @return \Graviton\RestBundle\Service\RestUtils
327
     */
328 2
    public function getRestUtils()
329
    {
330 2
        return $this->restUtils;
331
    }
332
333
    /**
334
     * Returns all records
335
     *
336
     * @param Request $request Current http request
337
     *
338
     * @return \Symfony\Component\HttpFoundation\Response $response Response with result or error
339
     */
340 1
    public function allAction(Request $request)
341
    {
342 1
        $model = $this->getModel();
343 1
        $user = $this->getUser();
344
345 1
        if ($model instanceof PaginatorAwareInterface && !$model->hasPaginator()) {
346
            $paginator = new Paginator();
347
            $model->setPaginator($paginator);
348
        }
349
350 1
        $response = $this->getResponse()
351 1
            ->setStatusCode(Response::HTTP_OK);
352
353 1
        return $this->render(
354 1
            'GravitonRestBundle:Main:index.json.twig',
355 1
            ['response' => $this->serialize($model->findAll($request, $user))],
356
            $response
357 1
        );
358
    }
359
360
    /**
361
     * Writes a new Entry to the database
362
     *
363
     * @param Request $request Current http request
364
     *
365
     * @return \Symfony\Component\HttpFoundation\Response $response Result of action with data (if successful)
366
     */
367
    public function postAction(Request $request)
368
    {
369
        // Get the response object from container
370
        $response = $this->getResponse();
371
        $model = $this->getModel();
372
373
        $this->formValidator->checkJsonRequest($request, $response);
374
        $record = $this->formValidator->checkForm(
375
            $this->formValidator->getForm($request, $model),
376
            $model,
377
            $this->formDataMapper,
378
            $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...
379
        );
380
381
        // Insert the new record
382
        $record = $this->getModel()->insertRecord($record);
383
384
        // store id of new record so we dont need to reparse body later when needed
385
        $request->attributes->set('id', $record->getId());
386
387
        // Set status code
388
        $response->setStatusCode(Response::HTTP_CREATED);
389
390
        $response->headers->set(
391
            'Location',
392
            $this->getRouter()->generate($this->getRouteName($request), array('id' => $record->getId()))
393
        );
394
395
        return $response;
396
    }
397
398
    /**
399
     * Deserialize the given content throw an exception if something went wrong
400
     *
401
     * @param string $content       Request content
402
     * @param string $documentClass Document class
403
     *
404
     * @throws DeserializationException
405
     *
406
     * @return object $record Document
407
     */
408
    protected function deserialize($content, $documentClass)
409
    {
410
        $response = $this->getResponse();
411
412
        try {
413
            $record = $this->getRestUtils()->deserializeContent(
414
                $content,
415
                $documentClass
416
            );
417
        } catch (\Exception $e) {
418
            // pass the previous exception in this case to get the error message in the handler
419
            // http://php.net/manual/de/exception.getprevious.php
420
            $exception = new DeserializationException("Deserialization failed", $e);
421
422
            // at the moment, the response has to be set on the exception object.
423
            // try to refactor this and return the graviton.rest.response if none is set...
424
            $exception->setResponse($response);
425
            throw $exception;
426
        }
427
428
        return $record;
429
    }
430
431
    /**
432
     * Get the router from the dic
433
     *
434
     * @return Router
435
     */
436 1
    public function getRouter()
437
    {
438 1
        return $this->router;
439
    }
440
441
    /**
442
     * Update a record
443
     *
444
     * @param Number  $id      ID of record
445
     * @param Request $request Current http request
446
     *
447
     * @throws MalformedInputException
448
     *
449
     * @return Response $response Result of action with data (if successful)
450
     */
451
    public function putAction($id, Request $request)
452
    {
453
        $response = $this->getResponse();
454
        $model = $this->getModel();
455
456
        $this->formValidator->checkJsonRequest($request, $response);
457
458
        $record = $this->formValidator->checkForm(
459
            $this->formValidator->getForm($request, $model),
460
            $model,
461
            $this->formDataMapper,
462
            $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...
463
        );
464
465
        // does it really exist??
466
        $upsert = false;
467
        try {
468
            $this->findRecord($id);
469
        } catch (NotFoundException $e) {
470
            // who cares, we'll upsert it
471
            $upsert = true;
472
        }
473
474
        // handle missing 'id' field in input to a PUT operation
475
        // if it is settable on the document, let's set it and move on.. if not, inform the user..
476
        if ($record->getId() != $id) {
477
            // try to set it..
478
            if (is_callable(array($record, 'setId'))) {
479
                $record->setId($id);
480
            } else {
481
                throw new MalformedInputException('No ID was supplied in the request payload.');
482
            }
483
        }
484
485
        // And update the record, if everything is ok
486
        if ($upsert) {
487
            $this->getModel()->insertRecord($record);
488
        } else {
489
            $this->getModel()->updateRecord($id, $record);
490
        }
491
492
        // Set status code
493
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
494
495
        // store id of new record so we dont need to reparse body later when needed
496
        $request->attributes->set('id', $record->getId());
497
498
        return $response;
499
    }
500
501
    /**
502
     * Patch a record
503
     *
504
     * @param Number  $id      ID of record
505
     * @param Request $request Current http request
506
     *
507
     * @throws MalformedInputException
508
     *
509
     * @return Response $response Result of action with data (if successful)
510
     */
511
    public function patchAction($id, Request $request)
512
    {
513
        $response = $this->getResponse();
514
        $this->formValidator->checkJsonRequest($request, $response);
515
516
        // Check JSON Patch request
517
        $this->formValidator->checkJsonPatchRequest(json_decode($request->getContent(), 1));
518
519
        // Find record && apply $ref converter
520
        $record = $this->findRecord($id);
521
        $jsonDocument = $this->serialize($record);
522
523
        // Check/validate JSON Patch
524
        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...
525
            throw new InvalidJsonPatchException($this->jsonPatchValidator->getException()->getMessage());
526
        }
527
528
        try {
529
            // Apply JSON patches
530
            $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...
531
            $patchedDocument = $patch->apply();
532
        } catch (InvalidPatchDocumentJsonException $e) {
533
            throw new InvalidJsonPatchException($e->getMessage());
534
        } catch (InvalidTargetDocumentJsonException $e) {
535
            throw new InvalidJsonPatchException($e->getMessage());
536
        } catch (InvalidOperationException $e) {
537
            throw new InvalidJsonPatchException($e->getMessage());
538
        } catch (FailedTestException $e) {
539
            throw new InvalidJsonPatchException($e->getMessage());
540
        }
541
542
        // Validate result object
543
        $model = $this->getModel();
544
        $record = $this->formValidator->checkForm(
545
            $this->formValidator->getForm($request, $model),
546
            $model,
547
            $this->formDataMapper,
548
            $patchedDocument
549
        );
550
551
        // Update object
552
        $this->getModel()->updateRecord($id, $record);
553
554
        // Set status code
555
        $response->setStatusCode(Response::HTTP_OK);
556
557
        // Set Content-Location header
558
        $response->headers->set(
559
            'Content-Location',
560
            $this->getRouter()->generate($this->getRouteName($request), array('id' => $record->getId()))
561
        );
562
563
        return $response;
564
    }
565
566
    /**
567
     * Deletes a record
568
     *
569
     * @param Number $id ID of record
570
     *
571
     * @return Response $response Result of the action
572
     */
573 1
    public function deleteAction($id)
574
    {
575 1
        $response = $this->getResponse();
576
577
        // does this record exist?
578 1
        $this->findRecord($id);
579
580 1
        $this->getModel()->deleteRecord($id);
581 1
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
582
583 1
        return $response;
584
    }
585
586
    /**
587
     * Return OPTIONS results.
588
     *
589
     * @param Request $request Current http request
590
     *
591
     * @throws SerializationException
592
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
593
     */
594
    public function optionsAction(Request $request)
595
    {
596
        list($app, $module, , $modelName) = explode('.', $request->attributes->get('_route'));
597
598
        $response = $this->response;
599
        $response->setStatusCode(Response::HTTP_OK);
600
601
        // enabled methods for CorsListener
602
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
603
        try {
604
            $router = $this->getRouter();
605
            // if post route is available we assume everything is readable
606
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
607
        } catch (RouteNotFoundException $exception) {
608
            // only allow read methods
609
            $corsMethods = 'GET, OPTIONS';
610
        }
611
        $request->attributes->set('corsMethods', $corsMethods);
612
613
        return $response;
614
    }
615
616
617
    /**
618
     * Return schema GET results.
619
     *
620
     * @param Request $request Current http request
621
     * @param string  $id      ID of record
622
     *
623
     * @throws SerializationException
624
     * @return \Symfony\Component\HttpFoundation\Response $response Result of the action
625
     */
626
    public function schemaAction(Request $request, $id = null)
627
    {
628
        $request->attributes->set('schemaRequest', true);
629
630
        list($app, $module, , $modelName, $schemaType) = explode('.', $request->attributes->get('_route'));
631
632
        $response = $this->response;
633
        $response->setStatusCode(Response::HTTP_OK);
634
        $response->setPublic();
635
636
        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...
637
            $schema = $this->schemaUtils->getCollectionSchema($modelName, $this->getModel());
638
        } else {
639
            $schema = $this->schemaUtils->getModelSchema($modelName, $this->getModel());
640
        }
641
642
        // enabled methods for CorsListener
643
        $corsMethods = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
644
        try {
645
            $router = $this->getRouter();
646
            // if post route is available we assume everything is readable
647
            $router->generate(implode('.', array($app, $module, 'rest', $modelName, 'post')));
648
        } catch (RouteNotFoundException $exception) {
649
            // only allow read methods
650
            $corsMethods = 'GET, OPTIONS';
651
        }
652
        $request->attributes->set('corsMethods', $corsMethods);
653
654
        return $this->render(
655
            'GravitonRestBundle:Main:index.json.twig',
656
            ['response' => $this->serialize($schema)],
657
            $response
658
        );
659
    }
660
661
    /**
662
     * Get the validator
663
     *
664
     * @return ValidatorInterface
665
     */
666
    public function getValidator()
667
    {
668
        return $this->validator;
669
    }
670
671
    /**
672
     * Renders a view.
673
     *
674
     * @param string   $view       The view name
675
     * @param array    $parameters An array of parameters to pass to the view
676
     * @param Response $response   A response instance
677
     *
678
     * @return Response A Response instance
679
     */
680 2
    public function render($view, array $parameters = array(), Response $response = null)
681
    {
682 2
        return $this->templating->renderResponse($view, $parameters, $response);
683
    }
684
685
    /**
686
     * @param Request $request request
687
     * @return string
688
     */
689
    private function getRouteName(Request $request)
690
    {
691
        $routeName = $request->get('_route');
692
        $routeParts = explode('.', $routeName);
693
        $routeType = end($routeParts);
694
695
        if ($routeType == 'post') {
696
            $routeName = substr($routeName, 0, -4) . 'get';
697
        }
698
699
        return $routeName;
700
    }
701
702
    /**
703
     * Get the user who's logged in
704
     *
705
     * @return Mixed | false
706
     */
707 1
    public function getUser()
708
    {
709 1
        if ($token = $this->tokenStorage->getToken()) {
710 1
            return $token->getUser()->getUser();
711
        }
712
713
        return false;
714
    }
715
}
716