diff --git a/.gitignore b/.gitignore index 0c46198..04167d5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ Thumbs.db # composer itself is not needed composer.phar +composer.lock # Mac DS_Store Files .DS_Store diff --git a/README.md b/README.md index 0bec2c0..26ca03c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ +<p align="center"><img width="30%" src ="https://jsonapi.org/images/jsonapi.png" /></p> Implementation of JSON API specification for the Yii framework ================================================================== [](https://packagist.org/packages/tuyakhov/yii2-json-api) [](https://scrutinizer-ci.com/g/tuyakhov/yii2-json-api/?branch=master) [](https://scrutinizer-ci.com/g/tuyakhov/yii2-json-api/build-status/master) [](https://packagist.org/packages/tuyakhov/yii2-json-api) + Installation ------------ @@ -166,6 +168,12 @@ As the result: } } ``` +Pagination +--------------------------- +The `page` query parameter family is reserved for pagination. +This library implements a page-based strategy and allows the usage of query parameters such as `page[number]` and `page[size]` +Example: `http://yourdomain.com/users?page[number]=3&page[size]=10` + Enabling JSON API Input --------------------------- 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 @@ -208,3 +216,117 @@ $model->load(\Yii::$app->request->post()); By default type `users` will be converted into `User` (singular, camelCase) which corresponds to the model's `formName()` method (which you may override). You can override the `JsonApiParser::formNameCallback` property which refers to a callback that converts 'type' member to form name. 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. + +Examples +-------- +Controller: +```php +class UserController extends \yii\rest\Controller +{ + public $serializer = 'tuyakhov\jsonapi\Serializer'; + + /** + * @inheritdoc + */ + public function behaviors() + { + return ArrayHelper::merge(parent::behaviors(), [ + 'contentNegotiator' => [ + 'class' => ContentNegotiator::className(), + 'formats' => [ + 'application/vnd.api+json' => Response::FORMAT_JSON, + ], + ] + ]); + } + + /** + * @inheritdoc + */ + public function actions() + { + return [ + 'create' => [ + 'class' => 'tuyakhov\jsonapi\actions\CreateAction', + 'modelClass' => ExampleModel::className() + ], + 'update' => [ + 'class' => 'tuyakhov\jsonapi\actions\UpdateAction', + 'modelClass' => ExampleModel::className() + ], + 'view' => [ + 'class' => 'tuyakhov\jsonapi\actions\ViewAction', + 'modelClass' => ExampleModel::className(), + ], + 'delete' => [ + 'class' => 'tuyakhov\jsonapi\actions\DeleteAction', + 'modelClass' => ExampleModel::className(), + ], + 'view-related' => [ + 'class' => 'tuyakhov\jsonapi\actions\ViewRelatedAction', + 'modelClass' => ExampleModel::className() + ], + 'update-relationship' => [ + 'class' => 'tuyakhov\jsonapi\actions\UpdateRelationshipAction', + 'modelClass' => ExampleModel::className() + ], + 'delete-relationship' => [ + 'class' => 'tuyakhov\jsonapi\actions\DeleteRelationshipAction', + 'modelClass' => ExampleModel::className() + ], + 'options' => [ + 'class' => 'yii\rest\OptionsAction', + ], + ]; + } +} + +``` + +Model: +```php +class User extends ActiveRecord implements LinksInterface, ResourceInterface +{ + use ResourceTrait; + + public function getLinks() + { + $reflect = new \ReflectionClass($this); + $controller = Inflector::camel2id($reflect->getShortName()); + return [ + Link::REL_SELF => Url::to(["$controller/view", 'id' => $this->getId()], true) + ]; + } +} +``` + +Configuration file `config/main.php`: +```php +return [ + // ... + 'components' => [ + 'request' => [ + 'parsers' => [ + 'application/vnd.api+json' => 'tuyakhov\jsonapi\JsonApiParser', + ] + ], + 'response' => [ + 'format' => \yii\web\Response::FORMAT_JSON, + 'formatters' => [ + \yii\web\Response::FORMAT_JSON => 'tuyakhov\jsonapi\JsonApiResponseFormatter' + ] + ], + 'urlManager' => [ + 'rules' => [ + [ + 'class' => 'tuyakhov\jsonapi\UrlRule', + 'controller' => ['user'], + ], + + ] + ] + // ... + ] + // ... +] +``` diff --git a/composer.json b/composer.json index 7b78444..76072ec 100644 --- a/composer.json +++ b/composer.json @@ -17,14 +17,18 @@ } ], "require": { - "yiisoft/yii2": "^2.0.10" + "yiisoft/yii2": "^2.0.13" }, "require-dev": { "phpunit/phpunit": "5.5.*" }, "autoload": { "psr-4": { - "tuyakhov\\jsonapi\\": "src/", + "tuyakhov\\jsonapi\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { "tuyakhov\\jsonapi\\tests\\": "tests/" } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 042c2f6..0000000 --- a/composer.lock +++ /dev/null @@ -1,1706 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "hash": "90fe276d27f5f93d6bc833a682829636", - "content-hash": "668e9cd59b0ef219b1eec0e83f18bb36", - "packages": [ - { - "name": "bower-asset/jquery", - "version": "2.2.4", - "source": { - "type": "git", - "url": "https://github.com/jquery/jquery-dist.git", - "reference": "c0185ab7c75aab88762c5aae780b9d83b80eda72" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/c0185ab7c75aab88762c5aae780b9d83b80eda72", - "reference": "c0185ab7c75aab88762c5aae780b9d83b80eda72", - "shasum": "" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "dist/jquery.js", - "bower-asset-ignore": [ - "package.json" - ] - }, - "license": [ - "MIT" - ], - "keywords": [ - "browser", - "javascript", - "jquery", - "library" - ] - }, - { - "name": "bower-asset/jquery.inputmask", - "version": "3.2.7", - "source": { - "type": "git", - "url": "https://github.com/RobinHerbots/jquery.inputmask.git", - "reference": "5a72c563b502b8e05958a524cdfffafe9987be38" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/5a72c563b502b8e05958a524cdfffafe9987be38", - "reference": "5a72c563b502b8e05958a524cdfffafe9987be38", - "shasum": "" - }, - "require": { - "bower-asset/jquery": ">=1.7" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": [ - "./dist/inputmask/inputmask.js" - ], - "bower-asset-ignore": [ - "**/*", - "!dist/*", - "!dist/inputmask/*", - "!dist/min/*", - "!dist/min/inputmask/*", - "!extra/bindings/*", - "!extra/dependencyLibs/*", - "!extra/phone-codes/*" - ] - }, - "license": [ - "http://opensource.org/licenses/mit-license.php" - ], - "description": "jquery.inputmask is a jquery plugin which create an input mask.", - "keywords": [ - "form", - "input", - "inputmask", - "jquery", - "mask", - "plugins" - ] - }, - { - "name": "bower-asset/punycode", - "version": "v1.3.2", - "source": { - "type": "git", - "url": "https://github.com/bestiejs/punycode.js.git", - "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", - "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", - "shasum": "" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "punycode.js", - "bower-asset-ignore": [ - "coverage", - "tests", - ".*", - "component.json", - "Gruntfile.js", - "node_modules", - "package.json" - ] - } - }, - { - "name": "bower-asset/yii2-pjax", - "version": "v2.0.6", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/jquery-pjax.git", - "reference": "60728da6ade5879e807a49ce59ef9a72039b8978" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/60728da6ade5879e807a49ce59ef9a72039b8978", - "reference": "60728da6ade5879e807a49ce59ef9a72039b8978", - "shasum": "" - }, - "require": { - "bower-asset/jquery": ">=1.8" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "./jquery.pjax.js", - "bower-asset-ignore": [ - ".travis.yml", - "Gemfile", - "Gemfile.lock", - "CONTRIBUTING.md", - "vendor/", - "script/", - "test/" - ] - }, - "license": [ - "MIT" - ] - }, - { - "name": "cebe/markdown", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/cebe/markdown.git", - "reference": "c30eb5e01fe021cc5bba2f9ee0eeef96d4931166" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cebe/markdown/zipball/c30eb5e01fe021cc5bba2f9ee0eeef96d4931166", - "reference": "c30eb5e01fe021cc5bba2f9ee0eeef96d4931166", - "shasum": "" - }, - "require": { - "lib-pcre": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "cebe/indent": "*", - "facebook/xhprof": "*@dev", - "phpunit/phpunit": "4.1.*" - }, - "bin": [ - "bin/markdown" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "cebe\\markdown\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "http://cebe.cc/", - "role": "Creator" - } - ], - "description": "A super fast, highly extensible markdown parser for PHP", - "homepage": "https://github.com/cebe/markdown#readme", - "keywords": [ - "extensible", - "fast", - "gfm", - "markdown", - "markdown-extra" - ], - "time": "2016-09-14 20:40:20" - }, - { - "name": "ezyang/htmlpurifier", - "version": "v4.8.0", - "source": { - "type": "git", - "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2", - "reference": "d0c392f77d2f2a3dcf7fcb79e2a1e2b8804e75b2", - "shasum": "" - }, - "require": { - "php": ">=5.2" - }, - "type": "library", - "autoload": { - "psr-0": { - "HTMLPurifier": "library/" - }, - "files": [ - "library/HTMLPurifier.composer.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL" - ], - "authors": [ - { - "name": "Edward Z. Yang", - "email": "admin@htmlpurifier.org", - "homepage": "http://ezyang.com" - } - ], - "description": "Standards compliant HTML filter written in PHP", - "homepage": "http://htmlpurifier.org/", - "keywords": [ - "html" - ], - "time": "2016-07-16 12:58:58" - }, - { - "name": "yiisoft/yii2", - "version": "2.0.10", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "5bfcb7a6dfa9771e2248eb8c4448613330f343ff" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/5bfcb7a6dfa9771e2248eb8c4448613330f343ff", - "reference": "5bfcb7a6dfa9771e2248eb8c4448613330f343ff", - "shasum": "" - }, - "require": { - "bower-asset/jquery": "2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", - "bower-asset/jquery.inputmask": "~3.2.2", - "bower-asset/punycode": "1.3.*", - "bower-asset/yii2-pjax": "~2.0.1", - "cebe/markdown": "~1.0.0 | ~1.1.0", - "ext-ctype": "*", - "ext-mbstring": "*", - "ezyang/htmlpurifier": "~4.6", - "lib-pcre": "*", - "php": ">=5.4.0", - "yiisoft/yii2-composer": "~2.0.4" - }, - "bin": [ - "yii" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "yii\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com", - "homepage": "http://www.yiiframework.com/", - "role": "Founder and project lead" - }, - { - "name": "Alexander Makarov", - "email": "sam@rmcreative.ru", - "homepage": "http://rmcreative.ru/", - "role": "Core framework development" - }, - { - "name": "Maurizio Domba", - "homepage": "http://mdomba.info/", - "role": "Core framework development" - }, - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "http://cebe.cc/", - "role": "Core framework development" - }, - { - "name": "Timur Ruziev", - "email": "resurtm@gmail.com", - "homepage": "http://resurtm.com/", - "role": "Core framework development" - }, - { - "name": "Paul Klimov", - "email": "klimov.paul@gmail.com", - "role": "Core framework development" - }, - { - "name": "Dmitry Naumenko", - "email": "d.naumenko.a@gmail.com", - "role": "Core framework development" - } - ], - "description": "Yii PHP Framework Version 2", - "homepage": "http://www.yiiframework.com/", - "keywords": [ - "framework", - "yii2" - ], - "time": "2016-10-20 12:02:50" - }, - { - "name": "yiisoft/yii2-composer", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "7452fd908a5023b8bb5ea1b123a174ca080de464" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/7452fd908a5023b8bb5ea1b123a174ca080de464", - "reference": "7452fd908a5023b8bb5ea1b123a174ca080de464", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "yii\\composer\\Plugin", - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "yii\\composer\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com" - } - ], - "description": "The composer plugin for Yii extension installer", - "keywords": [ - "composer", - "extension installer", - "yii2" - ], - "time": "2016-02-06 00:49:24" - } - ], - "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", - "shasum": "" - }, - "require": { - "php": ">=5.3,<8.0-DEV" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2015-06-14 21:17:01" - }, - { - "name": "myclabs/deep-copy", - "version": "1.5.5", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/399c1f9781e222f6eb6cc238796f5200d1b7f108", - "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2016-10-31 17:19:45" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2015-12-27 11:43:31" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.2.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30 07:12:33" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.2", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", - "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2016-06-10 07:14:17" - }, - { - "name": "phpspec/prophecy", - "version": "v1.6.1", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "58a8137754bc24b25740d4281399a4a3596058e0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", - "reference": "58a8137754bc24b25740d4281399a4a3596058e0", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", - "sebastian/comparator": "^1.1", - "sebastian/recursion-context": "^1.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2016-06-07 08:13:47" - }, - { - "name": "phpunit/php-code-coverage", - "version": "4.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6cba06ff75a1a63a71033e1a01b89056f3af1e8d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6cba06ff75a1a63a71033e1a01b89056f3af1e8d", - "reference": "6cba06ff75a1a63a71033e1a01b89056f3af1e8d", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "^1.4.2", - "sebastian/code-unit-reverse-lookup": "~1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "~1.0|~2.0" - }, - "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "^5.4" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.4.0", - "ext-xmlwriter": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2016-11-01 05:06:24" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2015-06-21 13:08:43" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21 13:50:34" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4|~5" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2016-05-12 18:03:57" - }, - { - "name": "phpunit/php-token-stream", - "version": "1.4.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", - "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2015-09-15 10:49:45" - }, - { - "name": "phpunit/phpunit", - "version": "5.5.7", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3f67cee782c9abfaee5e32fd2f57cdd54bc257ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3f67cee782c9abfaee5e32fd2f57cdd54bc257ba", - "reference": "3f67cee782c9abfaee5e32fd2f57cdd54bc257ba", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "^4.0.1", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "~1.1", - "sebastian/diff": "~1.2", - "sebastian/environment": "^1.3 || ^2.0", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/object-enumerator": "~1.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0|~2.0", - "symfony/yaml": "~2.1|~3.0" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-tidy": "*", - "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.5.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2016-10-03 13:04:15" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "3.4.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "238d7a2723bce689c79eeac9c7d5e1d623bb9dc2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/238d7a2723bce689c79eeac9c7d5e1d623bb9dc2", - "reference": "238d7a2723bce689c79eeac9c7d5e1d623bb9dc2", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2016-10-09 07:01:45" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2016-02-13 06:45:14" - }, - { - "name": "sebastian/comparator", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", - "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2015-07-26 15:48:44" - }, - { - "name": "sebastian/diff", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2015-12-08 07:14:41" - }, - { - "name": "sebastian/environment", - "version": "1.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2016-08-18 05:49:44" - }, - { - "name": "sebastian/exporter", - "version": "1.2.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2016-06-17 09:04:28" - }, - { - "name": "sebastian/global-state", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2015-10-12 03:26:01" - }, - { - "name": "sebastian/object-enumerator", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2016-01-28 13:25:10" - }, - { - "name": "sebastian/recursion-context", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "913401df809e99e4f47b27cdd781f4a258d58791" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", - "reference": "913401df809e99e4f47b27cdd781f4a258d58791", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2015-11-11 19:50:13" - }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28 20:34:47" - }, - { - "name": "sebastian/version", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", - "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-02-04 12:56:52" - }, - { - "name": "symfony/yaml", - "version": "v3.1.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/7ff51b06c6c3d5cc6686df69004a42c69df09e27", - "reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2016-10-24 18:41:13" - }, - { - "name": "webmozart/assert", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "bb2d123231c095735130cc8f6d31385a44c7b308" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308", - "reference": "bb2d123231c095735130cc8f6d31385a44c7b308", - "shasum": "" - }, - "require": { - "php": "^5.3.3|^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2016-08-09 15:02:57" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} diff --git a/src/Controller.php b/src/Controller.php new file mode 100644 index 0000000..7286eba --- /dev/null +++ b/src/Controller.php @@ -0,0 +1,32 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi; + + +use yii\filters\ContentNegotiator; +use yii\helpers\ArrayHelper; +use yii\web\Response; + +class Controller extends \yii\rest\Controller +{ + public $serializer = 'tuyakhov\jsonapi\Serializer'; + + /** + * @inheritdoc + */ + public function behaviors() + { + return ArrayHelper::merge(parent::behaviors(), [ + 'contentNegotiator' => [ + 'class' => ContentNegotiator::className(), + 'formats' => [ + 'application/vnd.api+json' => Response::FORMAT_JSON, + ], + ] + ]); + } + +} \ No newline at end of file diff --git a/src/Inflector.php b/src/Inflector.php index cb33a28..469c46c 100644 --- a/src/Inflector.php +++ b/src/Inflector.php @@ -30,7 +30,7 @@ public static function var2member($var) */ public static function member2var($member) { - return str_replace(' ', '_', preg_replace('/[^A-Za-z0-9]+/', ' ', $member)); + return str_replace(' ', '_', preg_replace('/[^A-Za-z0-9\.]+/', ' ', $member)); } /** diff --git a/src/JsonApiParser.php b/src/JsonApiParser.php index 730f976..6792165 100644 --- a/src/JsonApiParser.php +++ b/src/JsonApiParser.php @@ -34,13 +34,16 @@ class JsonApiParser extends JsonParser public function parse($rawBody, $contentType) { $array = parent::parse($rawBody, $contentType); - $data = ArrayHelper::getValue($array, 'data', []); - if (empty($data)) { + if (!empty($array) && !ArrayHelper::keyExists('data', $array)) { if ($this->throwException) { throw new BadRequestHttpException('The request MUST include a single resource object as primary data.'); } return []; } + $data = ArrayHelper::getValue($array, 'data', []); + if (empty($data)) { + return []; + } if (ArrayHelper::isAssociative($data)) { $result = $this->parseResource($data); @@ -109,12 +112,14 @@ protected function parseRelationships(array $relObjects = []) { $relationships = []; foreach ($relObjects as $name => $relationship) { - if (!$relData = ArrayHelper::getValue($relationship, 'data')) { + if (!ArrayHelper::keyExists('data', $relationship)) { continue; } + $relData = ArrayHelper::getValue($relationship, 'data', []); if (!ArrayHelper::isIndexed($relData)) { $relData = [$relData]; } + $relationships[$name] = []; foreach ($relData as $identifier) { if (isset($identifier['type']) && isset($identifier['id'])) { $formName = $this->typeToFormName($identifier['type']); diff --git a/src/JsonApiResponseFormatter.php b/src/JsonApiResponseFormatter.php index 705a454..fb37fd7 100644 --- a/src/JsonApiResponseFormatter.php +++ b/src/JsonApiResponseFormatter.php @@ -8,11 +8,31 @@ use yii\base\Component; use yii\helpers\ArrayHelper; use yii\helpers\Json; +use yii\helpers\Url; +use yii\web\ErrorHandler; +use yii\web\Link; use yii\web\Response; use yii\web\ResponseFormatterInterface; class JsonApiResponseFormatter extends Component implements ResponseFormatterInterface { + /** + * Mapping between the error handler component and JSON API error object + * @see ErrorHandler::convertExceptionToArray() + */ + const ERROR_EXCEPTION_MAPPING = [ + 'title' => 'name', + 'detail' => 'message', + 'code' => 'code', + 'status' => 'status' + ]; + /** + * An error object MAY have the following members + * @link http://jsonapi.org/format/#error-objects + */ + const ERROR_ALLOWED_MEMBERS = [ + 'id', 'links', 'status', 'code', 'title', 'detail', 'source', 'meta' + ]; /** * @var integer the encoding options passed to [[Json::encode()]]. For more details please refer to * <http://www.php.net/manual/en/function.json-encode.php>. @@ -34,19 +54,41 @@ class JsonApiResponseFormatter extends Component implements ResponseFormatterInt public function format($response) { $response->getHeaders()->set('Content-Type', 'application/vnd.api+json; charset=UTF-8'); - if ($response->data !== null) { - $options = $this->encodeOptions; - if ($this->prettyPrint) { - $options |= JSON_PRETTY_PRINT; + $options = $this->encodeOptions; + if ($this->prettyPrint) { + $options |= JSON_PRETTY_PRINT; + } + + $apiDocument = $response->data; + + if (!$response->isEmpty && empty($apiDocument)) { + $apiDocument = ['data' => $response->data]; + if (\Yii::$app->controller) { + $apiDocument['links'] = Link::serialize([ + Link::REL_SELF => Url::current([], true) + ]); } - $apiDocument = $response->data; - if ($response->isClientError || $response->isServerError) { - if (ArrayHelper::isAssociative($response->data)) { - $response->data = [$response->data]; + } + + if ($response->isClientError || $response->isServerError) { + if (ArrayHelper::isAssociative($response->data)) { + $response->data = [$response->data]; + } + $formattedErrors = []; + foreach ($response->data as $error) { + $formattedError = array_intersect_key($error, array_flip(static::ERROR_ALLOWED_MEMBERS)); + foreach (static::ERROR_EXCEPTION_MAPPING as $member => $key) { + if (isset($error[$key])) { + $formattedError[$member] = (string) $error[$key]; + } + } + if (!empty($formattedError)) { + $formattedErrors[] = $formattedError; } - $apiDocument = ['errors' => $response->data]; } - + $apiDocument = ['errors' => $formattedErrors]; + } + if ($apiDocument !== null) { $response->content = Json::encode($apiDocument, $options); } } diff --git a/src/Pagination.php b/src/Pagination.php new file mode 100644 index 0000000..a277466 --- /dev/null +++ b/src/Pagination.php @@ -0,0 +1,37 @@ +<?php + + +namespace tuyakhov\jsonapi; + +use Yii; +use yii\web\Request; + +/** + * This class enables the `page` query parameter family. + * Query parameters such as page[number] and page[size] might be used. + * @link https://jsonapi.org/format/1.1/#fetching-pagination + */ +class Pagination extends \yii\data\Pagination +{ + /** @var string default page size parameter */ + public $pageSizeParam = 'size'; + + /** @var string default page number parameter */ + public $pageParam = 'number'; + + /** + * Support `page` query parameter family + */ + public function init() + { + if ($this->params === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof Request ? $request->getQueryParam('page') : []; + if (!is_array($params)) { + $params = []; + } + $this->params = $params; + } + } + +} \ No newline at end of file diff --git a/src/ResourceInterface.php b/src/ResourceInterface.php index 6afc163..aeb52fe 100644 --- a/src/ResourceInterface.php +++ b/src/ResourceInterface.php @@ -19,10 +19,11 @@ public function getResourceAttributes(array $fields = []); /** * The "relationships" member of the resource object describing relationships between the resource and other JSON API resources. + * @param array $linked specific resource linkage that a client has requested. * @return ResourceIdentifierInterface[] represent references from the resource object in which it’s defined to other resource objects. */ - public function getResourceRelationships(); + public function getResourceRelationships(array $linked = []); public function setResourceRelationship($name, $relationship); -} +} \ No newline at end of file diff --git a/src/ResourceTrait.php b/src/ResourceTrait.php index 8be89a7..df296e2 100644 --- a/src/ResourceTrait.php +++ b/src/ResourceTrait.php @@ -12,6 +12,12 @@ trait ResourceTrait { + /** + * @var bool a flag that enables/disables deleting of the model that contains the foreign key when setting relationships + * By default the model's foreign key will be set `null` and saved. + */ + protected $allowDeletingResources = false; + /** * @return string */ @@ -51,19 +57,26 @@ public function getResourceAttributes(array $fields = []) } /** + * @param array $linked * @return array */ - public function getResourceRelationships() + public function getResourceRelationships(array $linked = []) { - $relationships = []; $fields = []; if ($this instanceof Arrayable) { $fields = $this->extraFields(); } + $resolvedFields = $this->resolveFields($fields); + $keys = array_keys($resolvedFields); + + $relationships = array_fill_keys($keys, null); + $linkedFields = array_intersect($keys, $linked); - foreach ($this->resolveFields($fields) as $name => $definition) { + foreach ($linkedFields as $name) { + $definition = $resolvedFields[$name]; $relationships[$name] = is_string($definition) ? $this->$definition : call_user_func($definition, $this, $name); } + return $relationships; } @@ -80,6 +93,7 @@ public function setResourceRelationship($name, $relationship) if (!is_array($relationship)) { $relationship = [$relationship]; } + $this->unlinkAll($name, $this->allowDeletingResources); foreach ($relationship as $key => $value) { if ($value instanceof ActiveRecordInterface) { $this->link($name, $value); @@ -131,4 +145,20 @@ protected function resolveFields(array $fields, array $fieldSet = []) return $result; } + + /** + * @param $value boolean + */ + public function setAllowDeletingResources($value) + { + $this->allowDeletingResources = $value; + } + + /** + * @return bool + */ + public function getAllowDeletingResources() + { + return $this->allowDeletingResources; + } } diff --git a/src/Serializer.php b/src/Serializer.php index 3881173..481f79c 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -102,15 +102,23 @@ public function serialize($data) } /** + * @param array $included * @param ResourceInterface $model * @return array */ - protected function serializeModel(ResourceInterface $model) + protected function serializeModel(ResourceInterface $model, array $included = []) { $fields = $this->getRequestedFields(); $type = $this->pluralize ? Inflector::pluralize($model->getType()) : $model->getType(); $fields = isset($fields[$type]) ? $fields[$type] : []; + $topLevel = array_map(function($item) { + if (($pos = strrpos($item, '.')) !== false) { + return substr($item, 0, $pos); + } + return $item; + }, $included); + $attributes = $model->getResourceAttributes($fields); $attributes = array_combine($this->prepareMemberNames(array_keys($attributes)), array_values($attributes)); @@ -118,8 +126,7 @@ protected function serializeModel(ResourceInterface $model) 'attributes' => $attributes, ]); - $included = $this->getIncluded(); - $relationships = $model->getResourceRelationships(); + $relationships = $model->getResourceRelationships($topLevel); if (!empty($relationships)) { foreach ($relationships as $name => $items) { $relationship = []; @@ -132,17 +139,15 @@ protected function serializeModel(ResourceInterface $model) } elseif ($items instanceof ResourceIdentifierInterface) { $relationship = $this->serializeIdentifier($items); } + $memberName = $this->prepareMemberNames([$name]); + $memberName = reset($memberName); if (!empty($relationship)) { - $memberName = $this->prepareMemberNames([$name]); - $memberName = reset($memberName); - if (in_array($name, $included)) { - $data['relationships'][$memberName]['data'] = $relationship; - } - if ($model instanceof LinksInterface) { - $links = $model->getRelationshipLinks($memberName); - if (!empty($links)) { - $data['relationships'][$memberName]['links'] = Link::serialize($links); - } + $data['relationships'][$memberName]['data'] = $relationship; + } + if ($model instanceof LinksInterface) { + $links = $model->getRelationshipLinks($memberName); + if (!empty($links)) { + $data['relationships'][$memberName]['links'] = Link::serialize($links); } } } @@ -164,11 +169,14 @@ protected function serializeResource(ResourceInterface $resource) if ($this->request->getIsHead()) { return null; } else { - $data = ['data' => $this->serializeModel($resource)]; + $included = $this->getIncluded(); + $data = [ + 'data' => $this->serializeModel($resource, $included) + ]; - $included = $this->serializeIncluded($resource); - if (!empty($included)) { - $data['included'] = $included; + $relatedResources = $this->serializeIncluded($resource, $included); + if (!empty($relatedResources)) { + $data['included'] = $relatedResources; } return $data; @@ -200,42 +208,60 @@ protected function serializeIdentifier(ResourceIdentifierInterface $identifier) /** * @param ResourceInterface|array $resources + * @param array $included + * @param true $assoc * @return array */ - protected function serializeIncluded($resources) + protected function serializeIncluded($resources, array $included = [], $assoc = false) { - $included = $this->getIncluded(); $resources = is_array($resources) ? $resources : [$resources]; $data = []; + $inclusion = []; + foreach ($included as $path) { + if (($pos = strrpos($path, '.')) === false) { + $inclusion[$path] = []; + continue; + } + $name = substr($path, $pos + 1); + $key = substr($path, 0, $pos); + $inclusion[$key][] = $name; + } + foreach ($resources as $resource) { if (!$resource instanceof ResourceInterface) { continue; } - $relationships = $resource->getResourceRelationships(); + $relationships = $resource->getResourceRelationships(array_keys($inclusion)); foreach ($relationships as $name => $relationship) { - if (!in_array($name, $included)) { + if ($relationship === null) { continue; } if (!is_array($relationship)) { $relationship = [$relationship]; } foreach ($relationship as $model) { - if ($model instanceof ResourceInterface) { - $uniqueKey = $model->getType() . '/' . $model->getId(); - $data[$uniqueKey] = $this->serializeModel($model); + if (!$model instanceof ResourceInterface) { + continue; + } + $uniqueKey = $model->getType() . '/' . $model->getId(); + if (!isset($data[$uniqueKey])) { + $data[$uniqueKey] = $this->serializeModel($model, $inclusion[$name]); + } + if (!empty($inclusion[$name])) { + $data = array_merge($data, $this->serializeIncluded($model, $inclusion[$name], true)); } } } } - return array_values($data); + return $assoc ? $data : array_values($data); } /** * Serializes a data provider. * @param DataProviderInterface $dataProvider - * @return array the array representation of the data provider. + * @return null|array the array representation of the data provider. */ protected function serializeDataProvider($dataProvider) { @@ -245,17 +271,18 @@ protected function serializeDataProvider($dataProvider) $models = $dataProvider->getModels(); $data = []; + $included = $this->getIncluded(); foreach ($models as $model) { if ($model instanceof ResourceInterface) { - $data[] = $this->serializeModel($model); + $data[] = $this->serializeModel($model, $included); } } $result = ['data' => $data]; - $included = $this->serializeIncluded($models); - if (!empty($included)) { - $result['included'] = $included; + $relatedResources = $this->serializeIncluded($models, $included); + if (!empty($relatedResources)) { + $result['included'] = $relatedResources; } if (($pagination = $dataProvider->getPagination()) !== false) { @@ -295,9 +322,11 @@ protected function serializeModelErrors($model) $this->response->setStatusCode(422, 'Data Validation Failed.'); $result = []; foreach ($model->getFirstErrors() as $name => $message) { + $memberName = call_user_func($this->prepareMemberName, $name); $result[] = [ - 'source' => ['pointer' => "/data/attributes/{$name}"], + 'source' => ['pointer' => "/data/attributes/{$memberName}"], 'detail' => $message, + 'status' => '422' ]; } @@ -320,6 +349,9 @@ protected function getRequestedFields() return $fields; } + /** + * @return array|null + */ protected function getIncluded() { $include = $this->request->get($this->expandParam); diff --git a/src/UrlRule.php b/src/UrlRule.php new file mode 100644 index 0000000..a5206fd --- /dev/null +++ b/src/UrlRule.php @@ -0,0 +1,32 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi; + + +/** + * UrlRule is provided to simplify the creation of URL rules for JSON API support. + * @package tuyakhov\jsonapi + */ +class UrlRule extends \yii\rest\UrlRule +{ + /** + * @inheritdoc + */ + public function init() + { + $this->tokens = array_merge($this->tokens, array_merge([ + '{relationship}' => '<name:\w+>' + ])); + $this->patterns = array_merge($this->patterns, [ + 'DELETE {id}/relationships/{relationship}' => 'delete-relationship', + 'POST,PATCH {id}/relationships/{relationship}' => 'update-relationship', + 'GET {id}/{relationship}' => 'view-related', + '{id}/{relationship}' => 'options' + ]); + parent::init(); + } + +} \ No newline at end of file diff --git a/src/actions/Action.php b/src/actions/Action.php index 202f69d..27eb2a8 100644 --- a/src/actions/Action.php +++ b/src/actions/Action.php @@ -6,6 +6,7 @@ namespace tuyakhov\jsonapi\actions; use tuyakhov\jsonapi\ResourceInterface; +use tuyakhov\jsonapi\ResourceTrait; use yii\db\ActiveRecordInterface; use yii\db\BaseActiveRecord; use yii\helpers\ArrayHelper; @@ -23,10 +24,15 @@ class Action extends \yii\rest\Action */ public $allowFullReplacement = true; + /** + * @var bool Weather allow to delete the underlying resource if a relationship is deleted (as a garbage collection measure) + */ + public $enableResourceDeleting = false; + /** * Links the relationships with primary model. * @param $model ActiveRecordInterface - * @param array $data + * @param array $data relationship links */ protected function linkRelationships($model, array $data = []) { @@ -55,14 +61,16 @@ protected function linkRelationships($model, array $data = []) $ids[] = $relObject['id']; } - if (!$records = $relatedClass::find()->andWhere(['in', $relatedClass::primaryKey(), $ids])->all()) { + if ($related->multiple && !$this->allowFullReplacement) { continue; } + $records = $relatedClass::find()->andWhere(['in', $relatedClass::primaryKey(), $ids])->all(); - if ($related->multiple && !$this->allowFullReplacement) { - continue; + /** @see ResourceTrait::$allowDeletingResources */ + if (method_exists($model, 'setAllowDeletingResources')) { + $model->setAllowDeletingResources($this->enableResourceDeleting); } - $model->unlinkAll($name); + $model->setResourceRelationship($name, $records); } } diff --git a/src/actions/DeleteAction.php b/src/actions/DeleteAction.php new file mode 100644 index 0000000..c49d941 --- /dev/null +++ b/src/actions/DeleteAction.php @@ -0,0 +1,40 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi\actions; + +use Yii; +use yii\db\BaseActiveRecord; +use yii\web\NotFoundHttpException; +use yii\web\ServerErrorHttpException; + +/** + * Implements the API endpoint for deleting resources. + * @link http://jsonapi.org/format/#crud-deleting + */ +class DeleteAction extends Action +{ + /** + * Deletes a resource. + * @param mixed $id id of the resource to be deleted. + * @throws NotFoundHttpException + * @throws ServerErrorHttpException on failure. + */ + public function run($id) + { + /** @var BaseActiveRecord $model */ + $model = $this->findModel($id); + + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id, $model); + } + + if ($model->delete() === false) { + throw new ServerErrorHttpException('Failed to delete the resource for unknown reason.'); + } + + Yii::$app->getResponse()->setStatusCode(204); + } +} \ No newline at end of file diff --git a/src/actions/DeleteRelationshipAction.php b/src/actions/DeleteRelationshipAction.php new file mode 100644 index 0000000..96bb3ee --- /dev/null +++ b/src/actions/DeleteRelationshipAction.php @@ -0,0 +1,97 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi\actions; + +use Yii; +use yii\data\ActiveDataProvider; +use yii\db\ActiveRecordInterface; +use yii\db\ActiveRelationTrait; +use yii\db\BaseActiveRecord; +use yii\helpers\ArrayHelper; +use yii\web\ForbiddenHttpException; +use yii\web\NotFoundHttpException; + +/** + * Deletes the specified members from a relationship + * @link http://jsonapi.org/format/#crud-updating-relationships + */ +class DeleteRelationshipAction extends Action +{ + /** + * Removes the relationships from primary model. + * @var callable + */ + public $unlinkRelationships; + + /** + * @param string $id an ID of the primary resource + * @param string $name a name of the related resource + * @return ActiveDataProvider + * @throws ForbiddenHttpException + * @throws NotFoundHttpException + * @throws \yii\base\InvalidConfigException + */ + public function run($id, $name) + { + /** @var BaseActiveRecord $model */ + $model = $this->findModel($id); + + if (!$related = $model->getRelation($name, false)) { + throw new NotFoundHttpException('Relationship does not exist'); + } + + if (!$related->multiple) { + throw new ForbiddenHttpException('Unsupported request to update relationship'); + } + + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id, $model, $name); + } + + $this->unlinkRelationships($model, [$name => Yii::$app->getRequest()->getBodyParams()]); + + + return new ActiveDataProvider([ + 'query' => $related + ]); + } + + /** + * Removes the relationships from primary model. + * @param $model ActiveRecordInterface + * @param array $data relationship links + */ + protected function unlinkRelationships($model, array $data = []) + { + if ($this->unlinkRelationships !== null) { + call_user_func($this->unlinkRelationships, $this, $model, $data); + return; + } + + foreach ($data as $name => $relationship) { + /** @var $related ActiveRelationTrait */ + if (!$related = $model->getRelation($name, false)) { + continue; + } + /** @var BaseActiveRecord $relatedClass */ + $relatedClass = new $related->modelClass; + $relationships = ArrayHelper::keyExists($relatedClass->formName(), $relationship) ? $relationship[$relatedClass->formName()] : []; + + $ids = []; + foreach ($relationships as $index => $relObject) { + if (!isset($relObject['id'])) { + continue; + } + $ids[] = $relObject['id']; + } + + $records = $relatedClass::find()->andWhere(['in', $relatedClass::primaryKey(), $ids])->all(); + foreach ($records as $record) { + $model->unlink($name, $record, $this->enableResourceDeleting); + } + } + } +} \ No newline at end of file diff --git a/src/actions/IndexAction.php b/src/actions/IndexAction.php new file mode 100644 index 0000000..85be334 --- /dev/null +++ b/src/actions/IndexAction.php @@ -0,0 +1,133 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi\actions; + + +use tuyakhov\jsonapi\Inflector; +use tuyakhov\jsonapi\Pagination; +use yii\data\ActiveDataProvider; +use yii\data\DataFilter; +use Yii; + +class IndexAction extends Action +{ + /** + * @var callable a PHP callable that will be called to prepare a data provider that + * should return a collection of the models. If not set, [[prepareDataProvider()]] will be used instead. + * The signature of the callable should be: + * + * ```php + * function (IndexAction $action) { + * // $action is the action object currently running + * } + * ``` + * + * The callable should return an instance of [[ActiveDataProvider]]. + * + * If [[dataFilter]] is set the result of [[DataFilter::build()]] will be passed to the callable as a second parameter. + * In this case the signature of the callable should be the following: + * + * ```php + * function (IndexAction $action, mixed $filter) { + * // $action is the action object currently running + * // $filter the built filter condition + * } + * ``` + */ + public $prepareDataProvider; + /** + * @var DataFilter|null data filter to be used for the search filter composition. + * You must setup this field explicitly in order to enable filter processing. + * For example: + * + * ```php + * [ + * 'class' => 'yii\data\ActiveDataFilter', + * 'searchModel' => function () { + * return (new \yii\base\DynamicModel(['id' => null, 'name' => null, 'price' => null])) + * ->addRule('id', 'integer') + * ->addRule('name', 'trim') + * ->addRule('name', 'string') + * ->addRule('price', 'number'); + * }, + * ] + * ``` + * + * @see DataFilter + * + * @since 2.0.13 + */ + public $dataFilter; + + + /** + * @return ActiveDataProvider + * @throws \yii\base\InvalidConfigException + */ + public function run() + { + if ($this->checkAccess) { + call_user_func($this->checkAccess, $this->id); + } + + return $this->prepareDataProvider(); + } + + /** + * Prepares the data provider that should return the requested collection of the models. + * @return mixed|null|object|DataFilter|ActiveDataProvider + * @throws \yii\base\InvalidConfigException + */ + protected function prepareDataProvider() + { + $filter = $this->getFilter(); + + if ($this->prepareDataProvider !== null) { + return call_user_func($this->prepareDataProvider, $this, $filter); + } + + /* @var $modelClass \yii\db\BaseActiveRecord */ + $modelClass = $this->modelClass; + + $query = $modelClass::find(); + if (!empty($filter)) { + $query->andWhere($filter); + } + + return Yii::createObject([ + 'class' => ActiveDataProvider::className(), + 'query' => $query, + 'pagination' => [ + 'class' => Pagination::className(), + ], + 'sort' => [ + 'enableMultiSort' => true + ] + ]); + } + + protected function getFilter() + { + if ($this->dataFilter === null) { + return null; + } + $requestParams = Yii::$app->getRequest()->getQueryParam('filter', []); + $attributeMap = []; + foreach ($requestParams as $attribute => $value) { + $attributeMap[$attribute] = Inflector::camel2id(Inflector::variablize($attribute), '_'); + if (is_string($value) && strpos($value, ',') !== false) { + $requestParams[$attribute] = ['in' => explode(',', $value)]; + } + } + $config = array_merge(['attributeMap' => $attributeMap], $this->dataFilter); + /** @var DataFilter $dataFilter */ + $dataFilter = Yii::createObject($config); + if ($dataFilter->load(['filter' => $requestParams])) { + return $dataFilter->build(); + } + return null; + } +} \ No newline at end of file diff --git a/src/actions/UpdateRelationshipAction.php b/src/actions/UpdateRelationshipAction.php index 218ebe3..54deb24 100644 --- a/src/actions/UpdateRelationshipAction.php +++ b/src/actions/UpdateRelationshipAction.php @@ -5,7 +5,6 @@ namespace tuyakhov\jsonapi\actions; -use tuyakhov\jsonapi\ResourceInterface; use yii\data\ActiveDataProvider; use yii\db\BaseActiveRecord; use yii\web\BadRequestHttpException; diff --git a/src/actions/ViewAction.php b/src/actions/ViewAction.php new file mode 100644 index 0000000..1570dfe --- /dev/null +++ b/src/actions/ViewAction.php @@ -0,0 +1,12 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi\actions; + + +class ViewAction extends \yii\rest\ViewAction +{ + +} \ No newline at end of file diff --git a/src/actions/ViewRelatedAction.php b/src/actions/ViewRelatedAction.php index 9810071..8fbcf01 100644 --- a/src/actions/ViewRelatedAction.php +++ b/src/actions/ViewRelatedAction.php @@ -6,6 +6,7 @@ namespace tuyakhov\jsonapi\actions; +use tuyakhov\jsonapi\Pagination; use tuyakhov\jsonapi\ResourceInterface; use yii\data\ActiveDataProvider; use yii\db\ActiveQuery; @@ -45,15 +46,18 @@ public function run($id, $name) } if ($this->prepareDataProvider !== null) { - return call_user_func($this->prepareDataProvider, $this, $related); + return call_user_func($this->prepareDataProvider, $this, $related, $name); } if ($related->multiple) { return new ActiveDataProvider([ - 'query' => $related + 'query' => $related, + 'pagination' => [ + 'class' => Pagination::className(), + ], ]); } else { return $related->one(); } } -} \ No newline at end of file +} diff --git a/tests/JsonApiParserTest.php b/tests/JsonApiParserTest.php index 1a21816..d442273 100644 --- a/tests/JsonApiParserTest.php +++ b/tests/JsonApiParserTest.php @@ -8,9 +8,25 @@ use tuyakhov\jsonapi\JsonApiParser; use yii\helpers\Json; +use yii\web\BadRequestHttpException; class JsonApiParserTest extends TestCase { + public function testEmptyBody() + { + $parser = new JsonApiParser(); + $body = ''; + $this->assertEquals([], $parser->parse($body, '')); + } + + public function testMissingData() + { + $parser = new JsonApiParser(); + $this->expectException(BadRequestHttpException::class); + $body = Json::encode(['incorrect-member']); + $parser->parse($body, ''); + } + public function testSingleResource() { $parser = new JsonApiParser(); diff --git a/tests/JsonApiResponseFormatterTest.php b/tests/JsonApiResponseFormatterTest.php new file mode 100644 index 0000000..3e0e89a --- /dev/null +++ b/tests/JsonApiResponseFormatterTest.php @@ -0,0 +1,126 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ +namespace tuyakhov\jsonapi\tests; + + +use tuyakhov\jsonapi\JsonApiResponseFormatter; +use tuyakhov\jsonapi\Serializer; +use tuyakhov\jsonapi\tests\data\ResourceModel; +use yii\base\Controller; +use yii\helpers\Json; +use yii\web\Response; +use yii\web\ServerErrorHttpException; + +class JsonApiResponseFormatterTest extends TestCase +{ + public function testFormatException() + { + $formatter = new JsonApiResponseFormatter(); + $exception = new ServerErrorHttpException('Server error'); + $response = new Response(); + $response->setStatusCode($exception->statusCode); + $response->data = [ + 'name' => $exception->getName(), + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + 'status' => $exception->statusCode + ]; + $formatter->format($response); + $this->assertJson($response->content); + $this->assertSame(Json::encode([ + 'errors' => [ + [ + 'code' => '0', + 'status' => '500', + 'title' => Response::$httpStatuses[500], + 'detail' => 'Server error', + ] + ] + ]), $response->content); + } + + public function testFormModelError() + { + $formatter = new JsonApiResponseFormatter(); + $exception = new ServerErrorHttpException('Server error'); + $response = new Response(); + $response->setStatusCode($exception->statusCode); + $serializer = new Serializer(); + $model = new ResourceModel(); + $model->addError('field1', 'Error'); + $model->addError('field2', 'Test Error'); + $response->data = $serializer->serialize($model); + $formatter->format($response); + $this->assertJson($response->content); + $this->assertSame(Json::encode([ + 'errors' => [ + [ + 'source' => ['pointer' => "/data/attributes/field1"], + 'detail' => 'Error', + 'status' => '422' + ], + [ + 'source' => ['pointer' => "/data/attributes/field2"], + 'detail' => 'Test Error', + 'status' => '422' + ] + ] + ]), $response->content); + } + + public function testSuccessModel() + { + $formatter = new JsonApiResponseFormatter(); + $response = new Response(); + $serializer = new Serializer(); + $model = new ResourceModel(); + $response->data = $serializer->serialize($model); + $response->setStatusCode(200); + $formatter->format($response); + $this->assertJson($response->content); + $this->assertSame(Json::encode([ + 'data' => [ + 'id' => '123', + 'type' => 'resource-models', + 'attributes' => [ + 'field1' => 'test', + 'field2' => 2, + ], + 'links' => [ + 'self' => [ + 'href' => 'http://example.com/resource/123' + ] + ] + ] + ]), $response->content); + } + + public function testEmptyData() + { + $formatter = new JsonApiResponseFormatter(); + $response = new Response(); + $response->setStatusCode('200'); + $serializer = new Serializer(); + $response->data = $serializer->serialize(null); + $formatter->format($response); + $this->assertJson($response->content); + $this->assertSame(Json::encode([ + 'data' => null + ]), $response->content); + \Yii::$app->controller = new Controller('test', \Yii::$app); + $formatter->format($response); + $this->assertJson($response->content); + $this->assertSame(Json::encode([ + 'data' => null, + 'links' => [ + 'self' => ['href' => '/index.php?r=test'] + ] + ]), $response->content); + $response->clear(); + $response->setStatusCode(201); + $formatter->format($response); + $this->assertNull($response->content); + } +} \ No newline at end of file diff --git a/tests/SerializerTest.php b/tests/SerializerTest.php index b75c760..eba491d 100644 --- a/tests/SerializerTest.php +++ b/tests/SerializerTest.php @@ -35,14 +35,22 @@ public function testSerializeModelErrors() $model->addError('field1', 'Test error'); $model->addError('field2', 'Multiple error 1'); $model->addError('field2', 'Multiple error 2'); + $model->addError('first_name', 'Member name check'); $this->assertEquals([ [ 'source' => ['pointer' => "/data/attributes/field1"], 'detail' => 'Test error', + 'status' => '422' ], [ 'source' => ['pointer' => "/data/attributes/field2"], 'detail' => 'Multiple error 1', + 'status' => '422' + ], + [ + 'source' => ['pointer' => "/data/attributes/first-name"], + 'detail' => 'Member name check', + 'status' => '422' ] ], $serializer->serialize($model)); } @@ -67,19 +75,39 @@ public function testSerializeModelData() ], $serializer->serialize($model)); ResourceModel::$fields = ['first_name']; - ResourceModel::$extraFields = []; - $this->assertSame([ - 'data' => [ - 'id' => '123', - 'type' => 'resource-models', - 'attributes' => [ - 'first-name' => 'Bob', - ], - 'links' => [ - 'self' => ['href' => 'http://example.com/resource/123'] + ResourceModel::$extraFields = ['extraField1']; + $model->extraField1 = new ResourceModel(); + $relationship = [ + 'data' => ['id' => '123', 'type' => 'resource-models'], + 'links' => [ + 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'], + 'related' => ['href' => 'http://example.com/resource/123/extra-field1'], + ] + ]; + $resource = [ + 'id' => '123', + 'type' => 'resource-models', + 'attributes' => [ + 'first-name' => 'Bob', + ], + 'relationships' => [ + 'extra-field1' => [ + 'links' => $relationship['links'] ] + ], + 'links' => [ + 'self' => ['href' => 'http://example.com/resource/123'] ] - ], $serializer->serialize($model)); + ]; + $expected = [ + 'data' => $resource + ]; + $this->assertSame($expected, $serializer->serialize($model)); + $_POST[$serializer->request->methodParam] = 'POST'; + \Yii::$app->request->setQueryParams(['include' => 'extra-field1']); + $expected['included'][] = $resource; + $expected['data']['relationships']['extra-field1'] = $relationship; + $this->assertSame($expected, $serializer->serialize($model)); } public function testExpand() @@ -92,16 +120,17 @@ public function testExpand() 'field1' => 'test', 'field2' => 2, ], - ]; - $compoundModel['relationships'] = [ - 'extra-field1' => [ - 'data' => ['id' => '123', 'type' => 'resource-models'], - 'links' => [ - 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'], - 'related' => ['href' => 'http://example.com/resource/123/extra-field1'], + 'relationships' => [ + 'extra-field1' => [ + 'data' => ['id' => '123', 'type' => 'resource-models'], + 'links' => [ + 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'], + 'related' => ['href' => 'http://example.com/resource/123/extra-field1'], + ] ] ] ]; + unset($includedModel['relationships']['extra-field1']['data']); $compoundModel['links'] = $includedModel['links'] = [ 'self' => ['href' => 'http://example.com/resource/123'] ]; @@ -140,6 +169,70 @@ public function testExpand() ], $serializer->serialize($model)); } + public function testNestedRelationships() + { + ResourceModel::$fields = ['field1']; + ResourceModel::$extraFields = ['extraField1']; + $resource = new ResourceModel(); + $relationship = new ResourceModel(); + $subRelationship = new ResourceModel(); + $subRelationship->setId(321); + $relationship->extraField1 = $subRelationship; + $resource->extraField1 = $relationship; + $compoundDocument = [ + 'data' => [ + 'id' => '123', + 'type' => 'resource-models', + 'attributes' => ['field1' => 'test'], + 'relationships' => [ + 'extra-field1' => [ + 'data' => ['id' => '123', 'type' => 'resource-models'], + 'links' => [ + 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'], + 'related' => ['href' => 'http://example.com/resource/123/extra-field1'], + ] + ], + ], + 'links' => ['self' => ['href' => 'http://example.com/resource/123']], + ], + 'included' => [ + [ + 'id' => '123', + 'type' => 'resource-models', + 'attributes' => ['field1' => 'test'], + 'relationships' => [ + 'extra-field1' => [ + 'data' => ['id' => '321', 'type' => 'resource-models'], + 'links' => [ + 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'], + 'related' => ['href' => 'http://example.com/resource/123/extra-field1'], + ] + ], + ], + 'links' => ['self' => ['href' => 'http://example.com/resource/123']], + ], + [ + 'id' => '321', + 'type' => 'resource-models', + 'attributes' => ['field1' => 'test'], + 'relationships' => [ + 'extra-field1' => [ + 'links' => [ + 'self' => ['href' => 'http://example.com/resource/321/relationships/extra-field1'], + 'related' => ['href' => 'http://example.com/resource/321/extra-field1'], + ] + ], + ], + 'links' => ['self' => ['href' => 'http://example.com/resource/321']], + ] + ] + ]; + + $serializer = new Serializer(); + \Yii::$app->request->setQueryParams(['include' => 'extra-field1.extra-field1']); + $this->assertSame($compoundDocument, $serializer->serialize($resource)); + } + public function testIncludedDuplicates() { $serializer = new Serializer(); @@ -151,23 +244,25 @@ public function testIncludedDuplicates() 'field1' => 'test', 'field2' => 2, ], - ]; - $compoundModel['relationships'] = [ - 'extra-field1' => [ - 'data' => ['id' => '123', 'type' => 'resource-models'], - 'links' => [ - 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'], - 'related' => ['href' => 'http://example.com/resource/123/extra-field1'], - ] - ], - 'extra-field2' => [ - 'data' => ['id' => '123', 'type' => 'resource-models'], - 'links' => [ - 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field2'], - 'related' => ['href' => 'http://example.com/resource/123/extra-field2'], + 'relationships' => [ + 'extra-field1' => [ + 'data' => ['id' => '123', 'type' => 'resource-models'], + 'links' => [ + 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field1'], + 'related' => ['href' => 'http://example.com/resource/123/extra-field1'], + ] + ], + 'extra-field2' => [ + 'data' => ['id' => '123', 'type' => 'resource-models'], + 'links' => [ + 'self' => ['href' => 'http://example.com/resource/123/relationships/extra-field2'], + 'related' => ['href' => 'http://example.com/resource/123/extra-field2'], + ] ] ] ]; + unset($includedModel['relationships']['extra-field1']['data']); + unset($includedModel['relationships']['extra-field2']['data']); $compoundModel['links'] = $includedModel['links'] = [ 'self' => ['href' => 'http://example.com/resource/123'] ]; @@ -175,10 +270,12 @@ public function testIncludedDuplicates() $model = new ResourceModel(); ResourceModel::$fields = ['field1', 'field2']; ResourceModel::$extraFields = ['extraField1', 'extraField2']; + $relationship = new ResourceModel(); + $relationship->extraField1 = new ResourceModel(); + $model->extraField2 = $relationship; $model->extraField1 = new ResourceModel(); - $model->extraField2 = new ResourceModel(); - \Yii::$app->request->setQueryParams(['include' => 'extra-field1,extra-field2']); + \Yii::$app->request->setQueryParams(['include' => 'extra-field1,extra-field2.extra-field1']); $this->assertSame([ 'data' => $compoundModel, 'included' => [ @@ -191,7 +288,7 @@ public function testIncludedDuplicates() $includedModel ], 'links' => [ - 'self' => ['href' => '/index.php?r=&include=extra-field1%2Cextra-field2&page=1'] + 'self' => ['href' => '/index.php?r=&include=extra-field1%2Cextra-field2.extra-field1&page=1'] ], 'meta' => [ 'total-count' => 1, diff --git a/tests/TestCase.php b/tests/TestCase.php index 6ed1c5e..4b5711f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,6 +5,7 @@ namespace tuyakhov\jsonapi\tests; use \yii\helpers\ArrayHelper; +use yii\web\Response; class TestCase extends \PHPUnit_Framework_TestCase { @@ -37,6 +38,12 @@ protected function mockApplication($config = [], $appClass = '\yii\web\Applicati 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', 'scriptFile' => __DIR__ .'/index.php', 'scriptUrl' => '/index.php', + ], + 'response' => [ + 'format' => Response::FORMAT_JSON, + 'formatters' => [ + Response::FORMAT_JSON => 'tuyakhov\jsonapi\JsonApiResponseFormatter' + ] ] ], 'vendorPath' => $this->getVendorPath(), diff --git a/tests/actions/CreateActionTest.php b/tests/actions/CreateActionTest.php index 9609db0..7295a92 100644 --- a/tests/actions/CreateActionTest.php +++ b/tests/actions/CreateActionTest.php @@ -44,7 +44,7 @@ public function testSuccess() $this->assertInstanceOf(ResourceModel::className(), $model = $action->run()); $this->assertFalse($model->hasErrors()); - $relationships = $model->getResourceRelationships(); + $relationships = $model->getResourceRelationships(['extra_field1']); $this->assertArrayHasKey('extra_field1', $relationships); $this->assertInstanceOf(ResourceModel::className(), $relationships['extra_field1']); $this->assertEquals(124, $relationships['extra_field1']->id); diff --git a/tests/actions/DeleteActionTest.php b/tests/actions/DeleteActionTest.php new file mode 100644 index 0000000..01b2aff --- /dev/null +++ b/tests/actions/DeleteActionTest.php @@ -0,0 +1,29 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi\tests\actions; + +use tuyakhov\jsonapi\actions\DeleteAction; +use tuyakhov\jsonapi\tests\data\ActiveQuery; +use tuyakhov\jsonapi\tests\data\ResourceModel; +use tuyakhov\jsonapi\tests\TestCase; +use yii\web\Controller; + +class DeleteActionTest extends TestCase +{ + public function testSuccess() + { + \Yii::$app->controller = new Controller('test', \Yii::$app); + $action = new DeleteAction('test', \Yii::$app->controller, [ + 'modelClass' => ResourceModel::className(), + ]); + + ResourceModel::$id = 124; + ActiveQuery::$models = new ResourceModel(); + + $action->run(124); + $this->assertTrue(\Yii::$app->getResponse()->getIsEmpty()); + } +} \ No newline at end of file diff --git a/tests/actions/DeleteRelationshipActionTest.php b/tests/actions/DeleteRelationshipActionTest.php new file mode 100644 index 0000000..5f58746 --- /dev/null +++ b/tests/actions/DeleteRelationshipActionTest.php @@ -0,0 +1,37 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi\tests\actions; + +use tuyakhov\jsonapi\actions\DeleteRelationshipAction; +use tuyakhov\jsonapi\tests\data\ActiveQuery; +use tuyakhov\jsonapi\tests\data\ResourceModel; +use tuyakhov\jsonapi\tests\TestCase; +use yii\data\ActiveDataProvider; +use yii\web\Controller; +use yii\web\ForbiddenHttpException; + +class DeleteRelationshipActionTest extends TestCase +{ + public function testSuccess() + { + $model = new ResourceModel(); + $action = new DeleteRelationshipAction('test', new Controller('test', \Yii::$app), [ + 'modelClass' => ResourceModel::className() + ]); + ResourceModel::$related = [ + 'extraField1' => new ActiveQuery(ResourceModel::className(), ['multiple' => true]), + 'extraField2' => new ActiveQuery(ResourceModel::className()) + ]; + $action->findModel = function ($id, $action) use($model) { + return $model; + }; + $model->extraField1 = [new ResourceModel()]; + \Yii::$app->request->setBodyParams(['ResourceModel' => ['type' => 'resource-models', 'id' => 123]]); + $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run(1, 'extraField1')); + $this->expectException(ForbiddenHttpException::class); + $this->assertInstanceOf(ResourceModel::className(), $action->run(1, 'extraField2')); + } +} \ No newline at end of file diff --git a/tests/actions/IndexActionTest.php b/tests/actions/IndexActionTest.php new file mode 100644 index 0000000..75bc865 --- /dev/null +++ b/tests/actions/IndexActionTest.php @@ -0,0 +1,94 @@ +<?php +/** + * @author Anton Tuyakhov <atuyakhov@gmail.com> + */ + +namespace tuyakhov\jsonapi\tests\actions; + + +use tuyakhov\jsonapi\actions\IndexAction; +use tuyakhov\jsonapi\tests\data\ActiveQuery; +use tuyakhov\jsonapi\tests\data\ResourceModel; +use tuyakhov\jsonapi\tests\TestCase; +use yii\base\Controller; +use yii\data\ActiveDataFilter; +use yii\data\ActiveDataProvider; +use yii\db\Query; + +class IndexActionTest extends TestCase +{ + public function testSuccess() + { + $action = new IndexAction('test', new Controller('test', \Yii::$app), [ + 'modelClass' => ResourceModel::className(), + 'dataFilter' => [ + 'class' => ActiveDataFilter::className(), + 'searchModel' => ResourceModel::className() + ] + ]); + $filter = [ + 'filter' => ['field1' => 'test,qwe'], + 'sort' => 'field1,-field2' + ]; + \Yii::$app->getRequest()->setQueryParams($filter); + + $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run()); + $this->assertInstanceOf(Query::className(), $dataProvider->query); + $this->assertSame([ + 'IN', + 'field1', + ['test', 'qwe'] + ], $dataProvider->query->where); + $this->assertSame(['field1' => SORT_ASC, 'field2' => SORT_DESC], $dataProvider->getSort()->orders); + } + + public function testValidation() + { + $action = new IndexAction('test', new Controller('test', \Yii::$app), [ + 'modelClass' => ResourceModel::className(), + 'dataFilter' => [ + 'class' => ActiveDataFilter::className(), + 'searchModel' => ResourceModel::className() + ] + ]); + \Yii::$app->getRequest()->setQueryParams(['filter' => ['field1' => 1]]); + + $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run()); + $this->assertInstanceOf(Query::className(), $dataProvider->query); + $this->assertNull($dataProvider->query->where); + } + + public function testPagination() + { + $action = new IndexAction('test', new Controller('test', \Yii::$app), [ + 'modelClass' => ResourceModel::className(), + ]); + ActiveQuery::$models = [new ResourceModel(), new ResourceModel()]; + $params = ['page' => 1]; + \Yii::$app->getRequest()->setQueryParams($params); + + $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run()); + $this->assertSame(0, $dataProvider->getPagination()->page); + + $params = ['page' => ['number' => 2, 'size' => 1]]; + \Yii::$app->getRequest()->setQueryParams($params); + + $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run()); + + $this->assertSame(2, $dataProvider->getCount()); + $this->assertSame(2, $dataProvider->pagination->getPageCount()); + $this->assertSame(1, $dataProvider->pagination->getPageSize()); + $this->assertSame(1, $dataProvider->pagination->getOffset()); + + // test invalid value + $params = ['page' => 1]; + \Yii::$app->getRequest()->setQueryParams($params); + + $this->assertInstanceOf(ActiveDataProvider::className(), $dataProvider = $action->run()); + + $this->assertSame(2, $dataProvider->getCount()); + $this->assertSame(1, $dataProvider->pagination->getPageCount()); + $this->assertSame($dataProvider->pagination->defaultPageSize, $dataProvider->pagination->getPageSize()); + $this->assertSame(0, $dataProvider->pagination->getOffset()); + } +} \ No newline at end of file diff --git a/tests/actions/UpdateActionTest.php b/tests/actions/UpdateActionTest.php index 2f1b6f5..988dcb3 100644 --- a/tests/actions/UpdateActionTest.php +++ b/tests/actions/UpdateActionTest.php @@ -1,9 +1,6 @@ <?php /** - * Created by PhpStorm. - * User: anton - * Date: 08/03/2017 - * Time: 19:07 + * @author Anton Tuyakhov <atuyakhov@gmail.com> */ namespace tuyakhov\jsonapi\tests\actions; @@ -49,7 +46,7 @@ public function testSuccess() $this->assertInstanceOf(ResourceModel::className(), $model = $action->run(1)); $this->assertFalse($model->hasErrors()); $this->assertEquals('test', $model->field1); - $relationships = $model->getResourceRelationships(); + $relationships = $model->getResourceRelationships(['extra_field1']); $this->assertArrayHasKey('extra_field1', $relationships); $this->assertInstanceOf(ResourceModel::className(), $relationships['extra_field1']); $this->assertEquals(124, $relationships['extra_field1']->id); diff --git a/tests/data/ResourceModel.php b/tests/data/ResourceModel.php index b91b6c0..70d8c85 100644 --- a/tests/data/ResourceModel.php +++ b/tests/data/ResourceModel.php @@ -26,10 +26,26 @@ class ResourceModel extends Model implements ResourceInterface, LinksInterface public $username = ''; public $extraField1 = 'testExtra'; public $extraField2 = 42; + private $_id; + + public function rules() + { + return [ + ['field1', 'string'] + ]; + } public function getId() { - return static::$id; + if ($this->_id === null) { + $this->_id = static::$id; + } + return $this->_id; + } + + public function setId($value) + { + $this->_id = $value; } public function fields() @@ -72,9 +88,14 @@ public function getPrimaryKey($asArray = false) return $asArray ? [$this->getId()] : $this->getId(); } - public function unlinkAll() + public function unlinkAll($name, $delete = false) { - return; + $this->$name = null; + } + + public function unlink($name, $model) + { + array_pop($this->$name); } public function save() @@ -82,6 +103,11 @@ public function save() return true; } + public function delete() + { + return true; + } + public function getLinks() { return [