Skip to content

Commit 65ec704

Browse files
committed
add inflector
1 parent 45a8e61 commit 65ec704

File tree

6 files changed

+126
-56
lines changed

6 files changed

+126
-56
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: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
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
@@ -18,15 +16,15 @@ class JsonApiParser extends JsonParser
1816
* For example, 'articles' will be converted to 'Article'
1917
* @var callable
2018
*/
21-
public $formNameCallback;
19+
public $formNameCallback = ['tuyakhov\jsonapi\Inflector', 'type2form'];
2220

2321
/**
2422
* Converts member names to variable names
2523
* If not set, all special characters will be replaced by underscore
2624
* For example, 'first-name' will be converted to 'first_name'
2725
* @var callable
2826
*/
29-
public $memberNameCallback;
27+
public $memberNameCallback = ['tuyakhov\jsonapi\Inflector', 'member2var'];
3028

3129
/**
3230
* Parse resource object into the input data to populates the model
@@ -75,10 +73,7 @@ public function parse($rawBody, $contentType)
7573
*/
7674
protected function typeToFormName($type)
7775
{
78-
if ($this->formNameCallback !== null) {
79-
return call_user_func($this->formNameCallback, $type);
80-
}
81-
return Inflector::id2camel(Inflector::singularize($type));
76+
return call_user_func($this->formNameCallback, $type);
8277
}
8378

8479
/**
@@ -87,9 +82,6 @@ protected function typeToFormName($type)
8782
*/
8883
protected function parseMemberNames(array $memberNames = [])
8984
{
90-
$callback = $this->memberNameCallback !== null ? $this->memberNameCallback : function($name) {
91-
return str_replace(' ', '_', preg_replace('/[^A-Za-z0-9]+/', ' ', $name));
92-
};
93-
return array_map($callback, $memberNames);
85+
return array_map($this->memberNameCallback, $memberNames);
9486
}
9587
}

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: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@
1010
use yii\base\Model;
1111
use yii\data\DataProviderInterface;
1212
use yii\data\Pagination;
13-
use yii\helpers\Inflector;
1413
use yii\web\Link;
1514
use yii\web\Linkable;
1615
use yii\web\Request;
1716
use yii\web\Response;
18-
use yii\helpers\Inflector;
1917

2018
class Serializer extends Component
2119
{
@@ -61,7 +59,13 @@ class Serializer extends Component
6159
* For example, both 'firstName' and 'first_name' will be converted to 'first-name'.
6260
* @var callable
6361
*/
64-
public $prepareMemberName;
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'];
6569

6670

6771
/**
@@ -104,7 +108,8 @@ public function serialize($data)
104108
protected function serializeModel(ResourceInterface $model)
105109
{
106110
$fields = $this->getRequestedFields();
107-
$fields = isset($fields[$model->getType()]) ? $fields[$model->getType()] : [];
111+
$type = $this->pluralize ? Inflector::pluralize($model->getType()) : $model->getType();
112+
$fields = isset($fields[$type]) ? $fields[$type] : [];
108113

109114
$attributes = $model->getResourceAttributes($fields);
110115
$attributes = array_combine($this->prepareMemberNames(array_keys($attributes)), array_values($attributes));
@@ -127,15 +132,16 @@ protected function serializeModel(ResourceInterface $model)
127132
} elseif ($items instanceof ResourceIdentifierInterface) {
128133
$relationship = $this->serializeIdentifier($items);
129134
}
130-
131135
if (!empty($relationship)) {
136+
$memberName = $this->prepareMemberNames([$name]);
137+
$memberName = reset($memberName);
132138
if (in_array($name, $included)) {
133-
$data['relationships'][$name]['data'] = $relationship;
139+
$data['relationships'][$memberName]['data'] = $relationship;
134140
}
135141
if ($model instanceof LinksInterface) {
136-
$links = $model->getRelationshipLinks($name);
142+
$links = $model->getRelationshipLinks($memberName);
137143
if (!empty($links)) {
138-
$data['relationships'][$name]['links'] = Link::serialize($links);
144+
$data['relationships'][$memberName]['links'] = Link::serialize($links);
139145
}
140146
}
141147
}
@@ -304,15 +310,15 @@ protected function getRequestedFields()
304310
$fields = [];
305311
}
306312
foreach ($fields as $key => $field) {
307-
$fields[$key] = preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY);
313+
$fields[$key] = array_map($this->formatMemberName, preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY));
308314
}
309315
return $fields;
310316
}
311317

312318
protected function getIncluded()
313319
{
314320
$include = $this->request->get($this->expandParam);
315-
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)) : [];
316322
}
317323

318324

@@ -324,9 +330,6 @@ protected function getIncluded()
324330
*/
325331
protected function prepareMemberNames(array $memberNames = [])
326332
{
327-
$callback = $this->prepareMemberName !== null ? $this->prepareMemberName : function($name) {
328-
return Inflector::camel2id(Inflector::variablize($name));
329-
};
330-
return array_map($callback, $memberNames);
333+
return array_map($this->prepareMemberName, $memberNames);
331334
}
332335
}

