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
|
|
|
|
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); |
|
|
|
|
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()); |
|
|
|
|
337
|
|
|
|
338
|
|
|
// Apply JSON patches |
339
|
|
|
$patch = new Patch($jsonDocument, $request->getContent()); |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
478
|
|
|
* @throws UsernameNotFoundException |
479
|
|
|
*/ |
480
|
|
|
public function getSecurityUser() |
481
|
|
|
{ |
482
|
|
|
return $this->securityUtils->getSecurityUser(); |
483
|
|
|
} |
484
|
|
|
} |
485
|
|
|
|
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.