Skip to content

Commit acea2ed

Browse files
authored
Merge pull request #14 from tuyakhov/member_naming
member names
2 parents aae7cca + 65ec704 commit acea2ed

File tree

8 files changed

+203
-63
lines changed

8 files changed

+203
-63
lines changed

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class Controller extends \yii\rest\Controller
4646
```
4747
By default, the value of `type` is automatically pluralized.
4848
You can change this behavior by setting `tuyakhov\jsonapi\Serializer::$pluralize` property:
49-
```
49+
```php
5050
class Controller extends \yii\rest\Controller
5151
{
5252
public $serializer = [
@@ -166,7 +166,7 @@ As the result:
166166
}
167167
}
168168
```
169-
Enabling JSON Input
169+
Enabling JSON API Input
170170
---------------------------
171171
To let the API accept input data in JSON API format, configure the [[yii\web\Request::$parsers|parsers]] property of the request application component to use the [[tuyakhov\jsonapi\JsonApiParser]] for JSON input
172172
```php
@@ -176,3 +176,35 @@ To let the API accept input data in JSON API format, configure the [[yii\web\Req
176176
]
177177
]
178178
```
179+
By default it parses a HTTP request body so that you can populate model attributes with user inputs.
180+
For example the request body:
181+
```javascript
182+
{
183+
"data": {
184+
"type": "users",
185+
"id": "1",
186+
"attributes": {
187+
"first-name": "Bob",
188+
"last-name": "Homster"
189+
}
190+
}
191+
}
192+
```
193+
Will be resolved into the following array:
194+
```php
195+
// var_dump($_POST);
196+
[
197+
"User" => [
198+
"first_name" => "Bob",
199+
"last_name" => "Homster"
200+
]
201+
]
202+
```
203+
So you can access request body by calling `\Yii::$app->request->post()` and simply populate the model with input data:
204+
```php
205+
$model = new User();
206+
$model->load(\Yii::$app->request->post());
207+
```
208+
By default type `users` will be converted into `User` (singular, camelCase) which corresponds to the model's `formName()` method (which you may override).
209+
You can override the `JsonApiParser::formNameCallback` property which refers to a callback that converts 'type' member to form name.
210+
Also you could change the default behavior for conversion of member names to variable names ('first-name' converts into 'first_name') by setting `JsonApiParser::memberNameCallback` property.

src/Inflector.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
/**
3+
* @author Anton Tuyakhov <atuyakhov@gmail.com>
4+
*/
5+
6+
namespace tuyakhov\jsonapi;
7+
8+
use yii\helpers\BaseInflector;
9+
10+
class Inflector extends BaseInflector
11+
{
12+
/**
13+
* Format member names according to recommendations for JSON API implementations.
14+
* For example, both 'firstName' and 'first_name' will be converted to 'first-name'.
15+
* @link http://jsonapi.org/format/#document-member-names
16+
* @param $var string
17+
* @return string
18+
*/
19+
public static function var2member($var)
20+
{
21+
return self::camel2id(self::variablize($var));
22+
}
23+
24+
/**
25+
* Converts member names to variable names
26+
* All special characters will be replaced by underscore
27+
* For example, 'first-name' will be converted to 'first_name'
28+
* @param $member string
29+
* @return mixed
30+
*/
31+
public static function member2var($member)
32+
{
33+
return str_replace(' ', '_', preg_replace('/[^A-Za-z0-9]+/', ' ', $member));
34+
}
35+
36+
/**
37+
* Converts 'type' member to form name
38+
* Will be converted to singular form.
39+
* For example, 'articles' will be converted to 'Article'
40+
* @param $type string 'type' member of the document
41+
* @return string
42+
*/
43+
public static function type2form($type)
44+
{
45+
return self::id2camel(self::singularize($type));
46+
}
47+
}

src/JsonApiParser.php

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,26 @@
55

66
namespace tuyakhov\jsonapi;
77

8-
use yii\base\InvalidConfigException;
98
use yii\helpers\ArrayHelper;
10-
use yii\helpers\Inflector;
119
use \yii\web\JsonParser;
1210

