├── .gitignore
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src
├── Controller.php
├── Inflector.php
├── JsonApiParser.php
├── JsonApiResponseFormatter.php
├── LinksInterface.php
├── Pagination.php
├── ResourceIdentifierInterface.php
├── ResourceInterface.php
├── ResourceTrait.php
├── Serializer.php
├── UrlRule.php
└── actions
│ ├── Action.php
│ ├── CreateAction.php
│ ├── DeleteAction.php
│ ├── DeleteRelationshipAction.php
│ ├── IndexAction.php
│ ├── UpdateAction.php
│ ├── UpdateRelationshipAction.php
│ ├── ViewAction.php
│ └── ViewRelatedAction.php
└── tests
├── JsonApiParserTest.php
├── JsonApiResponseFormatterTest.php
├── SerializerTest.php
├── TestCase.php
├── actions
├── CreateActionTest.php
├── DeleteActionTest.php
├── DeleteRelationshipActionTest.php
├── IndexActionTest.php
├── UpdateActionTest.php
├── UpdateRelationshipActionTest.php
└── ViewRelatedActionTest.php
├── bootstrap.php
└── data
├── ActiveQuery.php
└── ResourceModel.php
/.gitignore:
--------------------------------------------------------------------------------
1 | # phpstorm project files
2 | .idea
3 |
4 | # netbeans project files
5 | nbproject
6 |
7 | # zend studio for eclipse project files
8 | .buildpath
9 | .project
10 | .settings
11 |
12 | # windows thumbnail cache
13 | Thumbs.db
14 |
15 | # composer vendor dir
16 | /vendor
17 |
18 | # composer itself is not needed
19 | composer.phar
20 | composer.lock
21 |
22 | # Mac DS_Store Files
23 | .DS_Store
24 |
25 | # phpunit itself is not needed
26 | phpunit.phar
27 | # local phpunit config
28 | /phpunit.xml
29 |
30 | # local tests configuration
31 | /tests/data/config.local.php
32 |
33 | /tests/runtime
34 | /tests/docker
35 | /tests/dockerids
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | Implementation of JSON API specification for the Yii framework
4 | ==================================================================
5 | [](https://packagist.org/packages/tuyakhov/yii2-json-api)
6 | [](https://scrutinizer-ci.com/g/tuyakhov/yii2-json-api/?branch=master) [](https://scrutinizer-ci.com/g/tuyakhov/yii2-json-api/build-status/master)
7 | [](https://packagist.org/packages/tuyakhov/yii2-json-api)
8 |
9 | Installation
10 | ------------
11 |
12 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
13 |
14 | Either run
15 |
16 | ```
17 | php composer.phar require --prefer-dist tuyakhov/yii2-json-api "*"
18 | ```
19 |
20 | or add
21 |
22 | ```
23 | "tuyakhov/yii2-json-api": "*"
24 | ```
25 |
26 | to the require section of your `composer.json` file.
27 |
28 | Data Serializing and Content Negotiation:
29 | -------------------------------------------
30 | Controller:
31 | ```php
32 | class Controller extends \yii\rest\Controller
33 | {
34 | public $serializer = 'tuyakhov\jsonapi\Serializer';
35 |
36 | public function behaviors()
37 | {
38 | return ArrayHelper::merge(parent::behaviors(), [
39 | 'contentNegotiator' => [
40 | 'class' => ContentNegotiator::className(),
41 | 'formats' => [
42 | 'application/vnd.api+json' => Response::FORMAT_JSON,
43 | ],
44 | ]
45 | ]);
46 | }
47 | }
48 | ```
49 | By default, the value of `type` is automatically pluralized.
50 | You can change this behavior by setting `tuyakhov\jsonapi\Serializer::$pluralize` property:
51 | ```php
52 | class Controller extends \yii\rest\Controller
53 | {
54 | public $serializer = [
55 | 'class' => 'tuyakhov\jsonapi\Serializer',
56 | 'pluralize' => false, // makes {"type": "user"}, instead of {"type": "users"}
57 | ];
58 | }
59 | ```
60 | Defining models:
61 | 1) Let's define `User` model and declare an `articles` relation
62 | ```php
63 | use tuyakhov\jsonapi\ResourceTrait;
64 | use tuyakhov\jsonapi\ResourceInterface;
65 |
66 | class User extends ActiveRecord implements ResourceInterface
67 | {
68 | use ResourceTrait;
69 |
70 | public function getArticles()
71 | {
72 | return $this->hasMany(Article::className(), ['author_id' => 'id']);
73 | }
74 | }
75 | ```
76 | 2) Now we need to define `Article` model
77 | ```php
78 | use tuyakhov\jsonapi\ResourceTrait;
79 | use tuyakhov\jsonapi\ResourceInterface;
80 |
81 | class Article extends ActiveRecord implements ResourceInterface
82 | {
83 | use ResourceTrait;
84 | }
85 | ```
86 | 3) As the result `User` model will be serialized into the proper json api resource object:
87 | ```javascript
88 | {
89 | "data": {
90 | "type": "users",
91 | "id": "1",
92 | "attributes": {
93 | // ... this user's attributes
94 | },
95 | "relationships": {
96 | "articles": {
97 | // ... this user's articles
98 | }
99 | }
100 | }
101 | }
102 | ```
103 | Controlling JSON API output
104 | ------------------------------
105 | The JSON response is generated by the `tuyakhov\jsonapi\JsonApiResponseFormatter` class which will
106 | use the `yii\helpers\Json` helper internally. This formatter can be configured with different options like
107 | for example the `$prettyPrint` option, which is useful on development for
108 | better readable responses, or `$encodeOptions` to control the output
109 | of the JSON encoding.
110 |
111 | The formatter can be configured in the `yii\web\Response::formatters` property of the `response` application
112 | component in the application configuration like the following:
113 |
114 | ```php
115 | 'response' => [
116 | // ...
117 | 'formatters' => [
118 | \yii\web\Response::FORMAT_JSON => [
119 | 'class' => 'tuyakhov\jsonapi\JsonApiResponseFormatter',
120 | 'prettyPrint' => YII_DEBUG, // use "pretty" output in debug mode
121 | 'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
122 | ],
123 | ],
124 | ],
125 | ```
126 | Links
127 | ---------------------------
128 | Your resource classes may support HATEOAS by implementing the `LinksInterface`.
129 | The interface contains `getLinks()` method which should return a list of links.
130 | Typically, you should return at least the `self` link representing the URL to the resource object itself.
131 | In order to appear the links in relationships `getLinks()` method should return `self` link.
132 | Based on this link each relationship will generate `self` and `related` links.
133 | By default it happens by appending a relationship name at the end of the `self` link of the primary model,
134 | you can simply change that behavior by overwriting `getRelationshipLinks()` method.
135 | For example,
136 | ```php
137 | class User extends ActiveRecord implements ResourceInterface, LinksInterface
138 | {
139 | use ResourceTrait;
140 |
141 | public function getLinks()
142 | {
143 | return [
144 | Link::REL_SELF => Url::to(['user/view', 'id' => $this->id], true),
145 | ];
146 | }
147 | }
148 | ```
149 | As the result:
150 | ```javascript
151 | {
152 | "data": {
153 | "type": "users",
154 | "id": "1",
155 | // ... this user's attributes
156 | "relationships": {
157 | "articles": {
158 | // ... article's data
159 | "links": {
160 | "self": {"href": "http://yourdomain.com/users/1/relationships/articles"},
161 | "related": {"href": "http://yourdomain.com/users/1/articles"}
162 | }
163 | }
164 | }
165 | "links": {
166 | "self": {"href": "http://yourdomain.com/users/1"}
167 | }
168 | }
169 | }
170 | ```
171 | Pagination
172 | ---------------------------
173 | The `page` query parameter family is reserved for pagination.
174 | This library implements a page-based strategy and allows the usage of query parameters such as `page[number]` and `page[size]`
175 | Example: `http://yourdomain.com/users?page[number]=3&page[size]=10`
176 |
177 | Enabling JSON API Input
178 | ---------------------------
179 | 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
180 | ```php
181 | 'request' => [
182 | 'parsers' => [
183 | 'application/vnd.api+json' => 'tuyakhov\jsonapi\JsonApiParser',
184 | ]
185 | ]
186 | ```
187 | By default it parses a HTTP request body so that you can populate model attributes with user inputs.
188 | For example the request body:
189 | ```javascript
190 | {
191 | "data": {
192 | "type": "users",
193 | "id": "1",
194 | "attributes": {
195 | "first-name": "Bob",
196 | "last-name": "Homster"
197 | }
198 | }
199 | }
200 | ```
201 | Will be resolved into the following array:
202 | ```php
203 | // var_dump($_POST);
204 | [
205 | "User" => [
206 | "first_name" => "Bob",
207 | "last_name" => "Homster"
208 | ]
209 | ]
210 | ```
211 | So you can access request body by calling `\Yii::$app->request->post()` and simply populate the model with input data:
212 | ```php
213 | $model = new User();
214 | $model->load(\Yii::$app->request->post());
215 | ```
216 | By default type `users` will be converted into `User` (singular, camelCase) which corresponds to the model's `formName()` method (which you may override).
217 | You can override the `JsonApiParser::formNameCallback` property which refers to a callback that converts 'type' member to form name.
218 | 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.
219 |
220 | Examples
221 | --------
222 | Controller:
223 | ```php
224 | class UserController extends \yii\rest\Controller
225 | {
226 | public $serializer = 'tuyakhov\jsonapi\Serializer';
227 |
228 | /**
229 | * @inheritdoc
230 | */
231 | public function behaviors()
232 | {
233 | return ArrayHelper::merge(parent::behaviors(), [
234 | 'contentNegotiator' => [
235 | 'class' => ContentNegotiator::className(),
236 | 'formats' => [
237 | 'application/vnd.api+json' => Response::FORMAT_JSON,
238 | ],
239 | ]
240 | ]);
241 | }
242 |
243 | /**
244 | * @inheritdoc
245 | */
246 | public function actions()
247 | {
248 | return [
249 | 'create' => [
250 | 'class' => 'tuyakhov\jsonapi\actions\CreateAction',
251 | 'modelClass' => ExampleModel::className()
252 | ],
253 | 'update' => [
254 | 'class' => 'tuyakhov\jsonapi\actions\UpdateAction',
255 | 'modelClass' => ExampleModel::className()
256 | ],
257 | 'view' => [
258 | 'class' => 'tuyakhov\jsonapi\actions\ViewAction',
259 | 'modelClass' => ExampleModel::className(),
260 | ],
261 | 'delete' => [
262 | 'class' => 'tuyakhov\jsonapi\actions\DeleteAction',
263 | 'modelClass' => ExampleModel::className(),
264 | ],
265 | 'view-related' => [
266 | 'class' => 'tuyakhov\jsonapi\actions\ViewRelatedAction',
267 | 'modelClass' => ExampleModel::className()
268 | ],
269 | 'update-relationship' => [
270 | 'class' => 'tuyakhov\jsonapi\actions\UpdateRelationshipAction',
271 | 'modelClass' => ExampleModel::className()
272 | ],
273 | 'delete-relationship' => [
274 | 'class' => 'tuyakhov\jsonapi\actions\DeleteRelationshipAction',
275 | 'modelClass' => ExampleModel::className()
276 | ],
277 | 'options' => [
278 | 'class' => 'yii\rest\OptionsAction',
279 | ],
280 | ];
281 | }
282 | }
283 |
284 | ```
285 |
286 | Model:
287 | ```php
288 | class User extends ActiveRecord implements LinksInterface, ResourceInterface
289 | {
290 | use ResourceTrait;
291 |
292 | public function getLinks()
293 | {
294 | $reflect = new \ReflectionClass($this);
295 | $controller = Inflector::camel2id($reflect->getShortName());
296 | return [
297 | Link::REL_SELF => Url::to(["$controller/view", 'id' => $this->getId()], true)
298 | ];
299 | }
300 | }
301 | ```
302 |
303 | Configuration file `config/main.php`:
304 | ```php
305 | return [
306 | // ...
307 | 'components' => [
308 | 'request' => [
309 | 'parsers' => [
310 | 'application/vnd.api+json' => 'tuyakhov\jsonapi\JsonApiParser',
311 | ]
312 | ],
313 | 'response' => [
314 | 'format' => \yii\web\Response::FORMAT_JSON,
315 | 'formatters' => [
316 | \yii\web\Response::FORMAT_JSON => 'tuyakhov\jsonapi\JsonApiResponseFormatter'
317 | ]
318 | ],
319 | 'urlManager' => [
320 | 'rules' => [
321 | [
322 | 'class' => 'tuyakhov\jsonapi\UrlRule',
323 | 'controller' => ['user'],
324 | ],
325 |
326 | ]
327 | ]
328 | // ...
329 | ]
330 | // ...
331 | ]
332 | ```
333 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tuyakhov/yii2-json-api",
3 | "type": "yii2-extension",
4 | "description": "Implementation of JSON API specification for the Yii framework",
5 | "keywords": ["yii2", "json api", "api", "json", "rest", "json-api"],
6 | "homepage": "https://github.com/tuyakhov/yii2-json-api",
7 | "license": "MIT",
8 | "support": {
9 | "issues": "https://github.com/tuyakhov/yii2-json-api/issues",
10 | "source": "https://github.com/tuyakhov/yii2-json-api"
11 | },
12 | "authors": [
13 | {
14 | "name": "Anton Tuyakhov",
15 | "email": "atuyakhov@gmail.com",
16 | "role": "Developer"
17 | }
18 | ],
19 | "require": {
20 | "yiisoft/yii2": "^2.0.13"
21 | },
22 | "require-dev": {
23 | "phpunit/phpunit": "5.5.*"
24 | },
25 | "autoload": {
26 | "psr-4": {
27 | "tuyakhov\\jsonapi\\": "src/"
28 | }
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "tuyakhov\\jsonapi\\tests\\": "tests/"
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | ./tests
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/Controller.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 |
9 | use yii\filters\ContentNegotiator;
10 | use yii\helpers\ArrayHelper;
11 | use yii\web\Response;
12 |
13 | class Controller extends \yii\rest\Controller
14 | {
15 | public $serializer = 'tuyakhov\jsonapi\Serializer';
16 |
17 | /**
18 | * @inheritdoc
19 | */
20 | public function behaviors()
21 | {
22 | return ArrayHelper::merge(parent::behaviors(), [
23 | 'contentNegotiator' => [
24 | 'class' => ContentNegotiator::className(),
25 | 'formats' => [
26 | 'application/vnd.api+json' => Response::FORMAT_JSON,
27 | ],
28 | ]
29 | ]);
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/src/Inflector.php:
--------------------------------------------------------------------------------
1 |
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:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 | use yii\helpers\ArrayHelper;
9 | use yii\web\BadRequestHttpException;
10 | use \yii\web\JsonParser;
11 |
12 | class JsonApiParser extends JsonParser
13 | {
14 | /**
15 | * Converts 'type' member to form name
16 | * If not set, type will be converted to singular form.
17 | * For example, 'articles' will be converted to 'Article'
18 | * @var callable
19 | */
20 | public $formNameCallback = ['tuyakhov\jsonapi\Inflector', 'type2form'];
21 |
22 | /**
23 | * Converts member names to variable names
24 | * If not set, all special characters will be replaced by underscore
25 | * For example, 'first-name' will be converted to 'first_name'
26 | * @var callable
27 | */
28 | public $memberNameCallback = ['tuyakhov\jsonapi\Inflector', 'member2var'];
29 |
30 | /**
31 | * Parse resource object into the input data to populates the model
32 | * @inheritdoc
33 | */
34 | public function parse($rawBody, $contentType)
35 | {
36 | $array = parent::parse($rawBody, $contentType);
37 | if (!empty($array) && !ArrayHelper::keyExists('data', $array)) {
38 | if ($this->throwException) {
39 | throw new BadRequestHttpException('The request MUST include a single resource object as primary data.');
40 | }
41 | return [];
42 | }
43 | $data = ArrayHelper::getValue($array, 'data', []);
44 | if (empty($data)) {
45 | return [];
46 | }
47 | if (ArrayHelper::isAssociative($data)) {
48 | $result = $this->parseResource($data);
49 |
50 | $relObjects = ArrayHelper::getValue($data, 'relationships', []);
51 | $result['relationships'] = $this->parseRelationships($relObjects);
52 | } else {
53 | foreach ($data as $object) {
54 | $resource = $this->parseResource($object);
55 | foreach (array_keys($resource) as $key) {
56 | $result[$key][] = $resource[$key];
57 | }
58 | }
59 | }
60 |
61 | return isset($result) ? $result : $array;
62 | }
63 |
64 | /**
65 | * @param $type 'type' member of the document
66 | * @return string form name
67 | */
68 | protected function typeToFormName($type)
69 | {
70 | return call_user_func($this->formNameCallback, $type);
71 | }
72 |
73 | /**
74 | * @param array $memberNames
75 | * @return array variable names
76 | */
77 | protected function parseMemberNames(array $memberNames = [])
78 | {
79 | return array_map($this->memberNameCallback, $memberNames);
80 | }
81 |
82 | /**
83 | * @param $item
84 | * @return array
85 | * @throws BadRequestHttpException
86 | */
87 | protected function parseResource($item)
88 | {
89 | if (!$type = ArrayHelper::getValue($item, 'type')) {
90 | if ($this->throwException) {
91 | throw new BadRequestHttpException('The resource object MUST contain at least a type member');
92 | }
93 | return [];
94 | }
95 | $formName = $this->typeToFormName($type);
96 |
97 | $attributes = ArrayHelper::getValue($item, 'attributes', []);
98 | $attributes = array_combine($this->parseMemberNames(array_keys($attributes)), array_values($attributes));
99 |
100 | if ($id = ArrayHelper::getValue($item, 'id')) {
101 | $attributes['id'] = $id;
102 | }
103 |
104 | return [$formName => $attributes];
105 | }
106 |
107 | /**
108 | * @param array $relObjects
109 | * @return array
110 | */
111 | protected function parseRelationships(array $relObjects = [])
112 | {
113 | $relationships = [];
114 | foreach ($relObjects as $name => $relationship) {
115 | if (!ArrayHelper::keyExists('data', $relationship)) {
116 | continue;
117 | }
118 | $relData = ArrayHelper::getValue($relationship, 'data', []);
119 | if (!ArrayHelper::isIndexed($relData)) {
120 | $relData = [$relData];
121 | }
122 | $relationships[$name] = [];
123 | foreach ($relData as $identifier) {
124 | if (isset($identifier['type']) && isset($identifier['id'])) {
125 | $formName = $this->typeToFormName($identifier['type']);
126 | $relationships[$name][$formName][] = ['id' => $identifier['id']];
127 | }
128 | }
129 | }
130 | return $relationships;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/JsonApiResponseFormatter.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 | use yii\base\Component;
9 | use yii\helpers\ArrayHelper;
10 | use yii\helpers\Json;
11 | use yii\helpers\Url;
12 | use yii\web\ErrorHandler;
13 | use yii\web\Link;
14 | use yii\web\Response;
15 | use yii\web\ResponseFormatterInterface;
16 |
17 | class JsonApiResponseFormatter extends Component implements ResponseFormatterInterface
18 | {
19 | /**
20 | * Mapping between the error handler component and JSON API error object
21 | * @see ErrorHandler::convertExceptionToArray()
22 | */
23 | const ERROR_EXCEPTION_MAPPING = [
24 | 'title' => 'name',
25 | 'detail' => 'message',
26 | 'code' => 'code',
27 | 'status' => 'status'
28 | ];
29 | /**
30 | * An error object MAY have the following members
31 | * @link http://jsonapi.org/format/#error-objects
32 | */
33 | const ERROR_ALLOWED_MEMBERS = [
34 | 'id', 'links', 'status', 'code', 'title', 'detail', 'source', 'meta'
35 | ];
36 | /**
37 | * @var integer the encoding options passed to [[Json::encode()]]. For more details please refer to
38 | * .
39 | * Default is `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`.
40 | */
41 | public $encodeOptions = 320;
42 | /**
43 | * @var bool whether to format the output in a readable "pretty" format. This can be useful for debugging purpose.
44 | * If this is true, `JSON_PRETTY_PRINT` will be added to [[encodeOptions]].
45 | * Defaults to `false`.
46 | */
47 | public $prettyPrint = false;
48 |
49 | /**
50 | * Formats response data in JSON format.
51 | * @link http://jsonapi.org/format/upcoming/#document-structure
52 | * @param Response $response
53 | */
54 | public function format($response)
55 | {
56 | $response->getHeaders()->set('Content-Type', 'application/vnd.api+json; charset=UTF-8');
57 | $options = $this->encodeOptions;
58 | if ($this->prettyPrint) {
59 | $options |= JSON_PRETTY_PRINT;
60 | }
61 |
62 | $apiDocument = $response->data;
63 |
64 | if (!$response->isEmpty && empty($apiDocument)) {
65 | $apiDocument = ['data' => $response->data];
66 | if (\Yii::$app->controller) {
67 | $apiDocument['links'] = Link::serialize([
68 | Link::REL_SELF => Url::current([], true)
69 | ]);
70 | }
71 | }
72 |
73 | if ($response->isClientError || $response->isServerError) {
74 | if (ArrayHelper::isAssociative($response->data)) {
75 | $response->data = [$response->data];
76 | }
77 | $formattedErrors = [];
78 | foreach ($response->data as $error) {
79 | $formattedError = array_intersect_key($error, array_flip(static::ERROR_ALLOWED_MEMBERS));
80 | foreach (static::ERROR_EXCEPTION_MAPPING as $member => $key) {
81 | if (isset($error[$key])) {
82 | $formattedError[$member] = (string) $error[$key];
83 | }
84 | }
85 | if (!empty($formattedError)) {
86 | $formattedErrors[] = $formattedError;
87 | }
88 | }
89 | $apiDocument = ['errors' => $formattedErrors];
90 | }
91 | if ($apiDocument !== null) {
92 | $response->content = Json::encode($apiDocument, $options);
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/LinksInterface.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 | use \yii\web\Linkable;
9 |
10 | interface LinksInterface extends Linkable
11 | {
12 | public function getRelationshipLinks($name);
13 | }
--------------------------------------------------------------------------------
/src/Pagination.php:
--------------------------------------------------------------------------------
1 | params === null) {
28 | $request = Yii::$app->getRequest();
29 | $params = $request instanceof Request ? $request->getQueryParam('page') : [];
30 | if (!is_array($params)) {
31 | $params = [];
32 | }
33 | $this->params = $params;
34 | }
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/src/ResourceIdentifierInterface.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 | /**
9 | * Interface for a “resource identifier object” that identifies an individual resource.
10 | */
11 | interface ResourceIdentifierInterface
12 | {
13 | /**
14 | * The "id" member of a resource object.
15 | * @return string an ID that in pair with type uniquely identifies the resource.
16 | */
17 | public function getId();
18 |
19 | /**
20 | * The "type" member of a resource object.
21 | * @return string a type that identifies the resource.
22 | */
23 | public function getType();
24 | }
25 |
--------------------------------------------------------------------------------
/src/ResourceInterface.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 | /**
9 | * Interface for a “resource object” that represent an individual resource.
10 | */
11 | interface ResourceInterface extends ResourceIdentifierInterface
12 | {
13 | /**
14 | * The "attributes" member of the resource object representing some of the resource’s data.
15 | * @param array $fields specific fields that a client has requested.
16 | * @return array an array of attributes that represent information about the resource object in which it’s defined.
17 | */
18 | public function getResourceAttributes(array $fields = []);
19 |
20 | /**
21 | * The "relationships" member of the resource object describing relationships between the resource and other JSON API resources.
22 | * @param array $linked specific resource linkage that a client has requested.
23 | * @return ResourceIdentifierInterface[] represent references from the resource object in which it’s defined to other resource objects.
24 | */
25 | public function getResourceRelationships(array $linked = []);
26 |
27 | public function setResourceRelationship($name, $relationship);
28 |
29 | }
--------------------------------------------------------------------------------
/src/ResourceTrait.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 | use yii\base\Arrayable;
9 | use yii\db\ActiveRecordInterface;
10 | use yii\web\Link;
11 | use yii\web\Linkable;
12 |
13 | trait ResourceTrait
14 | {
15 | /**
16 | * @var bool a flag that enables/disables deleting of the model that contains the foreign key when setting relationships
17 | * By default the model's foreign key will be set `null` and saved.
18 | */
19 | protected $allowDeletingResources = false;
20 |
21 | /**
22 | * @return string
23 | */
24 | public function getId()
25 | {
26 | return (string) ($this instanceof ActiveRecordInterface ? $this->getPrimaryKey() : null);
27 | }
28 |
29 | /**
30 | * @return string
31 | */
32 | public function getType()
33 | {
34 | $reflect = new \ReflectionClass($this);
35 | $className = $reflect->getShortName();
36 | return Inflector::camel2id($className);
37 | }
38 |
39 | /**
40 | * @param array $fields
41 | * @return array
42 | */
43 | public function getResourceAttributes(array $fields = [])
44 | {
45 | $attributes = [];
46 | if ($this instanceof Arrayable) {
47 | $fieldDefinitions = $this->fields();
48 | } else {
49 | $vars = array_keys(\Yii::getObjectVars($this));
50 | $fieldDefinitions = array_combine($vars, $vars);
51 | }
52 |
53 | foreach ($this->resolveFields($fieldDefinitions, $fields) as $name => $definition) {
54 | $attributes[$name] = is_string($definition) ? $this->$definition : call_user_func($definition, $this, $name);
55 | }
56 | return $attributes;
57 | }
58 |
59 | /**
60 | * @param array $linked
61 | * @return array
62 | */
63 | public function getResourceRelationships(array $linked = [])
64 | {
65 | $fields = [];
66 | if ($this instanceof Arrayable) {
67 | $fields = $this->extraFields();
68 | }
69 | $resolvedFields = $this->resolveFields($fields);
70 | $keys = array_keys($resolvedFields);
71 |
72 | $relationships = array_fill_keys($keys, null);
73 | $linkedFields = array_intersect($keys, $linked);
74 |
75 | foreach ($linkedFields as $name) {
76 | $definition = $resolvedFields[$name];
77 | $relationships[$name] = is_string($definition) ? $this->$definition : call_user_func($definition, $this, $name);
78 | }
79 |
80 | return $relationships;
81 | }
82 |
83 | /**
84 | * @param string $name the case sensitive name of the relationship.
85 | * @param array|ActiveRecordInterface $relationship
86 | */
87 | public function setResourceRelationship($name, $relationship)
88 | {
89 | /** @var $this ActiveRecordInterface */
90 | if (!$this instanceof ActiveRecordInterface) {
91 | return;
92 | }
93 | if (!is_array($relationship)) {
94 | $relationship = [$relationship];
95 | }
96 | $this->unlinkAll($name, $this->allowDeletingResources);
97 | foreach ($relationship as $key => $value) {
98 | if ($value instanceof ActiveRecordInterface) {
99 | $this->link($name, $value);
100 | }
101 | }
102 | }
103 |
104 | /**
105 | * @param string $name the case sensitive name of the relationship.
106 | * @return array
107 | */
108 | public function getRelationshipLinks($name)
109 | {
110 | if (!$this instanceof Linkable) {
111 | return [];
112 | }
113 | $primaryLinks = $this->getLinks();
114 | if (!array_key_exists(Link::REL_SELF, $primaryLinks)) {
115 | return [];
116 | }
117 | $resourceLink = is_string($primaryLinks[Link::REL_SELF]) ? rtrim($primaryLinks[Link::REL_SELF], '/') : null;
118 | if (!$resourceLink) {
119 | return [];
120 | }
121 | return [
122 | Link::REL_SELF => "{$resourceLink}/relationships/{$name}",
123 | 'related' => "{$resourceLink}/{$name}",
124 | ];
125 | }
126 |
127 | /**
128 | * @param array $fields
129 | * @param array $fieldSet
130 | * @return array
131 | */
132 | protected function resolveFields(array $fields, array $fieldSet = [])
133 | {
134 | $result = [];
135 |
136 | foreach ($fields as $field => $definition) {
137 | if (is_int($field)) {
138 | $field = $definition;
139 | }
140 | $field = Inflector::camel2id(Inflector::variablize($field), '_');
141 | if (empty($fieldSet) || in_array($field, $fieldSet, true)) {
142 | $result[$field] = $definition;
143 | }
144 | }
145 |
146 | return $result;
147 | }
148 |
149 | /**
150 | * @param $value boolean
151 | */
152 | public function setAllowDeletingResources($value)
153 | {
154 | $this->allowDeletingResources = $value;
155 | }
156 |
157 | /**
158 | * @return bool
159 | */
160 | public function getAllowDeletingResources()
161 | {
162 | return $this->allowDeletingResources;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/Serializer.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 | use yii\base\Component;
9 | use yii\base\InvalidValueException;
10 | use yii\base\Model;
11 | use yii\data\DataProviderInterface;
12 | use yii\data\Pagination;
13 | use yii\web\Link;
14 | use yii\web\Linkable;
15 | use yii\web\Request;
16 | use yii\web\Response;
17 |
18 | class Serializer extends Component
19 | {
20 | /**
21 | * @var string the name of the query parameter containing the information about which fields should be returned
22 | * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined
23 | * by [[Model::fields()]] will be returned.
24 | */
25 | public $fieldsParam = 'fields';
26 | /**
27 | * @var string the name of the query parameter containing the information about which fields should be returned
28 | * in addition to those listed in [[fieldsParam]] for a resource object.
29 | */
30 | public $expandParam = 'include';
31 | /**
32 | * @var string the name of the envelope (e.g. `_links`) for returning the links objects.
33 | * It takes effect only, if `collectionEnvelope` is set.
34 | * @since 2.0.4
35 | */
36 | public $linksEnvelope = 'links';
37 | /**
38 | * @var string the name of the envelope (e.g. `_meta`) for returning the pagination object.
39 | * It takes effect only, if `collectionEnvelope` is set.
40 | * @since 2.0.4
41 | */
42 | public $metaEnvelope = 'meta';
43 | /**
44 | * @var Request the current request. If not set, the `request` application component will be used.
45 | */
46 | public $request;
47 | /**
48 | * @var Response the response to be sent. If not set, the `response` application component will be used.
49 | */
50 | public $response;
51 | /**
52 | * @var bool whether to automatically pluralize the `type` of resource.
53 | */
54 | public $pluralize = true;
55 |
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 |
71 | /**
72 | * @inheritdoc
73 | */
74 | public function init()
75 | {
76 | if ($this->request === null) {
77 | $this->request = \Yii::$app->getRequest();
78 | }
79 | if ($this->response === null) {
80 | $this->response = \Yii::$app->getResponse();
81 | }
82 | }
83 |
84 | /**
85 | * Serializes the given data into a format that can be easily turned into other formats.
86 | * This method mainly converts the objects of recognized types into array representation.
87 | * It will not do conversion for unknown object types or non-object data.
88 | * @param mixed $data the data to be serialized.
89 | * @return mixed the converted data.
90 | */
91 | public function serialize($data)
92 | {
93 | if ($data instanceof Model && $data->hasErrors()) {
94 | return $this->serializeModelErrors($data);
95 | } elseif ($data instanceof ResourceInterface) {
96 | return $this->serializeResource($data);
97 | } elseif ($data instanceof DataProviderInterface) {
98 | return $this->serializeDataProvider($data);
99 | } else {
100 | return $data;
101 | }
102 | }
103 |
104 | /**
105 | * @param array $included
106 | * @param ResourceInterface $model
107 | * @return array
108 | */
109 | protected function serializeModel(ResourceInterface $model, array $included = [])
110 | {
111 | $fields = $this->getRequestedFields();
112 | $type = $this->pluralize ? Inflector::pluralize($model->getType()) : $model->getType();
113 | $fields = isset($fields[$type]) ? $fields[$type] : [];
114 |
115 | $topLevel = array_map(function($item) {
116 | if (($pos = strrpos($item, '.')) !== false) {
117 | return substr($item, 0, $pos);
118 | }
119 | return $item;
120 | }, $included);
121 |
122 | $attributes = $model->getResourceAttributes($fields);
123 | $attributes = array_combine($this->prepareMemberNames(array_keys($attributes)), array_values($attributes));
124 |
125 | $data = array_merge($this->serializeIdentifier($model), [
126 | 'attributes' => $attributes,
127 | ]);
128 |
129 | $relationships = $model->getResourceRelationships($topLevel);
130 | if (!empty($relationships)) {
131 | foreach ($relationships as $name => $items) {
132 | $relationship = [];
133 | if (is_array($items)) {
134 | foreach ($items as $item) {
135 | if ($item instanceof ResourceIdentifierInterface) {
136 | $relationship[] = $this->serializeIdentifier($item);
137 | }
138 | }
139 | } elseif ($items instanceof ResourceIdentifierInterface) {
140 | $relationship = $this->serializeIdentifier($items);
141 | }
142 | $memberName = $this->prepareMemberNames([$name]);
143 | $memberName = reset($memberName);
144 | if (!empty($relationship)) {
145 | $data['relationships'][$memberName]['data'] = $relationship;
146 | }
147 | if ($model instanceof LinksInterface) {
148 | $links = $model->getRelationshipLinks($memberName);
149 | if (!empty($links)) {
150 | $data['relationships'][$memberName]['links'] = Link::serialize($links);
151 | }
152 | }
153 | }
154 | }
155 |
156 | if ($model instanceof Linkable) {
157 | $data['links'] = Link::serialize($model->getLinks());
158 | }
159 |
160 | return $data;
161 | }
162 |
163 | /**
164 | * @param ResourceInterface $resource
165 | * @return array
166 | */
167 | protected function serializeResource(ResourceInterface $resource)
168 | {
169 | if ($this->request->getIsHead()) {
170 | return null;
171 | } else {
172 | $included = $this->getIncluded();
173 | $data = [
174 | 'data' => $this->serializeModel($resource, $included)
175 | ];
176 |
177 | $relatedResources = $this->serializeIncluded($resource, $included);
178 | if (!empty($relatedResources)) {
179 | $data['included'] = $relatedResources;
180 | }
181 |
182 | return $data;
183 | }
184 | }
185 |
186 | /**
187 | * Serialize resource identifier object and make type juggling
188 | * @link http://jsonapi.org/format/#document-resource-object-identification
189 | * @param ResourceIdentifierInterface $identifier
190 | * @return array
191 | */
192 | protected function serializeIdentifier(ResourceIdentifierInterface $identifier)
193 | {
194 | $result = [];
195 | foreach (['id', 'type'] as $key) {
196 | $getter = 'get' . ucfirst($key);
197 | $value = $identifier->$getter();
198 | if ($value === null || is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
199 | throw new InvalidValueException("The value {$key} of resource object " . get_class($identifier) . ' MUST be a string.');
200 | }
201 | if ($key === 'type' && $this->pluralize) {
202 | $value = Inflector::pluralize($value);
203 | }
204 | $result[$key] = (string) $value;
205 | }
206 | return $result;
207 | }
208 |
209 | /**
210 | * @param ResourceInterface|array $resources
211 | * @param array $included
212 | * @param true $assoc
213 | * @return array
214 | */
215 | protected function serializeIncluded($resources, array $included = [], $assoc = false)
216 | {
217 | $resources = is_array($resources) ? $resources : [$resources];
218 | $data = [];
219 |
220 | $inclusion = [];
221 | foreach ($included as $path) {
222 | if (($pos = strrpos($path, '.')) === false) {
223 | $inclusion[$path] = [];
224 | continue;
225 | }
226 | $name = substr($path, $pos + 1);
227 | $key = substr($path, 0, $pos);
228 | $inclusion[$key][] = $name;
229 | }
230 |
231 | foreach ($resources as $resource) {
232 | if (!$resource instanceof ResourceInterface) {
233 | continue;
234 | }
235 | $relationships = $resource->getResourceRelationships(array_keys($inclusion));
236 | foreach ($relationships as $name => $relationship) {
237 | if ($relationship === null) {
238 | continue;
239 | }
240 | if (!is_array($relationship)) {
241 | $relationship = [$relationship];
242 | }
243 | foreach ($relationship as $model) {
244 | if (!$model instanceof ResourceInterface) {
245 | continue;
246 | }
247 | $uniqueKey = $model->getType() . '/' . $model->getId();
248 | if (!isset($data[$uniqueKey])) {
249 | $data[$uniqueKey] = $this->serializeModel($model, $inclusion[$name]);
250 | }
251 | if (!empty($inclusion[$name])) {
252 | $data = array_merge($data, $this->serializeIncluded($model, $inclusion[$name], true));
253 | }
254 | }
255 | }
256 | }
257 |
258 | return $assoc ? $data : array_values($data);
259 | }
260 |
261 | /**
262 | * Serializes a data provider.
263 | * @param DataProviderInterface $dataProvider
264 | * @return null|array the array representation of the data provider.
265 | */
266 | protected function serializeDataProvider($dataProvider)
267 | {
268 | if ($this->request->getIsHead()) {
269 | return null;
270 | } else {
271 | $models = $dataProvider->getModels();
272 | $data = [];
273 |
274 | $included = $this->getIncluded();
275 | foreach ($models as $model) {
276 | if ($model instanceof ResourceInterface) {
277 | $data[] = $this->serializeModel($model, $included);
278 | }
279 | }
280 |
281 | $result = ['data' => $data];
282 |
283 | $relatedResources = $this->serializeIncluded($models, $included);
284 | if (!empty($relatedResources)) {
285 | $result['included'] = $relatedResources;
286 | }
287 |
288 | if (($pagination = $dataProvider->getPagination()) !== false) {
289 | return array_merge($result, $this->serializePagination($pagination));
290 | }
291 |
292 | return $result;
293 | }
294 | }
295 |
296 | /**
297 | * Serializes a pagination into an array.
298 | * @param Pagination $pagination
299 | * @return array the array representation of the pagination
300 | * @see addPaginationHeaders()
301 | */
302 | protected function serializePagination($pagination)
303 | {
304 | return [
305 | $this->linksEnvelope => Link::serialize($pagination->getLinks(true)),
306 | $this->metaEnvelope => [
307 | 'total-count' => $pagination->totalCount,
308 | 'page-count' => $pagination->getPageCount(),
309 | 'current-page' => $pagination->getPage() + 1,
310 | 'per-page' => $pagination->getPageSize(),
311 | ],
312 | ];
313 | }
314 |
315 | /**
316 | * Serializes the validation errors in a model.
317 | * @param Model $model
318 | * @return array the array representation of the errors
319 | */
320 | protected function serializeModelErrors($model)
321 | {
322 | $this->response->setStatusCode(422, 'Data Validation Failed.');
323 | $result = [];
324 | foreach ($model->getFirstErrors() as $name => $message) {
325 | $memberName = call_user_func($this->prepareMemberName, $name);
326 | $result[] = [
327 | 'source' => ['pointer' => "/data/attributes/{$memberName}"],
328 | 'detail' => $message,
329 | 'status' => '422'
330 | ];
331 | }
332 |
333 | return $result;
334 | }
335 |
336 | /**
337 | * @return array
338 | */
339 | protected function getRequestedFields()
340 | {
341 | $fields = $this->request->get($this->fieldsParam);
342 |
343 | if (!is_array($fields)) {
344 | $fields = [];
345 | }
346 | foreach ($fields as $key => $field) {
347 | $fields[$key] = array_map($this->formatMemberName, preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY));
348 | }
349 | return $fields;
350 | }
351 |
352 | /**
353 | * @return array|null
354 | */
355 | protected function getIncluded()
356 | {
357 | $include = $this->request->get($this->expandParam);
358 | return is_string($include) ? array_map($this->formatMemberName, preg_split('/\s*,\s*/', $include, -1, PREG_SPLIT_NO_EMPTY)) : [];
359 | }
360 |
361 |
362 | /**
363 | * Format member names according to recommendations for JSON API implementations
364 | * @link http://jsonapi.org/format/#document-member-names
365 | * @param array $memberNames
366 | * @return array
367 | */
368 | protected function prepareMemberNames(array $memberNames = [])
369 | {
370 | return array_map($this->prepareMemberName, $memberNames);
371 | }
372 | }
373 |
--------------------------------------------------------------------------------
/src/UrlRule.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi;
7 |
8 |
9 | /**
10 | * UrlRule is provided to simplify the creation of URL rules for JSON API support.
11 | * @package tuyakhov\jsonapi
12 | */
13 | class UrlRule extends \yii\rest\UrlRule
14 | {
15 | /**
16 | * @inheritdoc
17 | */
18 | public function init()
19 | {
20 | $this->tokens = array_merge($this->tokens, array_merge([
21 | '{relationship}' => ''
22 | ]));
23 | $this->patterns = array_merge($this->patterns, [
24 | 'DELETE {id}/relationships/{relationship}' => 'delete-relationship',
25 | 'POST,PATCH {id}/relationships/{relationship}' => 'update-relationship',
26 | 'GET {id}/{relationship}' => 'view-related',
27 | '{id}/{relationship}' => 'options'
28 | ]);
29 | parent::init();
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/src/actions/Action.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\actions;
7 |
8 | use tuyakhov\jsonapi\ResourceInterface;
9 | use tuyakhov\jsonapi\ResourceTrait;
10 | use yii\db\ActiveRecordInterface;
11 | use yii\db\BaseActiveRecord;
12 | use yii\helpers\ArrayHelper;
13 |
14 | class Action extends \yii\rest\Action
15 | {
16 | /**
17 | * Links the relationships with primary model.
18 | * @var callable
19 | */
20 | public $linkRelationships;
21 |
22 | /**
23 | * @var bool Weather allow to do a full replacement of a to-many relationship
24 | */
25 | public $allowFullReplacement = true;
26 |
27 | /**
28 | * @var bool Weather allow to delete the underlying resource if a relationship is deleted (as a garbage collection measure)
29 | */
30 | public $enableResourceDeleting = false;
31 |
32 | /**
33 | * Links the relationships with primary model.
34 | * @param $model ActiveRecordInterface
35 | * @param array $data relationship links
36 | */
37 | protected function linkRelationships($model, array $data = [])
38 | {
39 | if ($this->linkRelationships !== null) {
40 | call_user_func($this->linkRelationships, $this, $model, $data);
41 | return;
42 | }
43 |
44 | if (!$model instanceof ResourceInterface) {
45 | return;
46 | }
47 |
48 | foreach ($data as $name => $relationship) {
49 | if (!$related = $model->getRelation($name, false)) {
50 | continue;
51 | }
52 | /** @var BaseActiveRecord $relatedClass */
53 | $relatedClass = new $related->modelClass;
54 | $relationships = ArrayHelper::keyExists($relatedClass->formName(), $relationship) ? $relationship[$relatedClass->formName()] : [];
55 |
56 | $ids = [];
57 | foreach ($relationships as $index => $relObject) {
58 | if (!isset($relObject['id'])) {
59 | continue;
60 | }
61 | $ids[] = $relObject['id'];
62 | }
63 |
64 | if ($related->multiple && !$this->allowFullReplacement) {
65 | continue;
66 | }
67 | $records = $relatedClass::find()->andWhere(['in', $relatedClass::primaryKey(), $ids])->all();
68 |
69 | /** @see ResourceTrait::$allowDeletingResources */
70 | if (method_exists($model, 'setAllowDeletingResources')) {
71 | $model->setAllowDeletingResources($this->enableResourceDeleting);
72 | }
73 |
74 | $model->setResourceRelationship($name, $records);
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/actions/CreateAction.php:
--------------------------------------------------------------------------------
1 |
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/DeleteAction.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\actions;
7 |
8 | use Yii;
9 | use yii\db\BaseActiveRecord;
10 | use yii\web\NotFoundHttpException;
11 | use yii\web\ServerErrorHttpException;
12 |
13 | /**
14 | * Implements the API endpoint for deleting resources.
15 | * @link http://jsonapi.org/format/#crud-deleting
16 | */
17 | class DeleteAction extends Action
18 | {
19 | /**
20 | * Deletes a resource.
21 | * @param mixed $id id of the resource to be deleted.
22 | * @throws NotFoundHttpException
23 | * @throws ServerErrorHttpException on failure.
24 | */
25 | public function run($id)
26 | {
27 | /** @var BaseActiveRecord $model */
28 | $model = $this->findModel($id);
29 |
30 | if ($this->checkAccess) {
31 | call_user_func($this->checkAccess, $this->id, $model);
32 | }
33 |
34 | if ($model->delete() === false) {
35 | throw new ServerErrorHttpException('Failed to delete the resource for unknown reason.');
36 | }
37 |
38 | Yii::$app->getResponse()->setStatusCode(204);
39 | }
40 | }
--------------------------------------------------------------------------------
/src/actions/DeleteRelationshipAction.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\actions;
7 |
8 | use Yii;
9 | use yii\data\ActiveDataProvider;
10 | use yii\db\ActiveRecordInterface;
11 | use yii\db\ActiveRelationTrait;
12 | use yii\db\BaseActiveRecord;
13 | use yii\helpers\ArrayHelper;
14 | use yii\web\ForbiddenHttpException;
15 | use yii\web\NotFoundHttpException;
16 |
17 | /**
18 | * Deletes the specified members from a relationship
19 | * @link http://jsonapi.org/format/#crud-updating-relationships
20 | */
21 | class DeleteRelationshipAction extends Action
22 | {
23 | /**
24 | * Removes the relationships from primary model.
25 | * @var callable
26 | */
27 | public $unlinkRelationships;
28 |
29 | /**
30 | * @param string $id an ID of the primary resource
31 | * @param string $name a name of the related resource
32 | * @return ActiveDataProvider
33 | * @throws ForbiddenHttpException
34 | * @throws NotFoundHttpException
35 | * @throws \yii\base\InvalidConfigException
36 | */
37 | public function run($id, $name)
38 | {
39 | /** @var BaseActiveRecord $model */
40 | $model = $this->findModel($id);
41 |
42 | if (!$related = $model->getRelation($name, false)) {
43 | throw new NotFoundHttpException('Relationship does not exist');
44 | }
45 |
46 | if (!$related->multiple) {
47 | throw new ForbiddenHttpException('Unsupported request to update relationship');
48 | }
49 |
50 | if ($this->checkAccess) {
51 | call_user_func($this->checkAccess, $this->id, $model, $name);
52 | }
53 |
54 | $this->unlinkRelationships($model, [$name => Yii::$app->getRequest()->getBodyParams()]);
55 |
56 |
57 | return new ActiveDataProvider([
58 | 'query' => $related
59 | ]);
60 | }
61 |
62 | /**
63 | * Removes the relationships from primary model.
64 | * @param $model ActiveRecordInterface
65 | * @param array $data relationship links
66 | */
67 | protected function unlinkRelationships($model, array $data = [])
68 | {
69 | if ($this->unlinkRelationships !== null) {
70 | call_user_func($this->unlinkRelationships, $this, $model, $data);
71 | return;
72 | }
73 |
74 | foreach ($data as $name => $relationship) {
75 | /** @var $related ActiveRelationTrait */
76 | if (!$related = $model->getRelation($name, false)) {
77 | continue;
78 | }
79 | /** @var BaseActiveRecord $relatedClass */
80 | $relatedClass = new $related->modelClass;
81 | $relationships = ArrayHelper::keyExists($relatedClass->formName(), $relationship) ? $relationship[$relatedClass->formName()] : [];
82 |
83 | $ids = [];
84 | foreach ($relationships as $index => $relObject) {
85 | if (!isset($relObject['id'])) {
86 | continue;
87 | }
88 | $ids[] = $relObject['id'];
89 | }
90 |
91 | $records = $relatedClass::find()->andWhere(['in', $relatedClass::primaryKey(), $ids])->all();
92 | foreach ($records as $record) {
93 | $model->unlink($name, $record, $this->enableResourceDeleting);
94 | }
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/src/actions/IndexAction.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\actions;
7 |
8 |
9 | use tuyakhov\jsonapi\Inflector;
10 | use tuyakhov\jsonapi\Pagination;
11 | use yii\data\ActiveDataProvider;
12 | use yii\data\DataFilter;
13 | use Yii;
14 |
15 | class IndexAction extends Action
16 | {
17 | /**
18 | * @var callable a PHP callable that will be called to prepare a data provider that
19 | * should return a collection of the models. If not set, [[prepareDataProvider()]] will be used instead.
20 | * The signature of the callable should be:
21 | *
22 | * ```php
23 | * function (IndexAction $action) {
24 | * // $action is the action object currently running
25 | * }
26 | * ```
27 | *
28 | * The callable should return an instance of [[ActiveDataProvider]].
29 | *
30 | * If [[dataFilter]] is set the result of [[DataFilter::build()]] will be passed to the callable as a second parameter.
31 | * In this case the signature of the callable should be the following:
32 | *
33 | * ```php
34 | * function (IndexAction $action, mixed $filter) {
35 | * // $action is the action object currently running
36 | * // $filter the built filter condition
37 | * }
38 | * ```
39 | */
40 | public $prepareDataProvider;
41 | /**
42 | * @var DataFilter|null data filter to be used for the search filter composition.
43 | * You must setup this field explicitly in order to enable filter processing.
44 | * For example:
45 | *
46 | * ```php
47 | * [
48 | * 'class' => 'yii\data\ActiveDataFilter',
49 | * 'searchModel' => function () {
50 | * return (new \yii\base\DynamicModel(['id' => null, 'name' => null, 'price' => null]))
51 | * ->addRule('id', 'integer')
52 | * ->addRule('name', 'trim')
53 | * ->addRule('name', 'string')
54 | * ->addRule('price', 'number');
55 | * },
56 | * ]
57 | * ```
58 | *
59 | * @see DataFilter
60 | *
61 | * @since 2.0.13
62 | */
63 | public $dataFilter;
64 |
65 |
66 | /**
67 | * @return ActiveDataProvider
68 | * @throws \yii\base\InvalidConfigException
69 | */
70 | public function run()
71 | {
72 | if ($this->checkAccess) {
73 | call_user_func($this->checkAccess, $this->id);
74 | }
75 |
76 | return $this->prepareDataProvider();
77 | }
78 |
79 | /**
80 | * Prepares the data provider that should return the requested collection of the models.
81 | * @return mixed|null|object|DataFilter|ActiveDataProvider
82 | * @throws \yii\base\InvalidConfigException
83 | */
84 | protected function prepareDataProvider()
85 | {
86 | $filter = $this->getFilter();
87 |
88 | if ($this->prepareDataProvider !== null) {
89 | return call_user_func($this->prepareDataProvider, $this, $filter);
90 | }
91 |
92 | /* @var $modelClass \yii\db\BaseActiveRecord */
93 | $modelClass = $this->modelClass;
94 |
95 | $query = $modelClass::find();
96 | if (!empty($filter)) {
97 | $query->andWhere($filter);
98 | }
99 |
100 | return Yii::createObject([
101 | 'class' => ActiveDataProvider::className(),
102 | 'query' => $query,
103 | 'pagination' => [
104 | 'class' => Pagination::className(),
105 | ],
106 | 'sort' => [
107 | 'enableMultiSort' => true
108 | ]
109 | ]);
110 | }
111 |
112 | protected function getFilter()
113 | {
114 | if ($this->dataFilter === null) {
115 | return null;
116 | }
117 | $requestParams = Yii::$app->getRequest()->getQueryParam('filter', []);
118 | $attributeMap = [];
119 | foreach ($requestParams as $attribute => $value) {
120 | $attributeMap[$attribute] = Inflector::camel2id(Inflector::variablize($attribute), '_');
121 | if (is_string($value) && strpos($value, ',') !== false) {
122 | $requestParams[$attribute] = ['in' => explode(',', $value)];
123 | }
124 | }
125 | $config = array_merge(['attributeMap' => $attributeMap], $this->dataFilter);
126 | /** @var DataFilter $dataFilter */
127 | $dataFilter = Yii::createObject($config);
128 | if ($dataFilter->load(['filter' => $requestParams])) {
129 | return $dataFilter->build();
130 | }
131 | return null;
132 | }
133 | }
--------------------------------------------------------------------------------
/src/actions/UpdateAction.php:
--------------------------------------------------------------------------------
1 |
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 | }
--------------------------------------------------------------------------------
/src/actions/UpdateRelationshipAction.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\actions;
7 |
8 | use yii\data\ActiveDataProvider;
9 | use yii\db\BaseActiveRecord;
10 | use yii\web\BadRequestHttpException;
11 | use yii\web\NotFoundHttpException;
12 | use Yii;
13 |
14 | /**
15 | * UpdateRelationshipAction implements the API endpoint for updating relationships.
16 | * @link http://jsonapi.org/format/#crud-updating-relationships
17 | */
18 | class UpdateRelationshipAction extends Action
19 | {
20 | /**
21 | * Update of relationships independently.
22 | * @param string $id an ID of the primary resource
23 | * @param string $name a name of the related resource
24 | * @return ActiveDataProvider|BaseActiveRecord
25 | * @throws BadRequestHttpException
26 | * @throws NotFoundHttpException
27 | */
28 | public function run($id, $name)
29 | {
30 | /** @var BaseActiveRecord $model */
31 | $model = $this->findModel($id);
32 |
33 | if (!$related = $model->getRelation($name, false)) {
34 | throw new NotFoundHttpException('Relationship does not exist');
35 | }
36 |
37 | if ($this->checkAccess) {
38 | call_user_func($this->checkAccess, $this->id, $model, $name);
39 | }
40 |
41 | $this->linkRelationships($model, [$name => Yii::$app->getRequest()->getBodyParams()]);
42 |
43 | if ($related->multiple) {
44 | return new ActiveDataProvider([
45 | 'query' => $related
46 | ]);
47 | } else {
48 | return $related->one();
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/actions/ViewAction.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\actions;
7 |
8 |
9 | class ViewAction extends \yii\rest\ViewAction
10 | {
11 |
12 | }
--------------------------------------------------------------------------------
/src/actions/ViewRelatedAction.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\actions;
7 |
8 |
9 | use tuyakhov\jsonapi\Pagination;
10 | use tuyakhov\jsonapi\ResourceInterface;
11 | use yii\data\ActiveDataProvider;
12 | use yii\db\ActiveQuery;
13 | use yii\web\BadRequestHttpException;
14 | use yii\web\NotFoundHttpException;
15 |
16 | class ViewRelatedAction extends Action
17 | {
18 | /**
19 | * Prepares the data provider that should return the requested collection of the models.
20 | * @var callable
21 | */
22 | public $prepareDataProvider;
23 |
24 | /**
25 | * @param $id
26 | * @param $name
27 | * @return ActiveDataProvider|\yii\db\ActiveRecordInterface
28 | * @throws BadRequestHttpException
29 | * @throws NotFoundHttpException
30 | */
31 | public function run($id, $name)
32 | {
33 | $model = $this->findModel($id);
34 |
35 | if (!$model instanceof ResourceInterface) {
36 | throw new BadRequestHttpException('Impossible to fetch related resource');
37 | }
38 |
39 | /** @var ActiveQuery $related */
40 | if (!$related = $model->getRelation($name, false)) {
41 | throw new NotFoundHttpException('Resource does not exist');
42 | }
43 |
44 | if ($this->checkAccess) {
45 | call_user_func($this->checkAccess, $this->id, $model, $name);
46 | }
47 |
48 | if ($this->prepareDataProvider !== null) {
49 | return call_user_func($this->prepareDataProvider, $this, $related, $name);
50 | }
51 |
52 | if ($related->multiple) {
53 | return new ActiveDataProvider([
54 | 'query' => $related,
55 | 'pagination' => [
56 | 'class' => Pagination::className(),
57 | ],
58 | ]);
59 | } else {
60 | return $related->one();
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/JsonApiParserTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\tests;
7 |
8 |
9 | use tuyakhov\jsonapi\JsonApiParser;
10 | use yii\helpers\Json;
11 | use yii\web\BadRequestHttpException;
12 |
13 | class JsonApiParserTest extends TestCase
14 | {
15 | public function testEmptyBody()
16 | {
17 | $parser = new JsonApiParser();
18 | $body = '';
19 | $this->assertEquals([], $parser->parse($body, ''));
20 | }
21 |
22 | public function testMissingData()
23 | {
24 | $parser = new JsonApiParser();
25 | $this->expectException(BadRequestHttpException::class);
26 | $body = Json::encode(['incorrect-member']);
27 | $parser->parse($body, '');
28 | }
29 |
30 | public function testSingleResource()
31 | {
32 | $parser = new JsonApiParser();
33 | $body = Json::encode([
34 | 'data' => [
35 | 'type' => 'resource-models',
36 | 'attributes' => [
37 | 'field1' => 'test',
38 | 'field2' => 2,
39 | 'first-name' => 'Bob'
40 | ],
41 | 'relationships' => [
42 | 'author' => [
43 | 'data' => [
44 | 'id' => '321',
45 | 'type' => 'resource-models'
46 | ]
47 | ],
48 | 'client' => [
49 | 'data' => [
50 | ['id' => '321', 'type' => 'resource-models'],
51 | ['id' => '123', 'type' => 'resource-models']
52 | ]
53 | ]
54 | ]
55 | ]
56 | ]);
57 | $this->assertEquals([
58 | 'ResourceModel' => [
59 | 'field1' => 'test',
60 | 'field2' => 2,
61 | 'first_name' => 'Bob',
62 | ],
63 | 'relationships' => [
64 | 'author' => [
65 | 'ResourceModel' => [
66 | ['id' => '321']
67 | ]
68 | ],
69 | 'client' => [
70 | 'ResourceModel' => [
71 | ['id' => '321'],
72 | ['id' => '123']
73 | ]
74 | ]
75 | ]
76 | ], $parser->parse($body, ''));
77 | }
78 |
79 | public function testMultiple()
80 | {
81 | $parser = new JsonApiParser();
82 | $resourceActual = [
83 | 'type' => 'resource-models',
84 | 'id' => 12,
85 | 'attributes' => [
86 | 'field1' => 'test',
87 | 'field2' => 2,
88 | 'first-name' => 'Bob'
89 | ],
90 | ];
91 | $resourceExpected = [
92 | 'id' => 12,
93 | 'field1' => 'test',
94 | 'field2' => 2,
95 | 'first_name' => 'Bob',
96 | ];
97 | $body = Json::encode([
98 | 'data' => [
99 | $resourceActual,
100 | $resourceActual
101 | ]
102 | ]);
103 | $this->assertEquals([
104 | 'ResourceModel' => [
105 | $resourceExpected,
106 | $resourceExpected
107 | ],
108 | ], $parser->parse($body, ''));
109 | }
110 | }
--------------------------------------------------------------------------------
/tests/JsonApiResponseFormatterTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 | namespace tuyakhov\jsonapi\tests;
6 |
7 |
8 | use tuyakhov\jsonapi\JsonApiResponseFormatter;
9 | use tuyakhov\jsonapi\Serializer;
10 | use tuyakhov\jsonapi\tests\data\ResourceModel;
11 | use yii\base\Controller;
12 | use yii\helpers\Json;
13 | use yii\web\Response;
14 | use yii\web\ServerErrorHttpException;
15 |
16 | class JsonApiResponseFormatterTest extends TestCase
17 | {
18 | public function testFormatException()
19 | {
20 | $formatter = new JsonApiResponseFormatter();
21 | $exception = new ServerErrorHttpException('Server error');
22 | $response = new Response();
23 | $response->setStatusCode($exception->statusCode);
24 | $response->data = [
25 | 'name' => $exception->getName(),
26 | 'message' => $exception->getMessage(),
27 | 'code' => $exception->getCode(),
28 | 'status' => $exception->statusCode
29 | ];
30 | $formatter->format($response);
31 | $this->assertJson($response->content);
32 | $this->assertSame(Json::encode([
33 | 'errors' => [
34 | [
35 | 'code' => '0',
36 | 'status' => '500',
37 | 'title' => Response::$httpStatuses[500],
38 | 'detail' => 'Server error',
39 | ]
40 | ]
41 | ]), $response->content);
42 | }
43 |
44 | public function testFormModelError()
45 | {
46 | $formatter = new JsonApiResponseFormatter();
47 | $exception = new ServerErrorHttpException('Server error');
48 | $response = new Response();
49 | $response->setStatusCode($exception->statusCode);
50 | $serializer = new Serializer();
51 | $model = new ResourceModel();
52 | $model->addError('field1', 'Error');
53 | $model->addError('field2', 'Test Error');
54 | $response->data = $serializer->serialize($model);
55 | $formatter->format($response);
56 | $this->assertJson($response->content);
57 | $this->assertSame(Json::encode([
58 | 'errors' => [
59 | [
60 | 'source' => ['pointer' => "/data/attributes/field1"],
61 | 'detail' => 'Error',
62 | 'status' => '422'
63 | ],
64 | [
65 | 'source' => ['pointer' => "/data/attributes/field2"],
66 | 'detail' => 'Test Error',
67 | 'status' => '422'
68 | ]
69 | ]
70 | ]), $response->content);
71 | }
72 |
73 | public function testSuccessModel()
74 | {
75 | $formatter = new JsonApiResponseFormatter();
76 | $response = new Response();
77 | $serializer = new Serializer();
78 | $model = new ResourceModel();
79 | $response->data = $serializer->serialize($model);
80 | $response->setStatusCode(200);
81 | $formatter->format($response);
82 | $this->assertJson($response->content);
83 | $this->assertSame(Json::encode([
84 | 'data' => [
85 | 'id' => '123',
86 | 'type' => 'resource-models',
87 | 'attributes' => [
88 | 'field1' => 'test',
89 | 'field2' => 2,
90 | ],
91 | 'links' => [
92 | 'self' => [
93 | 'href' => 'http://example.com/resource/123'
94 | ]
95 | ]
96 | ]
97 | ]), $response->content);
98 | }
99 |
100 | public function testEmptyData()
101 | {
102 | $formatter = new JsonApiResponseFormatter();
103 | $response = new Response();
104 | $response->setStatusCode('200');
105 | $serializer = new Serializer();
106 | $response->data = $serializer->serialize(null);
107 | $formatter->format($response);
108 | $this->assertJson($response->content);
109 | $this->assertSame(Json::encode([
110 | 'data' => null
111 | ]), $response->content);
112 | \Yii::$app->controller = new Controller('test', \Yii::$app);
113 | $formatter->format($response);
114 | $this->assertJson($response->content);
115 | $this->assertSame(Json::encode([
116 | 'data' => null,
117 | 'links' => [
118 | 'self' => ['href' => '/index.php?r=test']
119 | ]
120 | ]), $response->content);
121 | $response->clear();
122 | $response->setStatusCode(201);
123 | $formatter->format($response);
124 | $this->assertNull($response->content);
125 | }
126 | }
--------------------------------------------------------------------------------
/tests/SerializerTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 | namespace tuyakhov\jsonapi\tests;
6 |
7 | use tuyakhov\jsonapi\tests\data\ResourceModel;
8 | use tuyakhov\jsonapi\Serializer;
9 | use yii\base\InvalidValueException;
10 | use yii\data\ArrayDataProvider;
11 |
12 | class SerializerTest extends TestCase
13 | {
14 | protected function setUp()
15 | {
16 | parent::setUp();
17 | ResourceModel::$fields = ['field1', 'field2'];
18 | ResourceModel::$extraFields = [];
19 | }
20 |
21 | // https://github.com/tuyakhov/yii2-json-api/pull/9
22 | public function testSerializeIdentifier()
23 | {
24 | ResourceModel::$id = [];
25 | $model = new ResourceModel();
26 | $serializer = new Serializer();
27 | $this->expectException(InvalidValueException::class);
28 | $serializer->serialize($model);
29 | }
30 |
31 | public function testSerializeModelErrors()
32 | {
33 | $serializer = new Serializer();
34 | $model = new ResourceModel();
35 | $model->addError('field1', 'Test error');
36 | $model->addError('field2', 'Multiple error 1');
37 | $model->addError('field2', 'Multiple error 2');
38 | $model->addError('first_name', 'Member name check');
39 | $this->assertEquals([
40 | [
41 | 'source' => ['pointer' => "/data/attributes/field1"],
42 | 'detail' => 'Test error',
43 | 'status' => '422'
44 | ],
45 | [
46 | 'source' => ['pointer' => "/data/attributes/field2"],
47 | 'detail' => 'Multiple error 1',
48 | 'status' => '422'
49 | ],
50 | [
51 | 'source' => ['pointer' => "/data/attributes/first-name"],
52 | 'detail' => 'Member name check',
53 | 'status' => '422'
54 | ]
55 | ], $serializer->serialize($model));
56 | }
57 |
58 | public function testSerializeModelData()
59 | {
60 | $serializer = new Serializer();
61 | ResourceModel::$id = 123;
62 | $model = new ResourceModel();
63 | $this->assertSame([
64 | 'data' => [
65 | 'id' => '123',
66 | 'type' => 'resource-models',
67 | 'attributes' => [
68 | 'field1' => 'test',
69 | 'field2' => 2,
70 | ],
71 | 'links' => [
72 | 'self' => ['href' => 'http://example.com/resource/123']
73 | ]
74 | ]
75 | ], $serializer->serialize($model));
76 |
77 | ResourceModel::$fields = ['first_name'];
78 | ResourceModel::$extraFields = ['extraField1'];
79 | $model->extraField1 = new ResourceModel();
80 | $relationship = [
81 | 'data' => ['id' => '123', 'type' => 'resource-models'],
82 | 'links' => [
83 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'],
84 | 'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
85 | ]
86 | ];
87 | $resource = [
88 | 'id' => '123',
89 | 'type' => 'resource-models',
90 | 'attributes' => [
91 | 'first-name' => 'Bob',
92 | ],
93 | 'relationships' => [
94 | 'extra-field1' => [
95 | 'links' => $relationship['links']
96 | ]
97 | ],
98 | 'links' => [
99 | 'self' => ['href' => 'http://example.com/resource/123']
100 | ]
101 | ];
102 | $expected = [
103 | 'data' => $resource
104 | ];
105 | $this->assertSame($expected, $serializer->serialize($model));
106 | $_POST[$serializer->request->methodParam] = 'POST';
107 | \Yii::$app->request->setQueryParams(['include' => 'extra-field1']);
108 | $expected['included'][] = $resource;
109 | $expected['data']['relationships']['extra-field1'] = $relationship;
110 | $this->assertSame($expected, $serializer->serialize($model));
111 | }
112 |
113 | public function testExpand()
114 | {
115 | $serializer = new Serializer();
116 | $compoundModel = $includedModel = [
117 | 'id' => '123',
118 | 'type' => 'resource-models',
119 | 'attributes' => [
120 | 'field1' => 'test',
121 | 'field2' => 2,
122 | ],
123 | 'relationships' => [
124 | 'extra-field1' => [
125 | 'data' => ['id' => '123', 'type' => 'resource-models'],
126 | 'links' => [
127 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'],
128 | 'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
129 | ]
130 | ]
131 | ]
132 | ];
133 | unset($includedModel['relationships']['extra-field1']['data']);
134 | $compoundModel['links'] = $includedModel['links'] = [
135 | 'self' => ['href' => 'http://example.com/resource/123']
136 | ];
137 | $model = new ResourceModel();
138 | ResourceModel::$fields = ['field1', 'field2'];
139 | ResourceModel::$extraFields = ['extraField1'];
140 | $model->extraField1 = new ResourceModel();
141 |
142 | \Yii::$app->request->setQueryParams(['include' => 'extra-field1']);
143 | $this->assertSame([
144 | 'data' => $compoundModel,
145 | 'included' => [
146 | $includedModel
147 | ]
148 | ], $serializer->serialize($model));
149 |
150 | \Yii::$app->request->setQueryParams(['include' => 'extra-field1,extra-field2']);
151 | $this->assertSame([
152 | 'data' => $compoundModel,
153 | 'included' => [
154 | $includedModel
155 | ]
156 | ], $serializer->serialize($model));
157 |
158 | \Yii::$app->request->setQueryParams(['include' => 'field1,extra-field2']);
159 | $compoundModel['relationships'] = [
160 | 'extra-field1' => [
161 | 'links' => [
162 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'],
163 | 'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
164 | ]
165 | ]
166 | ];
167 | $this->assertSame([
168 | 'data' => $compoundModel
169 | ], $serializer->serialize($model));
170 | }
171 |
172 | public function testNestedRelationships()
173 | {
174 | ResourceModel::$fields = ['field1'];
175 | ResourceModel::$extraFields = ['extraField1'];
176 | $resource = new ResourceModel();
177 | $relationship = new ResourceModel();
178 | $subRelationship = new ResourceModel();
179 | $subRelationship->setId(321);
180 | $relationship->extraField1 = $subRelationship;
181 | $resource->extraField1 = $relationship;
182 | $compoundDocument = [
183 | 'data' => [
184 | 'id' => '123',
185 | 'type' => 'resource-models',
186 | 'attributes' => ['field1' => 'test'],
187 | 'relationships' => [
188 | 'extra-field1' => [
189 | 'data' => ['id' => '123', 'type' => 'resource-models'],
190 | 'links' => [
191 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'],
192 | 'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
193 | ]
194 | ],
195 | ],
196 | 'links' => ['self' => ['href' => 'http://example.com/resource/123']],
197 | ],
198 | 'included' => [
199 | [
200 | 'id' => '123',
201 | 'type' => 'resource-models',
202 | 'attributes' => ['field1' => 'test'],
203 | 'relationships' => [
204 | 'extra-field1' => [
205 | 'data' => ['id' => '321', 'type' => 'resource-models'],
206 | 'links' => [
207 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'],
208 | 'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
209 | ]
210 | ],
211 | ],
212 | 'links' => ['self' => ['href' => 'http://example.com/resource/123']],
213 | ],
214 | [
215 | 'id' => '321',
216 | 'type' => 'resource-models',
217 | 'attributes' => ['field1' => 'test'],
218 | 'relationships' => [
219 | 'extra-field1' => [
220 | 'links' => [
221 | 'self' => ['href' => 'http://example.com/resource/321/relationships/extra-field1'],
222 | 'related' => ['href' => 'http://example.com/resource/321/extra-field1'],
223 | ]
224 | ],
225 | ],
226 | 'links' => ['self' => ['href' => 'http://example.com/resource/321']],
227 | ]
228 | ]
229 | ];
230 |
231 | $serializer = new Serializer();
232 | \Yii::$app->request->setQueryParams(['include' => 'extra-field1.extra-field1']);
233 | $this->assertSame($compoundDocument, $serializer->serialize($resource));
234 | }
235 |
236 | public function testIncludedDuplicates()
237 | {
238 | $serializer = new Serializer();
239 |
240 | $compoundModel = $includedModel = [
241 | 'id' => '123',
242 | 'type' => 'resource-models',
243 | 'attributes' => [
244 | 'field1' => 'test',
245 | 'field2' => 2,
246 | ],
247 | 'relationships' => [
248 | 'extra-field1' => [
249 | 'data' => ['id' => '123', 'type' => 'resource-models'],
250 | 'links' => [
251 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'],
252 | 'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
253 | ]
254 | ],
255 | 'extra-field2' => [
256 | 'data' => ['id' => '123', 'type' => 'resource-models'],
257 | 'links' => [
258 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field2'],
259 | 'related' => ['href' => 'http://example.com/resource/123/extra-field2'],
260 | ]
261 | ]
262 | ]
263 | ];
264 | unset($includedModel['relationships']['extra-field1']['data']);
265 | unset($includedModel['relationships']['extra-field2']['data']);
266 | $compoundModel['links'] = $includedModel['links'] = [
267 | 'self' => ['href' => 'http://example.com/resource/123']
268 | ];
269 |
270 | $model = new ResourceModel();
271 | ResourceModel::$fields = ['field1', 'field2'];
272 | ResourceModel::$extraFields = ['extraField1', 'extraField2'];
273 | $relationship = new ResourceModel();
274 | $relationship->extraField1 = new ResourceModel();
275 | $model->extraField2 = $relationship;
276 | $model->extraField1 = new ResourceModel();
277 |
278 | \Yii::$app->request->setQueryParams(['include' => 'extra-field1,extra-field2.extra-field1']);
279 | $this->assertSame([
280 | 'data' => $compoundModel,
281 | 'included' => [
282 | $includedModel
283 | ]
284 | ], $serializer->serialize($model));
285 | $this->assertSame([
286 | 'data' => [$compoundModel],
287 | 'included' => [
288 | $includedModel
289 | ],
290 | 'links' => [
291 | 'self' => ['href' => '/index.php?r=&include=extra-field1%2Cextra-field2.extra-field1&page=1']
292 | ],
293 | 'meta' => [
294 | 'total-count' => 1,
295 | 'page-count' => 1,
296 | 'current-page' => 1,
297 | 'per-page' => 20
298 | ]
299 | ], $serializer->serialize(new ArrayDataProvider([
300 | 'allModels' => [$model],
301 | 'pagination' => [
302 | 'route' => '/',
303 | ],
304 | ])));
305 | }
306 |
307 | public function dataProviderSerializeDataProvider()
308 | {
309 | $bob = new ResourceModel();
310 | $bob->username = 'Bob';
311 | $bob->extraField1 = new ResourceModel();
312 | $expectedBob = ['id' => '123', 'type' => 'resource-models',
313 | 'attributes' => ['username' => 'Bob'],
314 | 'links' => ['self' => ['href' => 'http://example.com/resource/123']],
315 | 'relationships' => ['extra-field1' => [
316 | 'links' => [
317 | 'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
318 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1']
319 | ]
320 | ]]];
321 | $tom = new ResourceModel();
322 | $tom->username = 'Tom';
323 | $tom->extraField1 = new ResourceModel();
324 | $expectedTom = [
325 | 'id' => '123', 'type' => 'resource-models',
326 | 'attributes' => ['username' => 'Tom'],
327 | 'links' => ['self' => ['href' => 'http://example.com/resource/123']],
328 | 'relationships' => ['extra-field1' => [
329 | 'links' => [
330 | 'related' => ['href' => 'http://example.com/resource/123/extra-field1'],
331 | 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1']
332 | ]
333 | ]]];
334 | return [
335 | [
336 | new ArrayDataProvider([
337 | 'allModels' => [
338 | $bob,
339 | $tom
340 | ],
341 | 'pagination' => [
342 | 'route' => '/',
343 | ],
344 | ]),
345 | [
346 | 'data' => [
347 | $expectedBob,
348 | $expectedTom
349 | ],
350 | 'meta' => [
351 | 'total-count' => 2,
352 | 'page-count' => 1,
353 | 'current-page' => 1,
354 | 'per-page' => 20
355 | ],
356 | 'links' => [
357 | 'self' => ['href' => '/index.php?r=&page=1']
358 | ]
359 | ]
360 | ],
361 | [
362 | new ArrayDataProvider([
363 | 'allModels' => [
364 | $bob,
365 | $tom
366 | ],
367 | 'pagination' => [
368 | 'route' => '/',
369 | 'pageSize' => 1,
370 | 'page' => 0
371 | ],
372 | ]),
373 | [
374 | 'data' => [
375 | $expectedBob
376 | ],
377 | 'meta' => [
378 | 'total-count' => 2,
379 | 'page-count' => 2,
380 | 'current-page' => 1,
381 | 'per-page' => 1
382 | ],
383 | 'links' => [
384 | 'self' => ['href' => '/index.php?r=&page=1&per-page=1'],
385 | 'next' => ['href' => '/index.php?r=&page=2&per-page=1'],
386 | 'last' => ['href' => '/index.php?r=&page=2&per-page=1']
387 | ]
388 | ]
389 | ],
390 | [
391 | new ArrayDataProvider([
392 | 'allModels' => [
393 | $bob,
394 | $tom
395 | ],
396 | 'pagination' => [
397 | 'route' => '/',
398 | 'pageSize' => 1,
399 | 'page' => 1
400 | ],
401 | ]),
402 | [
403 | 'data' => [
404 | $expectedTom
405 | ],
406 | 'meta' => [
407 | 'total-count' => 2,
408 | 'page-count' => 2,
409 | 'current-page' => 2,
410 | 'per-page' => 1
411 | ],
412 | 'links' => [
413 | 'self' => ['href' => '/index.php?r=&page=2&per-page=1'],
414 | 'first' => ['href' => '/index.php?r=&page=1&per-page=1'],
415 | 'prev' => ['href' => '/index.php?r=&page=1&per-page=1']
416 | ]
417 | ]
418 | ],
419 | [
420 | new ArrayDataProvider([
421 | 'allModels' => [
422 | $bob,
423 | $tom
424 | ],
425 | 'pagination' => false,
426 | ]),
427 | [
428 | 'data' => [
429 | $expectedBob,
430 | $expectedTom
431 | ]
432 | ]
433 | ],
434 | ];
435 | }
436 | /**
437 | * @dataProvider dataProviderSerializeDataProvider
438 | *
439 | * @param \yii\data\DataProviderInterface $dataProvider
440 | * @param array $expectedResult
441 | */
442 | public function testSerializeDataProvider($dataProvider, $expectedResult)
443 | {
444 | $serializer = new Serializer();
445 | ResourceModel::$extraFields = ['extraField1'];
446 | ResourceModel::$fields = ['username'];
447 | $this->assertEquals($expectedResult, $serializer->serialize($dataProvider));
448 | }
449 |
450 | public function testFieldSets()
451 | {
452 | $serializer = new Serializer();
453 | ResourceModel::$id = 123;
454 | ResourceModel::$fields = ['field1', 'first_name', 'field2'];
455 | $model = new ResourceModel();
456 | $expectedModel = [
457 | 'data' => [
458 | 'id' => '123',
459 | 'type' => 'resource-models',
460 | 'attributes' => [
461 | 'first-name' => 'Bob',
462 | ],
463 | 'links' => [
464 | 'self' => ['href' => 'http://example.com/resource/123']
465 | ]
466 | ]
467 | ];
468 | \Yii::$app->request->setQueryParams(['fields' => ['resource-models' => 'first-name']]);
469 | $this->assertSame($expectedModel, $serializer->serialize($model));
470 | $serializer->pluralize = false;
471 | \Yii::$app->request->setQueryParams(['fields' => ['resource-model' => 'first-name']]);
472 | $expectedModel['data']['type'] = 'resource-model';
473 | $this->assertSame($expectedModel, $serializer->serialize($model));
474 | }
475 |
476 | public function testTypeInflection()
477 | {
478 | $serializer = new Serializer();
479 | $serializer->pluralize = false;
480 | $model = new ResourceModel();
481 | ResourceModel::$fields = [];
482 | $this->assertSame([
483 | 'data' => [
484 | 'id' => '123',
485 | 'type' => 'resource-model',
486 | 'attributes' => [],
487 | 'links' => [
488 | 'self' => ['href' => 'http://example.com/resource/123']
489 | ]
490 | ]
491 | ], $serializer->serialize($model));
492 | }
493 | }
494 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 | namespace tuyakhov\jsonapi\tests;
6 |
7 | use \yii\helpers\ArrayHelper;
8 | use yii\web\Response;
9 |
10 | class TestCase extends \PHPUnit_Framework_TestCase
11 | {
12 | protected function setUp()
13 | {
14 | parent::setUp();
15 | $this->mockApplication();
16 | }
17 |
18 | protected function tearDown()
19 | {
20 | $this->destroyApplication();
21 | }
22 | /**
23 | * Populates Yii::$app with a new application
24 | * The application will be destroyed on tearDown() automatically.
25 | * @param array $config The application configuration, if needed
26 | * @param string $appClass name of the application class to create
27 | */
28 | protected function mockApplication($config = [], $appClass = '\yii\web\Application')
29 | {
30 | new $appClass(ArrayHelper::merge([
31 | 'id' => 'testapp',
32 | 'basePath' => __DIR__,
33 | 'components' => [
34 | 'request' => [
35 | 'parsers' => [
36 | 'application/vnd.api+json' => '\tuyakhov\jsonapi\JsonApiParser'
37 | ],
38 | 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq',
39 | 'scriptFile' => __DIR__ .'/index.php',
40 | 'scriptUrl' => '/index.php',
41 | ],
42 | 'response' => [
43 | 'format' => Response::FORMAT_JSON,
44 | 'formatters' => [
45 | Response::FORMAT_JSON => 'tuyakhov\jsonapi\JsonApiResponseFormatter'
46 | ]
47 | ]
48 | ],
49 | 'vendorPath' => $this->getVendorPath(),
50 | ], $config));
51 | }
52 | /**
53 | * @return string vendor path
54 | */
55 | protected function getVendorPath()
56 | {
57 | return dirname(dirname(__DIR__)) . '/vendor';
58 | }
59 | /**
60 | * Destroys application in Yii::$app by setting it to null.
61 | */
62 | protected function destroyApplication()
63 | {
64 | \Yii::$app = null;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/actions/CreateActionTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\tests\actions;
7 |
8 |
9 | use tuyakhov\jsonapi\actions\CreateAction;
10 | use tuyakhov\jsonapi\tests\data\ActiveQuery;
11 | use tuyakhov\jsonapi\tests\data\ResourceModel;
12 | use tuyakhov\jsonapi\tests\TestCase;
13 | use yii\base\Controller;
14 |
15 | class CreateActionTest extends TestCase
16 | {
17 | public function testSuccess()
18 | {
19 | ResourceModel::$extraFields = ['extraField1'];
20 | ResourceModel::$related = [
21 | 'extraField1' => new ActiveQuery(ResourceModel::className())
22 | ];
23 |
24 | \Yii::$app->controller = new Controller('test', \Yii::$app);
25 | $action = new CreateAction('test', \Yii::$app->controller, [
26 | 'modelClass' => ResourceModel::className(),
27 | ]);
28 |
29 | ResourceModel::$id = 124;
30 | ActiveQuery::$models = new ResourceModel();
31 | \Yii::$app->request->setBodyParams([
32 | 'ResourceModel' => [
33 | 'field1' => 'test',
34 | 'field2' => 2,
35 | ],
36 | 'relationships' => [
37 | 'extraField1' => [
38 | 'ResourceModel' => [
39 | 'id' => 124
40 | ],
41 | ]
42 | ]
43 | ]);
44 |
45 | $this->assertInstanceOf(ResourceModel::className(), $model = $action->run());
46 | $this->assertFalse($model->hasErrors());
47 | $relationships = $model->getResourceRelationships(['extra_field1']);
48 | $this->assertArrayHasKey('extra_field1', $relationships);
49 | $this->assertInstanceOf(ResourceModel::className(), $relationships['extra_field1']);
50 | $this->assertEquals(124, $relationships['extra_field1']->id);
51 | }
52 | }
--------------------------------------------------------------------------------
/tests/actions/DeleteActionTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\tests\actions;
7 |
8 | use tuyakhov\jsonapi\actions\DeleteAction;
9 | use tuyakhov\jsonapi\tests\data\ActiveQuery;
10 | use tuyakhov\jsonapi\tests\data\ResourceModel;
11 | use tuyakhov\jsonapi\tests\TestCase;
12 | use yii\web\Controller;
13 |
14 | class DeleteActionTest extends TestCase
15 | {
16 | public function testSuccess()
17 | {
18 | \Yii::$app->controller = new Controller('test', \Yii::$app);
19 | $action = new DeleteAction('test', \Yii::$app->controller, [
20 | 'modelClass' => ResourceModel::className(),
21 | ]);
22 |
23 | ResourceModel::$id = 124;
24 | ActiveQuery::$models = new ResourceModel();
25 |
26 | $action->run(124);
27 | $this->assertTrue(\Yii::$app->getResponse()->getIsEmpty());
28 | }
29 | }
--------------------------------------------------------------------------------
/tests/actions/DeleteRelationshipActionTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\tests\actions;
7 |
8 | use tuyakhov\jsonapi\actions\DeleteRelationshipAction;
9 | use tuyakhov\jsonapi\tests\data\ActiveQuery;
10 | use tuyakhov\jsonapi\tests\data\ResourceModel;
11 | use tuyakhov\jsonapi\tests\TestCase;
12 | use yii\data\ActiveDataProvider;
13 | use yii\web\Controller;
14 | use yii\web\ForbiddenHttpException;
15 |
16 | class DeleteRelationshipActionTest extends TestCase
17 | {
18 | public function testSuccess()
19 | {
20 | $model = new ResourceModel();
21 | $action = new DeleteRelationshipAction('test', new Controller('test', \Yii::$app), [
22 | 'modelClass' => ResourceModel::className()
23 | ]);
24 | ResourceModel::$related = [
25 | 'extraField1' => new ActiveQuery(ResourceModel::className(), ['multiple' => true]),
26 | 'extraField2' => new ActiveQuery(ResourceModel::className())
27 | ];
28 | $action->findModel = function ($id, $action) use($model) {
29 | return $model;
30 | };
31 | $model->extraField1 = [new ResourceModel()];
32 | \Yii::$app->request->setBodyParams(['ResourceModel' => ['type' => 'resource-models', 'id' => 123]]);
33 | $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run(1, 'extraField1'));
34 | $this->expectException(ForbiddenHttpException::class);
35 | $this->assertInstanceOf(ResourceModel::className(), $action->run(1, 'extraField2'));
36 | }
37 | }
--------------------------------------------------------------------------------
/tests/actions/IndexActionTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\tests\actions;
7 |
8 |
9 | use tuyakhov\jsonapi\actions\IndexAction;
10 | use tuyakhov\jsonapi\tests\data\ActiveQuery;
11 | use tuyakhov\jsonapi\tests\data\ResourceModel;
12 | use tuyakhov\jsonapi\tests\TestCase;
13 | use yii\base\Controller;
14 | use yii\data\ActiveDataFilter;
15 | use yii\data\ActiveDataProvider;
16 | use yii\db\Query;
17 |
18 | class IndexActionTest extends TestCase
19 | {
20 | public function testSuccess()
21 | {
22 | $action = new IndexAction('test', new Controller('test', \Yii::$app), [
23 | 'modelClass' => ResourceModel::className(),
24 | 'dataFilter' => [
25 | 'class' => ActiveDataFilter::className(),
26 | 'searchModel' => ResourceModel::className()
27 | ]
28 | ]);
29 | $filter = [
30 | 'filter' => ['field1' => 'test,qwe'],
31 | 'sort' => 'field1,-field2'
32 | ];
33 | \Yii::$app->getRequest()->setQueryParams($filter);
34 |
35 | $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run());
36 | $this->assertInstanceOf(Query::className(), $dataProvider->query);
37 | $this->assertSame([
38 | 'IN',
39 | 'field1',
40 | ['test', 'qwe']
41 | ], $dataProvider->query->where);
42 | $this->assertSame(['field1' => SORT_ASC, 'field2' => SORT_DESC], $dataProvider->getSort()->orders);
43 | }
44 |
45 | public function testValidation()
46 | {
47 | $action = new IndexAction('test', new Controller('test', \Yii::$app), [
48 | 'modelClass' => ResourceModel::className(),
49 | 'dataFilter' => [
50 | 'class' => ActiveDataFilter::className(),
51 | 'searchModel' => ResourceModel::className()
52 | ]
53 | ]);
54 | \Yii::$app->getRequest()->setQueryParams(['filter' => ['field1' => 1]]);
55 |
56 | $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run());
57 | $this->assertInstanceOf(Query::className(), $dataProvider->query);
58 | $this->assertNull($dataProvider->query->where);
59 | }
60 |
61 | public function testPagination()
62 | {
63 | $action = new IndexAction('test', new Controller('test', \Yii::$app), [
64 | 'modelClass' => ResourceModel::className(),
65 | ]);
66 | ActiveQuery::$models = [new ResourceModel(), new ResourceModel()];
67 | $params = ['page' => 1];
68 | \Yii::$app->getRequest()->setQueryParams($params);
69 |
70 | $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run());
71 | $this->assertSame(0, $dataProvider->getPagination()->page);
72 |
73 | $params = ['page' => ['number' => 2, 'size' => 1]];
74 | \Yii::$app->getRequest()->setQueryParams($params);
75 |
76 | $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run());
77 |
78 | $this->assertSame(2, $dataProvider->getCount());
79 | $this->assertSame(2, $dataProvider->pagination->getPageCount());
80 | $this->assertSame(1, $dataProvider->pagination->getPageSize());
81 | $this->assertSame(1, $dataProvider->pagination->getOffset());
82 |
83 | // test invalid value
84 | $params = ['page' => 1];
85 | \Yii::$app->getRequest()->setQueryParams($params);
86 |
87 | $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run());
88 |
89 | $this->assertSame(2, $dataProvider->getCount());
90 | $this->assertSame(1, $dataProvider->pagination->getPageCount());
91 | $this->assertSame($dataProvider->pagination->defaultPageSize, $dataProvider->pagination->getPageSize());
92 | $this->assertSame(0, $dataProvider->pagination->getOffset());
93 | }
94 | }
--------------------------------------------------------------------------------
/tests/actions/UpdateActionTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\tests\actions;
7 |
8 | use tuyakhov\jsonapi\actions\UpdateAction;
9 | use tuyakhov\jsonapi\tests\data\ActiveQuery;
10 | use tuyakhov\jsonapi\tests\data\ResourceModel;
11 | use tuyakhov\jsonapi\tests\TestCase;
12 | use yii\base\Controller;
13 |
14 | class UpdateActionTest extends TestCase
15 | {
16 | public function testSuccess()
17 | {
18 | ResourceModel::$extraFields = ['extraField1'];
19 | ResourceModel::$related = [
20 | 'extraField1' => new ActiveQuery(ResourceModel::className())
21 | ];
22 |
23 | \Yii::$app->controller = new Controller('test', \Yii::$app);
24 | $action = new UpdateAction('test', \Yii::$app->controller, [
25 | 'modelClass' => ResourceModel::className(),
26 | ]);
27 |
28 | ResourceModel::$id = 124;
29 | $model = new ResourceModel();
30 | $model->field1 = '41231';
31 | ActiveQuery::$models = $model;
32 | \Yii::$app->request->setBodyParams([
33 | 'ResourceModel' => [
34 | 'field1' => 'test',
35 | 'field2' => 2,
36 | ],
37 | 'relationships' => [
38 | 'extraField1' => [
39 | 'ResourceModel' => [
40 | 'id' => 124
41 | ],
42 | ]
43 | ]
44 | ]);
45 |
46 | $this->assertInstanceOf(ResourceModel::className(), $model = $action->run(1));
47 | $this->assertFalse($model->hasErrors());
48 | $this->assertEquals('test', $model->field1);
49 | $relationships = $model->getResourceRelationships(['extra_field1']);
50 | $this->assertArrayHasKey('extra_field1', $relationships);
51 | $this->assertInstanceOf(ResourceModel::className(), $relationships['extra_field1']);
52 | $this->assertEquals(124, $relationships['extra_field1']->id);
53 | }
54 | }
--------------------------------------------------------------------------------
/tests/actions/UpdateRelationshipActionTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\tests\actions;
7 |
8 |
9 | use tuyakhov\jsonapi\actions\UpdateRelationshipAction;
10 | use tuyakhov\jsonapi\tests\data\ActiveQuery;
11 | use tuyakhov\jsonapi\tests\data\ResourceModel;
12 | use tuyakhov\jsonapi\tests\TestCase;
13 | use yii\base\Controller;
14 | use yii\data\ActiveDataProvider;
15 |
16 | class UpdateRelationshipActionTest extends TestCase
17 | {
18 | public function testSuccess()
19 | {
20 | $model = new ResourceModel();
21 | $action = new UpdateRelationshipAction('test', new Controller('test', \Yii::$app), [
22 | 'modelClass' => ResourceModel::className()
23 | ]);
24 | ResourceModel::$related = [
25 | 'extraField1' => new ActiveQuery(ResourceModel::className(), ['multiple' => true]),
26 | 'extraField2' => new ActiveQuery(ResourceModel::className())
27 | ];
28 | $action->findModel = function ($id, $action) use($model) {
29 | return $model;
30 | };
31 | ActiveQuery::$models = [new ResourceModel(), new ResourceModel()];
32 | \Yii::$app->request->setBodyParams(['ResourceModel' => ['type' => 'resource-models', 'id' => 123]]);
33 | $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run(1, 'extraField1'));
34 | $this->assertEquals(2, count($dataProvider->getModels()));
35 | $this->assertInstanceOf(ResourceModel::className(), $action->run(1, 'extraField2'));
36 | }
37 | }
--------------------------------------------------------------------------------
/tests/actions/ViewRelatedActionTest.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 | namespace tuyakhov\jsonapi\tests\actions;
6 |
7 | use tuyakhov\jsonapi\tests\TestCase;
8 | use tuyakhov\jsonapi\actions\ViewRelatedAction;
9 | use yii\base\Controller;
10 | use tuyakhov\jsonapi\tests\data\ResourceModel;
11 | use yii\data\ActiveDataProvider;
12 | use \tuyakhov\jsonapi\tests\data\ActiveQuery;
13 | use yii\web\NotFoundHttpException;
14 |
15 | class ViewRelatedActionTest extends TestCase
16 | {
17 |
18 | public function testSuccess()
19 | {
20 | $model = new ResourceModel();
21 | $action = new ViewRelatedAction('test', new Controller('test', \Yii::$app), [
22 | 'modelClass' => ResourceModel::className()
23 | ]);
24 | ResourceModel::$related = [
25 | 'extraField1' => new ActiveQuery(ResourceModel::className(), ['multiple' => true]),
26 | 'extraField2' => new ActiveQuery(ResourceModel::className())
27 | ];
28 | $action->findModel = function ($id, $action) use($model) {
29 | return $model;
30 | };
31 |
32 | $this->assertInstanceOf(ActiveDataProvider::className(), $action->run(1, 'extraField1'));
33 | $this->assertInstanceOf(ResourceModel::className(), $action->run(1, 'extraField2'));
34 | }
35 |
36 | public function testInvalidRelation()
37 | {
38 | $action = new ViewRelatedAction('test', new Controller('test', \Yii::$app), [
39 | 'modelClass' => ResourceModel::className()
40 | ]);
41 | $action->findModel = function ($id, $action) {
42 | return new ResourceModel();
43 | };
44 | $this->expectException(NotFoundHttpException::class);
45 | $action->run(1, 'invalid');
46 | }
47 | }
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 | namespace tuyakhov\jsonapi\tests\data;
6 |
7 |
8 | class ActiveQuery extends \yii\db\ActiveQuery
9 | {
10 | public static $models = [];
11 |
12 | public function one($db = null)
13 | {
14 | return isset(self::$models[0]) ? self::$models[0] : new $this->modelClass;
15 | }
16 |
17 | public function all($db = null)
18 | {
19 | return self::$models;
20 | }
21 |
22 | public function count($q = '*', $db = null)
23 | {
24 | return count(self::$models);
25 | }
26 | }
--------------------------------------------------------------------------------
/tests/data/ResourceModel.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace tuyakhov\jsonapi\tests\data;
7 |
8 | use tuyakhov\jsonapi\LinksInterface;
9 | use tuyakhov\jsonapi\ResourceInterface;
10 | use tuyakhov\jsonapi\ResourceTrait;
11 | use yii\base\Model;
12 | use yii\helpers\Url;
13 | use yii\web\Link;
14 |
15 | class ResourceModel extends Model implements ResourceInterface, LinksInterface
16 | {
17 | use ResourceTrait;
18 |
19 | public static $id = '123';
20 | public static $fields = ['field1', 'field2'];
21 | public static $extraFields = [];
22 | public static $related = [];
23 | public $field1 = 'test';
24 | public $field2 = 2;
25 | public $first_name = 'Bob';
26 | public $username = '';
27 | public $extraField1 = 'testExtra';
28 | public $extraField2 = 42;
29 | private $_id;
30 |
31 | public function rules()
32 | {
33 | return [
34 | ['field1', 'string']
35 | ];
36 | }
37 |
38 | public function getId()
39 | {
40 | if ($this->_id === null) {
41 | $this->_id = static::$id;
42 | }
43 | return $this->_id;
44 | }
45 |
46 | public function setId($value)
47 | {
48 | $this->_id = $value;
49 | }
50 |
51 | public function fields()
52 | {
53 | return static::$fields;
54 | }
55 |
56 | public function extraFields()
57 | {
58 | return static::$extraFields;
59 | }
60 |
61 | public function getRelation($name)
62 | {
63 | return isset(static::$related[$name]) ? static::$related[$name] : null;
64 | }
65 |
66 | public function setResourceRelationship($name, $relationship)
67 | {
68 | $this->$name = $relationship;
69 | }
70 |
71 | public static function find()
72 | {
73 | return new ActiveQuery(self::className());
74 | }
75 |
76 | public static function findOne()
77 | {
78 | return self::find()->one();
79 | }
80 |
81 | public static function primaryKey()
82 | {
83 | return ['id'];
84 | }
85 |
86 | public function getPrimaryKey($asArray = false)
87 | {
88 | return $asArray ? [$this->getId()] : $this->getId();
89 | }
90 |
91 | public function unlinkAll($name, $delete = false)
92 | {
93 | $this->$name = null;
94 | }
95 |
96 | public function unlink($name, $model)
97 | {
98 | array_pop($this->$name);
99 | }
100 |
101 | public function save()
102 | {
103 | return true;
104 | }
105 |
106 | public function delete()
107 | {
108 | return true;
109 | }
110 |
111 | public function getLinks()
112 | {
113 | return [
114 | Link::REL_SELF => Url::to('http://example.com/resource/' . $this->getId())
115 | ];
116 | }
117 | }
118 |
--------------------------------------------------------------------------------