Skip to content

Commit 2d21bbd

Browse files
authored
Merge pull request #22 from tuyakhov/rest_actions
Json api actions
2 parents acea2ed + adaf485 commit 2d21bbd

14 files changed

+443
-100
lines changed

src/JsonApiParser.php

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
namespace tuyakhov\jsonapi;
77

88
use yii\helpers\ArrayHelper;
9+
use yii\web\BadRequestHttpException;
910
use \yii\web\JsonParser;
1011

1112
class JsonApiParser extends JsonParser
@@ -33,37 +34,27 @@ class JsonApiParser extends JsonParser
3334
public function parse($rawBody, $contentType)
3435
{
3536
$array = parent::parse($rawBody, $contentType);
36-
if ($type = ArrayHelper::getValue($array, 'data.type')) {
37-
$formName = $this->typeToFormName($type);
38-
if ($attributes = ArrayHelper::getValue($array, 'data.attributes')) {
39-
$result[$formName] = array_combine($this->parseMemberNames(array_keys($attributes)), array_values($attributes));
40-
} elseif ($id = ArrayHelper::getValue($array, 'data.id')) {
41-
$result[$formName] = ['id' => $id, 'type' => $type];
42-
}
43-
if ($relationships = ArrayHelper::getValue($array, 'data.relationships')) {
44-
foreach ($relationships as $name => $relationship) {
45-
if (isset($relationship[0])) {
46-
foreach ($relationship as $item) {
47-
if (isset($item['type']) && isset($item['id'])) {
48-
$formName = $this->typeToFormName($item['type']);
49-
$result[$name][$formName][] = $item;
50-
}
51-
}
52-
} elseif (isset($relationship['type']) && isset($relationship['id'])) {
53-
$formName = $this->typeToFormName($relationship['type']);
54-
$result[$name][$formName] = $relationship;
55-
}
56-
}
37+
$data = ArrayHelper::getValue($array, 'data', []);
38+
if (empty($data)) {
39+
if ($this->throwException) {
40+
throw new BadRequestHttpException('The request MUST include a single resource object as primary data.');
5741
}
42+
return [];
43+
}
44+
if (ArrayHelper::isAssociative($data)) {
45+
$result = $this->parseResource($data);
46+
47+
$relObjects = ArrayHelper::getValue($data, 'relationships', []);
48+
$result['relationships'] = $this->parseRelationships($relObjects);
5849
} else {
59-
$data = ArrayHelper::getValue($array, 'data', []);
60-
foreach ($data as $relationLink) {
61-
if (isset($relationLink['type']) && isset($relationLink['id'])) {
62-
$formName = $this->typeToFormName($relationLink['type']);
63-
$result[$formName][] = $relationLink;
50+
foreach ($data as $object) {
51+
$resource = $this->parseResource($object);
52+
foreach (array_keys($resource) as $key) {
53+
$result[$key][] = $resource[$key];
6454
}
6555
}
6656
}
57+
6758
return isset($result) ? $result : $array;
6859
}
6960

@@ -84,4 +75,53 @@ protected function parseMemberNames(array $memberNames = [])
8475
{
8576
return array_map($this->memberNameCallback, $memberNames);
8677
}
78+
79+
/**
80+
* @param $item
81+
* @return array
82+
* @throws BadRequestHttpException
83+
*/
84+
protected function parseResource($item)
85+
{
86+
if (!$type = ArrayHelper::getValue($item, 'type')) {
87+
if ($this->throwException) {
88+
throw new BadRequestHttpException('The resource object MUST contain at least a type member');
89+
}
90+
return [];
91+
}
92+
$formName = $this->typeToFormName($type);
93+
94+
$attributes = ArrayHelper::getValue($item, 'attributes', []);
95+
$attributes = array_combine($this->parseMemberNames(array_keys($attributes)), array_values($attributes));
96+
97+
if ($id = ArrayHelper::getValue($item, 'id')) {
98+
$attributes['id'] = $id;
99+
}
100+
101+
return [$formName => $attributes];
102+
}
103+
104+
/**
105+
* @param array $relObjects
106+
* @return array
107+
*/
108+
protected function parseRelationships(array $relObjects = [])
109+
{
110+
$relationships = [];
111+
foreach ($relObjects as $name => $relationship) {
112+
if (!$relData = ArrayHelper::getValue($relationship, 'data')) {
113+
continue;
114+
}
115+
if (!ArrayHelper::isIndexed($relData)) {
116+
$relData = [$relData];
117+
}
118+
foreach ($relData as $identifier) {
119+
if (isset($identifier['type']) && isset($identifier['id'])) {
120+
$formName = $this->typeToFormName($identifier['type']);
121+
$relationships[$name][$formName][] = ['id' => $identifier['id']];
122+
}
123+
}
124+
}
125+
return $relationships;
126+
}
87127
}

src/ResourceTrait.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,14 @@ public function getResourceRelationships()
6969

7070
/**
7171
* @param string $name the case sensitive name of the relationship.
72-
* @param $relationship
72+
* @param array|ActiveRecordInterface $relationship
7373
*/
7474
public function setResourceRelationship($name, $relationship)
7575
{
76+
/** @var $this ActiveRecordInterface */
77+
if (!$this instanceof ActiveRecordInterface) {
78+
return;
79+
}
7680
if (!is_array($relationship)) {
7781
$relationship = [$relationship];
7882
}

src/actions/Action.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/**
3+
* @author Anton Tuyakhov <atuyakhov@gmail.com>
4+
*/
5+
6+
namespace tuyakhov\jsonapi\actions;
7+
8+
use tuyakhov\jsonapi\ResourceInterface;
9+
use yii\db\ActiveRecordInterface;
10+
use yii\db\BaseActiveRecord;
11+
use yii\helpers\ArrayHelper;
12+
13+
class Action extends \yii\rest\Action
14+
{
15+
/**
16+
* Links the relationships with primary model.
17+
* @var callable
18+
*/
19+
public $linkRelationships;
20+
21+
/**
22+
* @var bool Weather allow to do a full replacement of a to-many relationship
23+
*/
24+
public $allowFullReplacement = true;
25+
26+
/**
27+
* Links the relationships with primary model.
28+
* @param $model ActiveRecordInterface
29+
* @param array $data
30+
*/
31+
protected function linkRelationships($model, array $data = [])
32+
{
33+
if ($this->linkRelationships !== null) {
34+
call_user_func($this->linkRelationships, $this, $model, $data);
35+
return;
36+
}
37+
38+
if (!$model instanceof ResourceInterface) {
39+
return;
40+
}
41+
42+
foreach ($data as $name => $relationship) {
43+
if (!$related = $model->getRelation($name, false)) {
44+
continue;
45+
}
46+
/** @var BaseActiveRecord $relatedClass */
47+
$relatedClass = new $related->modelClass;
48+
$relationships = ArrayHelper::keyExists($relatedClass->formName(), $relationship) ? $relationship[$relatedClass->formName()] : [];
49+
50+
$ids = [];
51+
foreach ($relationships as $index => $relObject) {
52+
if (!isset($relObject['id'])) {
53+
continue;
54+
}
55+
$ids[] = $relObject['id'];
56+
}
57+
58+
if (!$records = $relatedClass::find()->andWhere(['in', $relatedClass::primaryKey(), $ids])->all()) {
59+
continue;
60+
}
61+
62+
if ($related->multiple && !$this->allowFullReplacement) {
63+
continue;
64+
}
65+
$model->unlinkAll($name);
66+
$model->setResourceRelationship($name, $records);
67+
}
68+
}
69+
}

src/actions/CreateAction.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
/**
3+
* @author Anton Tuyakhov <atuyakhov@gmail.com>
4+
*/
5+
6+
namespace tuyakhov\jsonapi\actions;
7+
8+
use Yii;
9+
use yii\base\Model;
10+
use yii\helpers\Url;
11+
use yii\web\ServerErrorHttpException;
12+
13+
class CreateAction extends Action
14+
{
15+
/**
16+
* @var string the scenario to be assigned to the new model before it is validated and saved.
17+
*/
18+
public $scenario = Model::SCENARIO_DEFAULT;
19+
20+
/**
21+
* @var string the name of the view action. This property is need to create the URL when the model is successfully created.
22+
*/
23+
public $viewAction = 'view';
24+
25+
/**
26+
* Links the relationships with primary model.
27+
* @var callable
28+
*/
29+
public $linkRelationships;
30+
31+
/**
32+
* Creates a new resource.
33+
* @return \yii\db\ActiveRecordInterface the model newly created
34+
* @throws ServerErrorHttpException if there is any error when creating the model
35+
*/
36+
public function run()
37+
{
38+
if ($this->checkAccess) {
39+
call_user_func($this->checkAccess, $this->id);
40+
}
41+
42+
/* @var $model \yii\db\ActiveRecord */
43+
$model = new $this->modelClass([
44+
'scenario' => $this->scenario,
45+
]);
46+
47+
$request = Yii::$app->getRequest();
48+
$model->load($request->getBodyParams());
49+
if ($model->save()) {
50+
$this->linkRelationships($model, $request->getBodyParam('relationships'));
51+
$response = Yii::$app->getResponse();
52+
$response->setStatusCode(201);
53+
$id = implode(',', array_values($model->getPrimaryKey(true)));
54+
$response->getHeaders()->set('Location', Url::toRoute([$this->viewAction, 'id' => $id], true));
55+
} elseif (!$model->hasErrors()) {
56+
throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
57+
}
58+
59+
return $model;
60+
}
61+
}

src/actions/UpdateAction.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
/**
3+
* @author Anton Tuyakhov <atuyakhov@gmail.com>
4+
*/
5+
6+
namespace tuyakhov\jsonapi\actions;
7+
8+
use yii\base\Model;
9+
use yii\db\ActiveRecord;
10+
use Yii;
11+
use yii\web\ServerErrorHttpException;
12+
13+
class UpdateAction extends Action
14+
{
15+
/**
16+
* @var string the scenario to be assigned to the model before it is validated and updated.
17+
*/
18+
public $scenario = Model::SCENARIO_DEFAULT;
19+
20+
/**
21+
* Updates an existing resource.
22+
* @param string $id the primary key of the model.
23+
* @return \yii\db\ActiveRecordInterface the model being updated
24+
* @throws ServerErrorHttpException if there is any error when updating the model
25+
*/
26+
public function run($id)
27+
{
28+
/* @var $model ActiveRecord */
29+
$model = $this->findModel($id);
30+
31+
if ($this->checkAccess) {
32+
call_user_func($this->checkAccess, $this->id, $model);
33+
}
34+
35+
$request = Yii::$app->getRequest();
36+
$model->scenario = $this->scenario;
37+
$model->load($request->getBodyParams());
38+
if ($model->save() === false && !$model->hasErrors()) {
39+
throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
40+
}
41+
42+
$this->linkRelationships($model, $request->getBodyParam('relationships'));
43+
44+
return $model;
45+
}
46+
}

0 commit comments

Comments
 (0)