1311
class JsonApiParser extends JsonParser
1412
{
1513
/**
16-
* @var array|callable|null
14+
* Converts 'type' member to form name
15+
* If not set, type will be converted to singular form.
16+
* For example, 'articles' will be converted to 'Article'
17+
* @var callable
1718
*/
18-
protected $formNameCallback;
19-
20-
public function __construct($formNameCallback = null)
21-
{
22-
if ($formNameCallback === null) {
23-
$formNameCallback = [$this, 'typeToFormName'];
24-
}
25-
if (!is_callable($formNameCallback, true)) {
26-
throw new InvalidConfigException('JsonApiParser::formNameCallback should be callable');
27-
}
28-
29-
$this->formNameCallback = $formNameCallback;
30-
}
19+
public $formNameCallback = ['tuyakhov\jsonapi\Inflector', 'type2form'];
3120

21+
/**
22+
* Converts member names to variable names
23+
* If not set, all special characters will be replaced by underscore
24+
* For example, 'first-name' will be converted to 'first_name'
25+
* @var callable
26+
*/
27+
public $memberNameCallback = ['tuyakhov\jsonapi\Inflector', 'member2var'];
3228

3329
/**
3430
* Parse resource object into the input data to populates the model
@@ -38,9 +34,9 @@ public function parse($rawBody, $contentType)
3834
{
3935
$array = parent::parse($rawBody, $contentType);
4036
if ($type = ArrayHelper::getValue($array, 'data.type')) {
41-
$formName = call_user_func($this->formNameCallback, $type);
37+
$formName = $this->typeToFormName($type);
4238
if ($attributes = ArrayHelper::getValue($array, 'data.attributes')) {
43-
$result[$formName] = $attributes;
39+
$result[$formName] = array_combine($this->parseMemberNames(array_keys($attributes)), array_values($attributes));
4440
} elseif ($id = ArrayHelper::getValue($array, 'data.id')) {
4541
$result[$formName] = ['id' => $id, 'type' => $type];
4642
}
@@ -49,12 +45,12 @@ public function parse($rawBody, $contentType)
4945
if (isset($relationship[0])) {
5046
foreach ($relationship as $item) {
5147
if (isset($item['type']) && isset($item['id'])) {
52-
$formName = call_user_func($this->formNameCallback, $item['type']);
48+
$formName = $this->typeToFormName($item['type']);
5349
$result[$name][$formName][] = $item;
5450
}
5551
}
5652
} elseif (isset($relationship['type']) && isset($relationship['id'])) {
57-
$formName = call_user_func($this->formNameCallback, $relationship['type']);
53+
$formName = $this->typeToFormName($relationship['type']);
5854
$result[$name][$formName] = $relationship;
5955
}
6056
}
@@ -63,16 +59,29 @@ public function parse($rawBody, $contentType)
6359
$data = ArrayHelper::getValue($array, 'data', []);
6460
foreach ($data as $relationLink) {
6561
if (isset($relationLink['type']) && isset($relationLink['id'])) {
66-
$formName = call_user_func($this->formNameCallback, $relationLink['type']);
62+
$formName = $this->typeToFormName($relationLink['type']);
6763
$result[$formName][] = $relationLink;
6864
}
6965
}
7066
}
7167
return isset($result) ? $result : $array;
7268
}
7369

70+
/**
71+
* @param $type 'type' member of the document
72+
* @return string form name
73+
*/
7474
protected function typeToFormName($type)
7575
{
76-
return Inflector::id2camel(Inflector::singularize($type));
76+
return call_user_func($this->formNameCallback, $type);
77+
}
78+
79+
/**
80+
* @param array $memberNames
81+
* @return array variable names
82+
*/
83+
protected function parseMemberNames(array $memberNames = [])
84+
{
85+
return array_map($this->memberNameCallback, $memberNames);
7786
}
7887
}

src/ResourceTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
use yii\base\Arrayable;
99
use yii\db\ActiveRecordInterface;
10-
use yii\helpers\Inflector;
1110
use yii\web\Link;
1211
use yii\web\Linkable;
1312

@@ -120,6 +119,7 @@ protected function resolveFields(array $fields, array $fieldSet = [])
120119
if (is_int($field)) {
121120
$field = $definition;
122121
}
122+
$field = Inflector::camel2id(Inflector::variablize($field), '_');
123123
if (empty($fieldSet) || in_array($field, $fieldSet, true)) {
124124
$result[$field] = $definition;
125125
}

src/Serializer.php

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use yii\web\Linkable;
1515
use yii\web\Request;
1616
use yii\web\Response;
17-
use yii\helpers\Inflector;
1817