tests/SerializerTest.php

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,12 @@ public function testExpand()
9393
'field2' => 2,
9494
],
9595
];
96-
$includedModel['relationships'] = [
97-
'extraField1' => [
98-
'links' => [
99-
'self' => ['href' => 'http://example.com/resource/123/relationships/extraField1'],
100-
'related' => ['href' => 'http://example.com/resource/123/extraField1'],
101-
]
102-
]
103-
];
10496
$compoundModel['relationships'] = [
105-
'extraField1' => [
97+
'extra-field1' => [
10698
'data' => ['id' => '123', 'type' => 'resource-models'],
10799
'links' => [
108-
'self' => ['href' => 'http://example.com/resource/123/relationships/extraField1'],
109-
'related' => ['href' => 'http://example.com/resource/123/extraField1'],
100+
'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'],
101+
'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
110102
]
111103
]
112104
];
@@ -118,28 +110,28 @@ public function testExpand()
118110
ResourceModel::$extraFields = ['extraField1'];
119111
$model->extraField1 = new ResourceModel();
120112

121-
\Yii::$app->request->setQueryParams(['include' => 'extraField1']);
113+
\Yii::$app->request->setQueryParams(['include' => 'extra-field1']);
122114
$this->assertSame([
123115
'data' => $compoundModel,
124116
'included' => [
125117
$includedModel
126118
]
127119
], $serializer->serialize($model));
128120

129-
\Yii::$app->request->setQueryParams(['include' => 'extraField1,extraField2']);
121+
\Yii::$app->request->setQueryParams(['include' => 'extra-field1,extra-field2']);
130122
$this->assertSame([
131123
'data' => $compoundModel,
132124
'included' => [
133125
$includedModel
134126
]
135127
], $serializer->serialize($model));
136128

137-
\Yii::$app->request->setQueryParams(['include' => 'field1,extraField2']);
129+
\Yii::$app->request->setQueryParams(['include' => 'field1,extra-field2']);
138130
$compoundModel['relationships'] = [
139-
'extraField1' => [
131+
'extra-field1' => [
140132
'links' => [
141-
'self' => ['href' => 'http://example.com/resource/123/relationships/extraField1'],
142-
'related' => ['href' => 'http://example.com/resource/123/extraField1'],
133+
'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'],
134+
'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
143135
]
144136
]
145137
];
@@ -156,10 +148,10 @@ public function dataProviderSerializeDataProvider()
156148
$expectedBob = ['id' => '123', 'type' => 'resource-models',
157149
'attributes' => ['username' => 'Bob'],
158150
'links' => ['self' => ['href' => 'http://example.com/resource/123']],
159-
'relationships' => ['extraField1' => [
151+
'relationships' => ['extra-field1' => [
160152
'links' => [
161-
'related' => ['href' => 'http://example.com/resource/123/extraField1'],
162-
'self' => ['href' => 'http://example.com/resource/123/relationships/extraField1']
153+
'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
154+
'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1']
163155
]
164156
]]];
165157
$tom = new ResourceModel();
@@ -169,10 +161,10 @@ public function dataProviderSerializeDataProvider()
169161
'id' => '123', 'type' => 'resource-models',
170162
'attributes' => ['username' => 'Tom'],
171163
'links' => ['self' => ['href' => 'http://example.com/resource/123']],
172-
'relationships' => ['extraField1' => [
164+
'relationships' => ['extra-field1' => [
173165
'links' => [
174-
'related' => ['href' => 'http://example.com/resource/123/extraField1'],
175-
'self' => ['href' => 'http://example.com/resource/123/relationships/extraField1']
166+
'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
167+
'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1']
176168
]
177169
]]];
178170
return [
@@ -297,9 +289,7 @@ public function testFieldSets()
297289
ResourceModel::$id = 123;
298290
ResourceModel::$fields = ['field1', 'first_name', 'field2'];
299291
$model = new ResourceModel();
300-
301-
\Yii::$app->request->setQueryParams(['fields' => ['resource-models' => 'first_name']]);
302-
$this->assertSame([
292+
$expectedModel = [
303293
'data' => [
304294
'id' => '123',
305295
'type' => 'resource-models',
@@ -310,7 +300,13 @@ public function testFieldSets()
310300
'self' => ['href' => 'http://example.com/resource/123']
311301
]
312302
]
313-
], $serializer->serialize($model));
303+
];
304+
\Yii::$app->request->setQueryParams(['fields' => ['resource-models' => 'first-name']]);
305+
$this->assertSame($expectedModel, $serializer->serialize($model));
306+
$serializer->pluralize = false;
307+
\Yii::$app->request->setQueryParams(['fields' => ['resource-model' => 'first-name']]);
308+
$expectedModel['data']['type'] = 'resource-model';
309+
$this->assertSame($expectedModel, $serializer->serialize($model));
314310
}
315311

316312
public function testTypeInflection()

0 commit comments

Comments
 (0)