1918
class Serializer extends Component
2019
{
@@ -54,6 +53,21 @@ class Serializer extends Component
5453
*/
5554
public $pluralize = true;
5655

56+
/**
57+
* Prepares the member name that should be returned.
58+
* If not set, all member names will be converted to recommended format.
59+
* For example, both 'firstName' and 'first_name' will be converted to 'first-name'.
60+
* @var callable
61+
*/
62+
public $prepareMemberName = ['tuyakhov\jsonapi\Inflector', 'var2member'];
63+
64+
/**
65+
* Converts a member name to an attribute name.
66+
* @var callable
67+
*/
68+
public $formatMemberName = ['tuyakhov\jsonapi\Inflector', 'member2var'];
69+
70+
5771
/**
5872
* @inheritdoc
5973
*/
@@ -94,10 +108,14 @@ public function serialize($data)
94108
protected function serializeModel(ResourceInterface $model)
95109
{
96110
$fields = $this->getRequestedFields();
111+
$type = $this->pluralize ? Inflector::pluralize($model->getType()) : $model->getType();
112+
$fields = isset($fields[$type]) ? $fields[$type] : [];
113+
114+
$attributes = $model->getResourceAttributes($fields);
115+
$attributes = array_combine($this->prepareMemberNames(array_keys($attributes)), array_values($attributes));
97116

98-
$attributes = isset($fields[$model->getType()]) ? $fields[$model->getType()] : [];
99117
$data = array_merge($this->serializeIdentifier($model), [
100-
'attributes' => $model->getResourceAttributes($attributes),
118+
'attributes' => $attributes,
101119
]);
102120

103121
$included = $this->getIncluded();
@@ -114,16 +132,17 @@ protected function serializeModel(ResourceInterface $model)
114132
} elseif ($items instanceof ResourceIdentifierInterface) {
115133
$relationship = $this->serializeIdentifier($items);
116134
}
117-
118135
if (!empty($relationship)) {
136+
$memberName = $this->prepareMemberNames([$name]);
137+
$memberName = reset($memberName);
119138
if (in_array($name, $included)) {
120-
$data['relationships'][$name]['data'] = $relationship;
139+
$data['relationships'][$memberName]['data'] = $relationship;
121140
}
122-
}
123-
if ($model instanceof LinksInterface) {
124-
$links = $model->getRelationshipLinks($name);
125-
if (!empty($links)) {
126-
$data['relationships'][$name]['links'] = Link::serialize($links);
141+
if ($model instanceof LinksInterface) {
142+
$links = $model->getRelationshipLinks($memberName);
143+
if (!empty($links)) {
144+
$data['relationships'][$memberName]['links'] = Link::serialize($links);
145+
}
127146
}
128147
}
129148
}
@@ -291,14 +310,26 @@ protected function getRequestedFields()
291310
$fields = [];
292311
}
293312
foreach ($fields as $key => $field) {
294-
$fields[$key] = preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY);
313+
$fields[$key] = array_map($this->formatMemberName, preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY));
295314
}
296315
return $fields;
297316
}
298317

299318
protected function getIncluded()
300319
{
301320
$include = $this->request->get($this->expandParam);
302-
return is_string($include) ? preg_split('/\s*,\s*/', $include, -1, PREG_SPLIT_NO_EMPTY) : [];
321+
return is_string($include) ? array_map($this->formatMemberName, preg_split('/\s*,\s*/', $include, -1, PREG_SPLIT_NO_EMPTY)) : [];
322+
}
323+
324+
325+
/**
326+
* Format member names according to recommendations for JSON API implementations
327+
* @link http://jsonapi.org/format/#document-member-names
328+
* @param array $memberNames
329+
* @return array
330+
*/
331+
protected function prepareMemberNames(array $memberNames = [])
332+
{
333+
return array_map($this->prepareMemberName, $memberNames);
303334
}
304335
}

tests/JsonApiParserTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public function testParse()
2121
'attributes' => [
2222
'field1' => 'test',
2323
'field2' => 2,
24+
'first-name' => 'Bob'
2425
],
2526
'relationships' => [
2627
'author' => [
@@ -33,7 +34,8 @@ public function testParse()
3334
$this->assertEquals([
3435
'ResourceModel' => [
3536
'field1' => 'test',
36-
'field2' => 2
37+
'field2' => 2,
38+
'first_name' => 'Bob',
3739
],
3840
'author' => [
3941
'ResourceModel' => [

0 commit comments

Comments
 (0)