├── tests ├── _data │ └── .gitkeep ├── _output │ └── .gitignore ├── api.suite.yml ├── _support │ ├── Helper │ │ └── Api.php │ └── ApiTester.php ├── _bootstrap.php └── api │ ├── LoginUserCest.php │ └── CreateUserCest.php ├── runtime └── .gitignore ├── web ├── assets │ └── .gitignore ├── .htaccess └── index.php ├── .bowerrc ├── core ├── messages │ └── zh-CN │ │ ├── exception.php │ │ └── app.php ├── exceptions │ ├── ErrorCodes.php │ ├── InternalException.php │ └── InvalidArgumentException.php ├── types │ ├── UserStatus.php │ └── BaseType.php ├── EventBootstrap.php ├── traits │ └── ServiceTrait.php ├── requests │ ├── LoginRequest.php │ └── JoinRequest.php ├── behaviors │ └── LoggerBehavior.php ├── services │ └── UserService.php └── models │ └── User.php ├── config ├── params.php ├── test.php ├── console.php ├── web.php └── common.php ├── docker-compose.yml ├── .github ├── FUNDING.yml └── workflows │ ├── lint.yml │ └── test.yml ├── .editorconfig ├── grumphp.yml ├── modules └── v1 │ ├── Module.php │ └── controllers │ ├── UserController.php │ └── ActiveController.php ├── .env.example ├── yii.bat ├── .gitignore ├── _ide_helper.php ├── yii ├── mail └── layouts │ └── html.php ├── .scrutinizer.yml ├── codeception.yml ├── commands ├── HelloController.php └── GenerateController.php ├── controllers └── SiteController.php ├── migrations └── m200717_082932_create_user_table.php ├── LICENSE.md ├── composer.json ├── requirements.php └── README.md /tests/_data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /web/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "vendor/bower-asset" 3 | } 4 | -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_FILENAME} !-d 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule . index.php [L] 5 | -------------------------------------------------------------------------------- /core/messages/zh-CN/exception.php: -------------------------------------------------------------------------------- 1 | '参数异常', 7 | ]; 8 | -------------------------------------------------------------------------------- /core/exceptions/ErrorCodes.php: -------------------------------------------------------------------------------- 1 | env('APP_URL'), 5 | 'adminEmail' => env('ADMIN_EMAIL'), 6 | 'senderEmail' => env('SENDER_EMAIL'), 7 | 'senderName' => env('SENDER_NAME', env('APP_NAME')), 8 | ]; 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | php: 4 | image: yiisoftware/yii2-php:7.1-apache 5 | volumes: 6 | - ~/.composer-docker/cache:/root/.composer/cache:delegated 7 | - ./:/app:delegated 8 | ports: 9 | - '8000:80' -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: yiier 4 | custom: ['https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/20190424153510.png', 'https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/20190424153431.png'] 5 | -------------------------------------------------------------------------------- /tests/_support/Helper/Api.php: -------------------------------------------------------------------------------- 1 | 'basic-tests', 12 | ], $web); 13 | 14 | return ArrayHelper::merge($common, $config); 15 | -------------------------------------------------------------------------------- /core/messages/zh-CN/app.php: -------------------------------------------------------------------------------- 1 | '成功', 5 | 'Incorrect username or password.' => '账号或者密码错误。', 6 | '{attribute} can only be numbers and letters.' => '{attribute}只能为数字和字母。', 7 | 'Username' => '用户名', 8 | 'Email' => '邮箱', 9 | 'Password' => '密码', 10 | 'The JWT secret must be configured first.' => '必须先配置 JWT_SECRET', 11 | ]; 12 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | run(); 12 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 'active', 17 | self::UNACTIVATED => 'unactivated', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/v1/Module.php: -------------------------------------------------------------------------------- 1 | 7 | rem @link http://www.yiiframework.com/ 8 | rem @copyright Copyright (c) 2008 Yii Software LLC 9 | rem @license http://www.yiiframework.com/license/ 10 | rem ------------------------------------------------------------- 11 | 12 | @setlocal 13 | 14 | set YII_PATH=%~dp0 15 | 16 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe 17 | 18 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 19 | 20 | @endlocal 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | 29 | tests/_output/* 30 | tests/_support/_generated 31 | 32 | #vagrant folder 33 | /.vagrant 34 | 35 | # env 36 | .env 37 | .env.testing 38 | -------------------------------------------------------------------------------- /_ide_helper.php: -------------------------------------------------------------------------------- 1 | run(); 22 | exit($exitCode); 23 | -------------------------------------------------------------------------------- /tests/_support/ApiTester.php: -------------------------------------------------------------------------------- 1 | beforeSend($event); 17 | }); 18 | 19 | Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, function ($event) { 20 | \Yii::createObject(LoggerBehavior::class)->beforeAction(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/exceptions/InternalException.php: -------------------------------------------------------------------------------- 1 | 5 | * createTime : 2019/5/12 4:58 PM 6 | * description: 7 | */ 8 | 9 | namespace app\core\traits; 10 | 11 | use app\core\services\UserService; 12 | use Yii; 13 | use yii\base\InvalidConfigException; 14 | 15 | /** 16 | * Trait ServiceTrait 17 | * @property-read UserService $userService 18 | */ 19 | trait ServiceTrait 20 | { 21 | /** 22 | * @return UserService|object 23 | */ 24 | public function getUserService() 25 | { 26 | try { 27 | return Yii::createObject(UserService::class); 28 | } catch (InvalidConfigException $e) { 29 | return new UserService(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | beginPage() ?> 10 | 11 | 12 | 13 | 14 | <?= Html::encode($this->title) ?> 15 | head() ?> 16 | 17 | 18 | beginBody() ?> 19 | 20 | endBody() ?> 21 | 22 | 23 | endPage() ?> 24 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | nodes: 3 | analysis: 4 | tests: 5 | override: 6 | - php-scrutinizer-run 7 | checks: 8 | php: 9 | code_rating: true 10 | remove_extra_empty_lines: true 11 | remove_php_closing_tag: true 12 | remove_trailing_whitespace: true 13 | fix_use_statements: 14 | remove_unused: true 15 | preserve_multiple: false 16 | preserve_blanklines: true 17 | order_alphabetically: true 18 | fix_php_opening_tag: true 19 | fix_linefeed: true 20 | fix_line_ending: true 21 | fix_identation_4spaces: true 22 | fix_doc_comments: true 23 | tools: 24 | external_code_coverage: 25 | timeout: 600 26 | runs: 3 27 | php_analyzer: true 28 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | bootstrap: _bootstrap.php 3 | paths: 4 | tests: tests 5 | log: tests/_output 6 | data: tests/_data 7 | helpers: tests/_support 8 | settings: 9 | memory_limit: 1024M 10 | colors: true 11 | modules: 12 | config: 13 | Yii2: 14 | configFile: 'config/test.php' 15 | 16 | coverage: 17 | enabled: true 18 | 19 | # To enable code coverage: 20 | #coverage: 21 | # #c3_url: http://localhost:8080/index-test.php/ 22 | # enabled: true 23 | # #remote: true 24 | # #remote_config: '../codeception.yml' 25 | # whitelist: 26 | # include: 27 | # - models/* 28 | # - controllers/* 29 | # - commands/* 30 | # - mail/* 31 | # blacklist: 32 | # include: 33 | # - assets/* 34 | # - config/* 35 | # - runtime/* 36 | # - vendor/* 37 | # - views/* 38 | # - web/* 39 | # - tests/* 40 | -------------------------------------------------------------------------------- /commands/HelloController.php: -------------------------------------------------------------------------------- 1 | 20 | * @since 2.0 21 | */ 22 | class HelloController extends Controller 23 | { 24 | /** 25 | * This command echoes what you have entered as the message. 26 | * @param string $message the message to be echoed. 27 | * @return int Exit code 28 | */ 29 | public function actionIndex($message = 'hello world') 30 | { 31 | echo $message . "\n"; 32 | 33 | \Yii::error(['request_id' => \Yii::$app->requestId->id, 'test_request_id']); 34 | return ExitCode::OK; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /commands/GenerateController.php: -------------------------------------------------------------------------------- 1 | stdout("{$item} key [{$key}] set successfully.\n", Console::FG_GREEN); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config/console.php: -------------------------------------------------------------------------------- 1 | 'basic-console', 8 | 'basePath' => dirname(__DIR__), 9 | 'controllerNamespace' => 'app\commands', 10 | 'aliases' => [ 11 | '@bower' => '@vendor/bower-asset', 12 | '@npm' => '@vendor/npm-asset', 13 | '@tests' => '@app/tests', 14 | ], 15 | 'components' => [ 16 | 'user' => [ 17 | 'class' => 'yii\web\User', 18 | 'identityClass' => 'app\models\User', 19 | 'enableSession' => false, 20 | 'enableAutoLogin' => false, 21 | ], 22 | 'urlManager' => [ 23 | 'baseUrl' => env('APP_URL'), 24 | 'hostInfo' => env('APP_URL') 25 | ], 26 | ], 27 | 'params' => $params, 28 | ]; 29 | 30 | if (YII_ENV_DEV) { 31 | // configuration adjustments for 'dev' environment 32 | $config['bootstrap'][] = 'gii'; 33 | $config['modules']['gii'] = [ 34 | 'class' => 'yii\gii\Module', 35 | ]; 36 | } 37 | 38 | return \yii\helpers\ArrayHelper::merge($common, $config); 39 | -------------------------------------------------------------------------------- /controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | errorHandler->exception; 34 | if ($exception !== null) { 35 | Yii::error([ 36 | 'request_id' => Yii::$app->requestId->id, 37 | 'exception' => $exception->getMessage(), 38 | 'line' => $exception->getLine(), 39 | 'file' => $exception->getFile(), 40 | ], 'response_data_error'); 41 | return ['code' => $exception->getCode(), 'message' => $exception->getMessage()]; 42 | } 43 | return []; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /migrations/m200717_082932_create_user_table.php: -------------------------------------------------------------------------------- 1 | createTable('{{%user}}', [ 16 | 'id' => $this->primaryKey(), 17 | 'username' => $this->string(60)->notNull()->unique(), 18 | 'avatar' => $this->string()->defaultValue(''), 19 | 'email' => $this->string(120)->notNull()->unique(), 20 | 'auth_key' => $this->string()->notNull(), 21 | 'password_hash' => $this->string()->notNull(), 22 | 'password_reset_token' => $this->string()->defaultValue(''), 23 | 'status' => $this->tinyInteger()->defaultValue(0), 24 | 'created_at' => $this->timestamp()->defaultValue(null), 25 | 'updated_at' => $this->timestamp()->defaultValue(null), 26 | ]); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function safeDown() 33 | { 34 | $this->dropTable('{{%user}}'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/types/BaseType.php: -------------------------------------------------------------------------------- 1 | 'trim'], 20 | [['username', 'password'], 'required'], 21 | 22 | ['password', 'validatePassword'], 23 | ]; 24 | } 25 | 26 | public function validatePassword($attribute, $params) 27 | { 28 | if (!$this->hasErrors()) { 29 | $user = UserService::getUserByUsernameOrEmail($this->username); 30 | if (!$user || !$user->validatePassword($this->password)) { 31 | $this->addError($attribute, t('app', 'Incorrect username or password.')); 32 | } 33 | Yii::$app->user->setIdentity($user); 34 | } 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function attributeLabels() 41 | { 42 | return [ 43 | 'username' => t('app', 'Username'), 44 | 'password' => t('app', 'Password'), 45 | 'email' => t('app', 'Email'), 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/requests/JoinRequest.php: -------------------------------------------------------------------------------- 1 | '/^[a-z]\w*$/i', 26 | 'message' => t('app', '{attribute} can only be numbers and letters.') 27 | ], 28 | ['username', 'unique', 'targetClass' => User::class], 29 | ['username', 'string', 'min' => 4, 'max' => 60], 30 | 31 | ['email', 'string', 'min' => 2, 'max' => 120], 32 | ['email', 'unique', 'targetClass' => User::class], 33 | ['email', 'email'], 34 | 35 | ['password', 'required'], 36 | ['password', 'string', 'min' => 6], 37 | ]; 38 | } 39 | 40 | /** 41 | * @inheritdoc 42 | */ 43 | public function attributeLabels() 44 | { 45 | return [ 46 | 'username' => t('app', 'Username'), 47 | 'password' => t('app', 'Password'), 48 | 'email' => t('app', 'Email'), 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by forecho (https://forecho.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software LLC nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /core/behaviors/LoggerBehavior.php: -------------------------------------------------------------------------------- 1 | 'beforeSend', 18 | Controller::EVENT_BEFORE_ACTION => 'beforeAction' 19 | ]; 20 | } 21 | 22 | /** 23 | * @param $event 24 | * @throws \Exception 25 | */ 26 | public function beforeSend($event) 27 | { 28 | $response = $event->sender; 29 | if ($response->format != 'html') { 30 | $request = \Yii::$app->request; 31 | $requestId = Yii::$app->requestId->id; 32 | $code = ArrayHelper::getValue($response->data, 'code'); 33 | $message = [ 34 | 'request_id' => $requestId, 35 | 'type' => $code === 0 ? 'response_data_success' : 'response_data_error', 36 | 'header' => Json::encode($request->headers), 37 | 'params' => $request->bodyParams, 38 | 'url' => $request->absoluteUrl, 39 | 'response' => Json::encode($response->data) 40 | ]; 41 | $response->data = ['request_id' => $requestId] + $response->data; 42 | $code === 0 ? Yii::info($message, 'request') : Yii::error($message, 'request'); 43 | } 44 | } 45 | 46 | public function beforeAction() 47 | { 48 | return Yii::$app->requestId->id; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/api/LoginUserCest.php: -------------------------------------------------------------------------------- 1 | haveHttpHeader('content-type', 'application/json'); 27 | $I->sendPOST('/login', [ 28 | 'username' => 'demo', 29 | 'password' => 'pass123456', 30 | ]); 31 | $I->seeResponseCodeIs(HttpCode::OK); // 200 32 | $I->seeResponseIsJson(); 33 | $I->seeResponseContainsJson(['code' => ErrorCodes::INVALID_ARGUMENT_ERROR]); 34 | } 35 | 36 | 37 | /** 38 | * @param ApiTester $I 39 | * @param CreateUserCest $createUserCest 40 | */ 41 | public function userLogin(ApiTester $I, CreateUserCest $createUserCest) 42 | { 43 | $createUserCest->createUser($I); 44 | $I->haveHttpHeader('content-type', 'application/json'); 45 | $I->sendPOST('/login', [ 46 | 'username' => 'demo', 47 | 'password' => 'pass123', 48 | ]); 49 | $I->seeResponseCodeIs(HttpCode::OK); // 200 50 | $I->seeResponseIsJson(); 51 | $I->seeResponseContainsJson(['code' => 0]); 52 | $I->seeResponseJsonMatchesXpath('//data/token'); 53 | $I->seeResponseJsonMatchesXpath('//data/user/username'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modules/v1/controllers/UserController.php: -------------------------------------------------------------------------------- 1 | request->bodyParams; 39 | $data = $this->validate(new JoinRequest(), $params); 40 | 41 | /** @var JoinRequest $data */ 42 | return $this->userService->createUser($data); 43 | } 44 | 45 | /** 46 | * @return string[] 47 | * @throws InvalidArgumentException|\Throwable 48 | */ 49 | public function actionLogin() 50 | { 51 | $params = Yii::$app->request->bodyParams; 52 | $this->validate(new LoginRequest(), $params); 53 | $token = $this->userService->getToken(); 54 | $user = Yii::$app->user->identity; 55 | 56 | return [ 57 | 'user' => $user, 58 | 'token' => (string)$token, 59 | ]; 60 | } 61 | 62 | public function actionRefreshToken() 63 | { 64 | $user = Yii::$app->user->identity; 65 | $token = $this->userService->getToken(); 66 | return [ 67 | 'user' => $user, 68 | 'token' => (string)$token, 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/services/UserService.php: -------------------------------------------------------------------------------- 1 | username = $request->username; 27 | $user->email = $request->email; 28 | $user->setPassword($request->password); 29 | $user->generateAuthKey(); 30 | if (!$user->save()) { 31 | throw new \yii\db\Exception(Setup::errorMessage($user->firstErrors)); 32 | } 33 | } catch (Exception $e) { 34 | Yii::error( 35 | ['request_id' => Yii::$app->requestId->id, $user->attributes, $user->errors, (string)$e], 36 | __FUNCTION__ 37 | ); 38 | throw new InternalException($e->getMessage()); 39 | } 40 | return $user; 41 | } 42 | 43 | 44 | /** 45 | * @return string 46 | * @throws \Throwable 47 | */ 48 | public function getToken(): string 49 | { 50 | /** @var Jwt $jwt */ 51 | $jwt = Yii::$app->jwt; 52 | if (!$jwt->key) { 53 | throw new InternalException(t('app', 'The JWT secret must be configured first.')); 54 | } 55 | $signer = $jwt->getSigner('HS256'); 56 | $key = $jwt->getKey(); 57 | $time = time(); 58 | return (string)$jwt->getBuilder() 59 | ->issuedBy(params('appUrl')) 60 | ->identifiedBy(Yii::$app->name, true) 61 | ->issuedAt($time) 62 | ->expiresAt($time + 3600 * 72) 63 | ->withClaim('username', \user('username')) 64 | ->withClaim('id', \user('id')) 65 | ->getToken($signer, $key); 66 | } 67 | 68 | 69 | /** 70 | * @param string $value 71 | * @return User|ActiveRecord|null 72 | */ 73 | public static function getUserByUsernameOrEmail(string $value) 74 | { 75 | $condition = strpos($value, '@') ? ['email' => $value] : ['username' => $value]; 76 | return User::find()->where(['status' => UserStatus::ACTIVE]) 77 | ->andWhere($condition) 78 | ->one(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/api/CreateUserCest.php: -------------------------------------------------------------------------------- 1 | [ 29 | 'username' => 'demo', 30 | 'email' => 'demo', 31 | 'password' => 'pass123', 32 | ], 33 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR 34 | ], 35 | [ 36 | 'data' => [ 37 | 'username' => 'demo-sdsdkj', 38 | 'email' => 'demo@yii.com', 39 | 'password' => 'pass123', 40 | ], 41 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR 42 | ], 43 | [ 44 | 'data' => [ 45 | 'username' => 'demo-sdsdkj', 46 | 'email' => 'demo@yii.com', 47 | 'password' => 'pass1', 48 | ], 49 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR 50 | ], 51 | [ 52 | 'data' => [ 53 | 'username' => 'demo-sdsdkj', 54 | 'password' => 'pass1', 55 | ], 56 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR 57 | ], 58 | ]; 59 | } 60 | 61 | /** 62 | * @dataProvider makeValidateFailItems 63 | * @param ApiTester $I 64 | * @param Example $example 65 | */ 66 | public function createUserViaAPIFail(ApiTester $I, Example $example) 67 | { 68 | $I->haveHttpHeader('content-type', 'application/json'); 69 | $I->sendPOST('/join', $example['data']); 70 | $I->seeResponseCodeIs(HttpCode::OK); // 200 71 | $I->seeResponseIsJson(); 72 | $I->seeResponseContainsJson(['code' => $example['code']]); 73 | } 74 | 75 | /** 76 | * @param ApiTester $I 77 | */ 78 | public function createUser(ApiTester $I) 79 | { 80 | $I->haveHttpHeader('content-type', 'application/json'); 81 | $I->sendPOST('/join', [ 82 | 'username' => 'demo', 83 | 'email' => 'demo@yii.com', 84 | 'password' => 'pass123', 85 | ]); 86 | $I->seeResponseCodeIs(HttpCode::OK); // 200 87 | $I->seeResponseIsJson(); 88 | $I->seeResponseContainsJson(['code' => 0]); 89 | $I->seeResponseJsonMatchesXpath('//data/username'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forecho/yii2-rest-api", 3 | "description": "Yii 2 REST API Project Template", 4 | "keywords": [ 5 | "yii2", 6 | "framework", 7 | "rest", 8 | "basic", 9 | "project template" 10 | ], 11 | "type": "project", 12 | "license": "BSD-3-Clause", 13 | "support": { 14 | "issues": "https://github.com/forecho/yii2-rest-api/issues?state=open", 15 | "source": "https://github.com/forecho/yii2-rest-api" 16 | }, 17 | "minimum-stability": "stable", 18 | "require": { 19 | "php": ">=7.2", 20 | "yiisoft/yii2": "~2.0.14", 21 | "yiisoft/yii2-swiftmailer": "~2.0.0 || ~2.1.0", 22 | "yiithings/yii2-dotenv": "^1.0", 23 | "sizeg/yii2-jwt": "^2.0", 24 | "yiier/yii2-helpers": "^2.0" 25 | }, 26 | "require-dev": { 27 | "yiisoft/yii2-debug": "~2.1.0", 28 | "yiisoft/yii2-gii": "~2.1.0", 29 | "yiisoft/yii2-faker": "~2.0.0", 30 | "codeception/codeception": "^4.0", 31 | "codeception/verify": "~0.5.0 || ~1.1.0", 32 | "codeception/specify": "~0.4.6", 33 | "symfony/browser-kit": ">=2.7 <=4.2.4", 34 | "codeception/module-filesystem": "^1.0.0", 35 | "codeception/module-yii2": "^1.0.0", 36 | "codeception/module-asserts": "^1.0.0", 37 | "codeception/module-rest": "^1.0.0", 38 | "codeception/module-phpbrowser": "^1.0.0", 39 | "squizlabs/php_codesniffer": "^3.5.5", 40 | "phpro/grumphp": "^0.19.1", 41 | "mis/yii2-ide-helper": "^1.0" 42 | }, 43 | "config": { 44 | "process-timeout": 1800, 45 | "fxp-asset": { 46 | "enabled": false 47 | } 48 | }, 49 | "autoload": { 50 | "files": [ 51 | "vendor/yiier/yii2-helpers/src/GlobalFunctions.php", 52 | "vendor/yiier/yii2-helpers/src/SupportFunctions.php" 53 | ] 54 | }, 55 | "scripts": { 56 | "post-install-cmd": [ 57 | "yii\\composer\\Installer::postInstall" 58 | ], 59 | "post-create-project-cmd": [ 60 | "yii\\composer\\Installer::postCreateProject", 61 | "yii\\composer\\Installer::postInstall" 62 | ] 63 | }, 64 | "extra": { 65 | "yii\\composer\\Installer::postCreateProject": { 66 | "setPermission": [ 67 | { 68 | "runtime": "0777", 69 | "web/assets": "0777", 70 | "yii": "0755" 71 | } 72 | ] 73 | }, 74 | "yii\\composer\\Installer::postInstall": { 75 | "generateCookieValidationKey": [ 76 | "config/web.php" 77 | ] 78 | } 79 | }, 80 | "repositories": [ 81 | { 82 | "type": "composer", 83 | "url": "https://asset-packagist.org" 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action for Yii Framework with MySQL 2 | name: Testing 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | jobs: 9 | yii: 10 | name: Yii2 (PHP ${{ matrix.php-versions }}) 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | php-versions: ['7.2'] 16 | # php-versions: ['7.2', '7.3', '7.4'] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup PHP, with composer and extensions 22 | uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php 23 | with: 24 | php-version: ${{ matrix.php-versions }} 25 | extensions: mbstring, intl, gd, imagick, zip, dom, mysql 26 | coverage: xdebug #optional 27 | 28 | - name: Set up MySQL 29 | uses: mirromutth/mysql-action@v1.1 30 | with: 31 | collation server: utf8mb4_unicode_ci 32 | mysql version: 5.7 33 | mysql database: yii2 34 | mysql root password: root 35 | 36 | - name: Get composer cache directory 37 | id: composercache 38 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 39 | 40 | - name: Cache composer dependencies 41 | uses: actions/cache@v2 42 | with: 43 | path: ${{ steps.composercache.outputs.dir }} 44 | # Use composer.json for key, if composer.lock is not committed. 45 | # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 46 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 47 | restore-keys: ${{ runner.os }}-composer- 48 | 49 | - name: Install Composer dependencies 50 | run: | 51 | composer global require hirak/prestissimo 52 | composer install --no-progress --prefer-dist --no-interaction 53 | 54 | - name: Prepare the application 55 | run: | 56 | php -r "file_exists('.env') || copy('.env.example', '.env');" 57 | php yii generate/key 58 | 59 | - name: Run Tests 60 | run: | 61 | vendor/bin/codecept build 62 | php yii migrate --interactive=0 63 | nohup php -S localhost:8080 > yii.log 2>&1 & 64 | vendor/bin/codecept run --coverage --coverage-xml=coverage.clover 65 | wget https://scrutinizer-ci.com/ocular.phar 66 | php ocular.phar code-coverage:upload --format=php-clover tests/_output/coverage.clover 67 | # bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} || echo 'Codecov did not collect coverage reports' 68 | -------------------------------------------------------------------------------- /config/web.php: -------------------------------------------------------------------------------- 1 | 'basic', 10 | 'basePath' => dirname(__DIR__), 11 | 'aliases' => [ 12 | '@bower' => '@vendor/bower-asset', 13 | '@npm' => '@vendor/npm-asset', 14 | ], 15 | 'modules' => [ 16 | 'v1' => [ 17 | 'class' => 'app\modules\v1\Module', 18 | ], 19 | ], 20 | 'components' => [ 21 | 'request' => [ 22 | 'parsers' => [ 23 | 'application/json' => 'yii\web\JsonParser', 24 | ], 25 | // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation 26 | 'cookieValidationKey' => env('COOKIE_VALIDATION_KEY') 27 | ], 28 | 'response' => [ 29 | 'class' => 'yii\web\Response', 30 | 'on beforeSend' => function ($event) { 31 | yii::createObject([ 32 | 'class' => yiier\helpers\ResponseHandler::class, 33 | 'event' => $event, 34 | ])->formatResponse(); 35 | }, 36 | ], 37 | 'jwt' => [ 38 | 'class' => \sizeg\jwt\Jwt::class, 39 | 'key' => env('JWT_SECRET'), 40 | ], 41 | 'formatter' => [ 42 | 'dateFormat' => 'yyyy-MM-dd', 43 | 'datetimeFormat' => 'yyyy-MM-dd HH:mm:ss', 44 | 'decimalSeparator' => ',', 45 | 'thousandSeparator' => ' ', 46 | 'currencyCode' => 'CNY', 47 | ], 48 | 'user' => [ 49 | 'identityClass' => User::class, 50 | 'enableAutoLogin' => true, 51 | ], 52 | 'errorHandler' => [ 53 | 'errorAction' => 'site/error', 54 | ], 55 | 'mailer' => [ 56 | 'class' => 'yii\swiftmailer\Mailer', 57 | // send all mails to a file by default. You have to set 58 | // 'useFileTransport' to false and configure a transport 59 | // for the mailer to send real emails. 60 | 'useFileTransport' => true, 61 | ], 62 | 'urlManager' => [ 63 | 'enablePrettyUrl' => true, 64 | 'showScriptName' => false, 65 | 'rules' => [ 66 | "POST /" => '/user/', 67 | "GET health-check" => 'site/health-check', 68 | '///' => '//', 69 | ], 70 | ], 71 | ], 72 | 'params' => $params, 73 | ]; 74 | 75 | if (YII_ENV_DEV) { 76 | // configuration adjustments for 'dev' environment 77 | $config['bootstrap'][] = 'debug'; 78 | $config['modules']['debug'] = [ 79 | 'class' => 'yii\debug\Module', 80 | // uncomment the following to add your IP if you are not connecting from localhost. 81 | 'allowedIPs' => ['*'], 82 | ]; 83 | 84 | $config['bootstrap'][] = 'gii'; 85 | $config['modules']['gii'] = [ 86 | 'class' => 'yii\gii\Module', 87 | // uncomment the following to add your IP if you are not connecting from localhost. 88 | 'allowedIPs' => ['*'], 89 | ]; 90 | } 91 | 92 | return \yii\helpers\ArrayHelper::merge($common, $config); 93 | -------------------------------------------------------------------------------- /modules/v1/controllers/ActiveController.php: -------------------------------------------------------------------------------- 1 | 'yii\rest\Serializer', 27 | 'collectionEnvelope' => 'items', 28 | ]; 29 | 30 | public function behaviors() 31 | { 32 | $behaviors = parent::behaviors(); 33 | 34 | // 跨区请求 必须先删掉 authenticator 35 | $behaviors['authenticator']; 36 | unset($behaviors['authenticator']); 37 | 38 | $behaviors['corsFilter'] = [ 39 | 'class' => Cors::class, 40 | 'cors' => [ 41 | 'Origin' => ['*'], 42 | 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 43 | 'Access-Control-Request-Headers' => ['*'], 44 | 'Access-Control-Allow-Credentials' => true, 45 | 'Access-Control-Max-Age' => 86400, 46 | 'Access-Control-Expose-Headers' => [], 47 | ] 48 | ]; 49 | $behaviors['authenticator'] = [ 50 | 'class' => JwtHttpBearerAuth::class, 51 | 'optional' => array_merge($this->noAuthActions, ['options']), 52 | ]; 53 | 54 | return $behaviors; 55 | } 56 | 57 | public function actions() 58 | { 59 | $actions = parent::actions(); 60 | $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider']; 61 | return $actions; 62 | } 63 | 64 | /** 65 | * @return \yii\data\ActiveDataProvider 66 | */ 67 | public function prepareDataProvider() 68 | { 69 | $modelClass = $this->modelClass; 70 | 71 | $searchModel = new SearchModel( 72 | [ 73 | 'defaultOrder' => ['id' => SORT_DESC], 74 | 'model' => $modelClass, 75 | 'scenario' => 'default', 76 | 'pageSize' => $this->getPageSize() 77 | ] 78 | ); 79 | 80 | return $searchModel->search(['SearchModel' => Yii::$app->request->queryParams]); 81 | } 82 | 83 | /** 84 | * @return int 85 | */ 86 | protected function getPageSize() 87 | { 88 | if ($pageSize = (int)request('pageSize')) { 89 | if ($pageSize < self::MAX_PAGE_SIZE) { 90 | return $pageSize; 91 | } 92 | return self::MAX_PAGE_SIZE; 93 | } 94 | return self::DEFAULT_PAGE_SIZE; 95 | } 96 | 97 | 98 | /** 99 | * @param Model $model 100 | * @param array $params 101 | * @return Model 102 | * @throws InvalidArgumentException 103 | */ 104 | public function validate(Model $model, array $params): Model 105 | { 106 | $model->load($params, ''); 107 | if (!$model->validate()) { 108 | throw new InvalidArgumentException(Setup::errorMessage($model->firstErrors)); 109 | } 110 | return $model; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /config/common.php: -------------------------------------------------------------------------------- 1 | env('APP_TIME_ZONE'), 5 | 'language' => env('APP_LANGUAGE'), 6 | 'name' => env('APP_NAME'), 7 | 'bootstrap' => ['log', 'ideHelper', \app\core\EventBootstrap::class], 8 | 'components' => [ 9 | 'ideHelper' => [ 10 | 'class' => 'Mis\IdeHelper\IdeHelper', 11 | 'configFiles' => [ 12 | 'config/web.php', 13 | 'config/common.php', 14 | 'config/console.php', 15 | ], 16 | ], 17 | 'requestId' => [ 18 | 'class' => \yiier\helpers\RequestId::class, 19 | ], 20 | 'cache' => [ 21 | 'class' => 'yii\caching\FileCache', 22 | ], 23 | 'db' => [ 24 | 'class' => 'yii\db\Connection', 25 | 'dsn' => env('DB_DSN'), 26 | 'username' => env('DB_USERNAME'), 27 | 'password' => env('DB_PASSWORD'), 28 | 'tablePrefix' => env('DB_TABLE_PREFIX'), 29 | 'charset' => 'utf8mb4', 30 | 'enableSchemaCache' => YII_ENV_PROD, 31 | 'schemaCacheDuration' => 60, 32 | 'schemaCache' => 'cache', 33 | ], 34 | 'i18n' => [ 35 | 'translations' => [ 36 | 'app*' => [ 37 | 'class' => 'yii\i18n\PhpMessageSource', 38 | 'basePath' => '@app/core/messages', 39 | 'fileMap' => [ 40 | 'app' => 'app.php', 41 | 'app/error' => 'exception.php', 42 | ], 43 | ], 44 | ], 45 | ], 46 | 'log' => [ 47 | 'traceLevel' => YII_DEBUG ? 3 : 0, 48 | 'targets' => [ 49 | /** 50 | * 错误级别日志:当某些需要立马解决的致命问题发生的时候,调用此方法记录相关信息。 51 | * 使用方法:Yii::error() 52 | */ 53 | [ 54 | 'class' => 'yiier\helpers\FileTarget', 55 | // 日志等级 56 | 'levels' => ['error'], 57 | // 被收集记录的额外数据 58 | 'logVars' => ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION'], 59 | // 指定日志保存的文件名 60 | 'logFile' => '@app/runtime/logs/error/app.log', 61 | // 是否开启日志 (@app/runtime/logs/error/20151223_app.log) 62 | 'enableDatePrefix' => true, 63 | ], 64 | /** 65 | * 警告级别日志:当某些期望之外的事情发生的时候,使用该方法。 66 | * 使用方法:Yii::warning() 67 | */ 68 | [ 69 | 'class' => 'yiier\helpers\FileTarget', 70 | // 日志等级 71 | 'levels' => ['warning'], 72 | // 被收集记录的额外数据 73 | 'logVars' => ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION'], 74 | // 指定日志保存的文件名 75 | 'logFile' => '@app/runtime/logs/warning/app.log', 76 | // 是否开启日志 (@app/runtime/logs/warning/20151223_app.log) 77 | 'enableDatePrefix' => true, 78 | ], 79 | [ 80 | 'class' => 'yiier\helpers\FileTarget', 81 | 'levels' => ['info'], 82 | 'categories' => ['request'], 83 | 'logVars' => [], 84 | 'maxFileSize' => 1024, 85 | 'logFile' => '@app/runtime/logs/request/app.log', 86 | 'enableDatePrefix' => true 87 | ], 88 | [ 89 | 'class' => 'yiier\helpers\FileTarget', 90 | 'levels' => ['warning'], 91 | 'categories' => ['debug'], 92 | 'logVars' => [], 93 | 'maxFileSize' => 1024, 94 | 'logFile' => '@app/runtime/logs/debug/app.log', 95 | 'enableDatePrefix' => true 96 | ], 97 | ], 98 | ], 99 | ], 100 | ]; 101 | -------------------------------------------------------------------------------- /core/models/User.php: -------------------------------------------------------------------------------- 1 | TimestampBehavior::class, 47 | 'value' => date('Y-m-d H:i:s'), 48 | ], 49 | ]; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function rules() 56 | { 57 | return [ 58 | ['status', 'default', 'value' => UserStatus::ACTIVE], 59 | ['status', 'in', 'range' => [UserStatus::ACTIVE, UserStatus::UNACTIVATED]], 60 | [['username'], 'string', 'max' => 60], 61 | ]; 62 | } 63 | 64 | /** 65 | * @inheritdoc 66 | */ 67 | public static function findIdentity($id) 68 | { 69 | return static::find() 70 | ->where(['id' => $id, 'status' => UserStatus::ACTIVE]) 71 | ->limit(1) 72 | ->one(); 73 | } 74 | 75 | 76 | /** 77 | * @param mixed $token 78 | * @param null $type 79 | * @return void|IdentityInterface 80 | */ 81 | public static function findIdentityByAccessToken($token, $type = null) 82 | { 83 | $userId = (string)$token->getClaim('id'); 84 | return self::findIdentity($userId); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function getId() 91 | { 92 | return $this->getPrimaryKey(); 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function getAuthKey() 99 | { 100 | return $this->auth_key; 101 | } 102 | 103 | /** 104 | * @inheritdoc 105 | */ 106 | public function validateAuthKey($authKey) 107 | { 108 | return $this->getAuthKey() === $authKey; 109 | } 110 | 111 | /** 112 | * Validates password 113 | * 114 | * @param string $password password to validate 115 | * @return bool if password provided is valid for current user 116 | */ 117 | public function validatePassword($password) 118 | { 119 | return Yii::$app->security->validatePassword($password, $this->password_hash); 120 | } 121 | 122 | /** 123 | * Generates password hash from password and sets it to the model 124 | * 125 | * @param string $password 126 | * @throws \yii\base\Exception 127 | */ 128 | public function setPassword($password) 129 | { 130 | $this->password_hash = Yii::$app->security->generatePasswordHash($password); 131 | } 132 | 133 | /** 134 | * Generates "remember me" authentication key 135 | * @throws \yii\base\Exception 136 | */ 137 | public function generateAuthKey() 138 | { 139 | $this->auth_key = Yii::$app->security->generateRandomString(); 140 | } 141 | 142 | /** 143 | * Generates new password reset token 144 | * @throws \yii\base\Exception 145 | */ 146 | public function generatePasswordResetToken() 147 | { 148 | $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time(); 149 | } 150 | 151 | /** 152 | * Removes password reset token 153 | */ 154 | public function removePasswordResetToken() 155 | { 156 | $this->password_reset_token = null; 157 | } 158 | 159 | /** 160 | * @return array 161 | */ 162 | public function fields() 163 | { 164 | $fields = parent::fields(); 165 | unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']); 166 | 167 | $fields['created_at'] = function (self $model) { 168 | return DateHelper::datetimeToIso8601($model->created_at); 169 | }; 170 | 171 | $fields['updated_at'] = function (self $model) { 172 | return DateHelper::datetimeToIso8601($model->updated_at); 173 | }; 174 | 175 | return $fields; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /requirements.php: -------------------------------------------------------------------------------- 1 | Error\n\n" 34 | . "

The path to yii framework seems to be incorrect.

\n" 35 | . '

You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . ".

\n" 36 | . '

Please refer to the README on how to install Yii.

\n"; 37 | 38 | if (!empty($_SERVER['argv'])) { 39 | // do not print HTML when used in console mode 40 | echo strip_tags($message); 41 | } else { 42 | echo $message; 43 | } 44 | exit(1); 45 | } 46 | 47 | require_once($frameworkPath . '/requirements/YiiRequirementChecker.php'); 48 | $requirementsChecker = new YiiRequirementChecker(); 49 | 50 | $gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.'; 51 | $gdOK = $imagickOK = false; 52 | 53 | if (extension_loaded('imagick')) { 54 | $imagick = new Imagick(); 55 | $imagickFormats = $imagick->queryFormats('PNG'); 56 | if (in_array('PNG', $imagickFormats)) { 57 | $imagickOK = true; 58 | } else { 59 | $imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.'; 60 | } 61 | } 62 | 63 | if (extension_loaded('gd')) { 64 | $gdInfo = gd_info(); 65 | if (!empty($gdInfo['FreeType Support'])) { 66 | $gdOK = true; 67 | } else { 68 | $gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.'; 69 | } 70 | } 71 | 72 | /** 73 | * Adjust requirements according to your application specifics. 74 | */ 75 | $requirements = array( 76 | // Database : 77 | array( 78 | 'name' => 'PDO extension', 79 | 'mandatory' => true, 80 | 'condition' => extension_loaded('pdo'), 81 | 'by' => 'All DB-related classes', 82 | ), 83 | array( 84 | 'name' => 'PDO SQLite extension', 85 | 'mandatory' => false, 86 | 'condition' => extension_loaded('pdo_sqlite'), 87 | 'by' => 'All DB-related classes', 88 | 'memo' => 'Required for SQLite database.', 89 | ), 90 | array( 91 | 'name' => 'PDO MySQL extension', 92 | 'mandatory' => false, 93 | 'condition' => extension_loaded('pdo_mysql'), 94 | 'by' => 'All DB-related classes', 95 | 'memo' => 'Required for MySQL database.', 96 | ), 97 | array( 98 | 'name' => 'PDO PostgreSQL extension', 99 | 'mandatory' => false, 100 | 'condition' => extension_loaded('pdo_pgsql'), 101 | 'by' => 'All DB-related classes', 102 | 'memo' => 'Required for PostgreSQL database.', 103 | ), 104 | // Cache : 105 | array( 106 | 'name' => 'Memcache extension', 107 | 'mandatory' => false, 108 | 'condition' => extension_loaded('memcache') || extension_loaded('memcached'), 109 | 'by' => 'MemCache', 110 | 'memo' => extension_loaded('memcached') ? 'To use memcached set MemCache::useMemcached to true.' : '' 111 | ), 112 | // CAPTCHA: 113 | array( 114 | 'name' => 'GD PHP extension with FreeType support', 115 | 'mandatory' => false, 116 | 'condition' => $gdOK, 117 | 'by' => 'Captcha', 118 | 'memo' => $gdMemo, 119 | ), 120 | array( 121 | 'name' => 'ImageMagick PHP extension with PNG support', 122 | 'mandatory' => false, 123 | 'condition' => $imagickOK, 124 | 'by' => 'Captcha', 125 | 'memo' => $imagickMemo, 126 | ), 127 | // PHP ini : 128 | 'phpExposePhp' => array( 129 | 'name' => 'Expose PHP', 130 | 'mandatory' => false, 131 | 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"), 132 | 'by' => 'Security reasons', 133 | 'memo' => '"expose_php" should be disabled at php.ini', 134 | ), 135 | 'phpAllowUrlInclude' => array( 136 | 'name' => 'PHP allow url include', 137 | 'mandatory' => false, 138 | 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"), 139 | 'by' => 'Security reasons', 140 | 'memo' => '"allow_url_include" should be disabled at php.ini', 141 | ), 142 | 'phpSmtp' => array( 143 | 'name' => 'PHP mail SMTP', 144 | 'mandatory' => false, 145 | 'condition' => strlen(ini_get('SMTP')) > 0, 146 | 'by' => 'Email sending', 147 | 'memo' => 'PHP mail SMTP server required', 148 | ), 149 | ); 150 | 151 | // OPcache check 152 | if (!version_compare(phpversion(), '5.5', '>=')) { 153 | $requirements[] = array( 154 | 'name' => 'APC extension', 155 | 'mandatory' => false, 156 | 'condition' => extension_loaded('apc'), 157 | 'by' => 'ApcCache', 158 | ); 159 | } 160 | 161 | $result = $requirementsChecker->checkYii()->check($requirements)->getResult(); 162 | $requirementsChecker->render(); 163 | exit($result['summary']['errors'] === 0 ? 0 : 1); 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Yii 2 REST API Project Template

6 |
7 |

8 | 9 | Yii 2 REST API Project Template is a skeleton [Yii 2](http://www.yiiframework.com/) application best for 10 | rapidly creating small rest api projects. 11 | 12 | The template contains the basic features including user join/login api. 13 | It includes all commonly used configurations that would allow you to focus on adding new 14 | features to your application. 15 | 16 | [![Testing](https://github.com/forecho/yii2-rest-api/workflows/Testing/badge.svg)](https://github.com/forecho/yii2-rest-api/actions) 17 | [![Lint](https://github.com/forecho/yii2-rest-api/workflows/Lint/badge.svg)](https://github.com/forecho/yii2-rest-api/actions) 18 | [![Code Coverage](https://scrutinizer-ci.com/g/forecho/yii2-rest-api/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/forecho/yii2-rest-api/?branch=master) 19 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/forecho/yii2-rest-api/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/forecho/yii2-rest-api/?branch=master) 20 | [![Latest Stable Version](https://poser.pugx.org/forecho/yii2-rest-api/v/stable)](https://packagist.org/packages/forecho/yii2-rest-api) 21 | [![Total Downloads](https://poser.pugx.org/forecho/yii2-rest-api/downloads)](https://packagist.org/packages/forecho/yii2-rest-api) 22 | [![Latest Unstable Version](https://poser.pugx.org/forecho/yii2-rest-api/v/unstable)](https://packagist.org/packages/forecho/yii2-rest-api) 23 | [![License](https://poser.pugx.org/forecho/yii2-rest-api/license)](https://packagist.org/packages/forecho/yii2-rest-api) 24 | 25 | REQUIREMENTS 26 | ------------ 27 | 28 | The minimum requirement by this project template that your Web server supports PHP 7.2.0. 29 | 30 | INSTALLATION 31 | ------------ 32 | 33 | ### Install via Composer 34 | 35 | If you do not have [Composer](http://getcomposer.org/), you may install it by following the instructions 36 | at [getcomposer.org](http://getcomposer.org/doc/00-intro.md#installation-nix). 37 | 38 | You can then install this project template using the following command: 39 | 40 | ~~~ 41 | composer create-project --prefer-dist forecho/yii2-rest-api 42 | cd 43 | cp .env.example .env 44 | php yii generate/key # optional 45 | chmod 777 -R runtime/ 46 | ~~~ 47 | 48 | Now you should be able to access the application through the following URL, assuming `rest-api` is the directory 49 | directly under the Web root. 50 | 51 | ~~~ 52 | http://localhost//web/ 53 | ~~~ 54 | 55 | ### Install from GitHub 56 | 57 | Accessing [Use this template](https://github.com/forecho/yii2-rest-api/generate) Create a new repository from yii2-rest-api 58 | 59 | ```sh 60 | git clone xxxx 61 | cd 62 | cp .env.example .env 63 | chmod 777 -R runtime/ 64 | ``` 65 | 66 | You can then access the application through the following URL: 67 | 68 | ~~~ 69 | http://localhost//web/ 70 | ~~~ 71 | 72 | 73 | ### Install with Docker 74 | 75 | Update your vendor packages 76 | 77 | ```sh 78 | docker-compose run --rm php composer update --prefer-dist 79 | ``` 80 | 81 | Run the installation triggers (creating cookie validation code) 82 | 83 | ```sh 84 | docker-compose run --rm php composer install 85 | ``` 86 | 87 | Start the container 88 | 89 | ```sh 90 | docker-compose up -d 91 | ``` 92 | 93 | You can then access the application through the following URL: 94 | 95 | ``` 96 | http://127.0.0.1:8000 97 | ``` 98 | 99 | **NOTES:** 100 | - Minimum required Docker engine version `17.04` for development (see [Performance tuning for volume mounts](https://docs.docker.com/docker-for-mac/osxfs-caching/)) 101 | - The default configuration uses a host-volume in your home directory `.docker-composer` for composer caches 102 | 103 | Check out the packages 104 | ------------ 105 | 106 | - [yiithings/yii2-doten](https://github.com/yiithings/yii2-doten) 107 | - [sizeg/yii2-jwt](https://github.com/sizeg/yii2-jwt) 108 | - [yiier/yii2-helpers](https://github.com/yiier/yii2-helpers) 109 | 110 | Use 111 | ------------ 112 | 113 | At this time, you have a RESTful API server running at `http://127.0.0.1:8000`. It provides the following endpoints: 114 | 115 | * `GET /health-check`: a health check service provided for health checking purpose (needed when implementing a server cluster) 116 | * `POST /v1/join`: create a user 117 | * `POST /v1/login`: authenticates a user and generates a JWT 118 | * `POST /v1/refresh-token`: refresh a JWT 119 | 120 | Try the URL `http://localhost:8000/health-check` in a browser, and you should see something like `{"code":0,"data":"OK","message":"成功"}` displayed. 121 | 122 | If you have `cURL` or some API client tools (e.g. [Postman](https://www.getpostman.com/)), you may try the following 123 | more complex scenarios: 124 | 125 | ```shell 126 | # create a user via: POST /v1/join 127 | curl -X POST -H "Content-Type: application/json" -d '{"username":"demo","email":"demo@email.com","password":"pass123"}' http://localhost:8000/v1/join 128 | # should return like: {"code":0,"data":{"username":"demo","email":"demo@email.com","status":1,"created_at":"2020-07-18T16:38:11+08:00","updated_at":"2020-07-18T16:38:11+08:00","id":17},"message":"成功"} 129 | 130 | # authenticate the user via: POST /v1/login 131 | curl -X POST -H "Content-Type: application/json" -d '{"username": "demo", "password": "pass123"}' http://localhost:8000/v1/login 132 | # should return like: {"code":0,"data":{"user":{"id":4,"username":"dem211o1","avatar":"","email":"de21mo1@mail.com","status":1,"created_at":"2020-07-17T23:49:39+08:00","updated_at":"2020-07-17T23:49:39+08:00"},"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IllpaS1SRVNULUFQSSJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3QiLCJqdGkiOiJZaWktUkVTVC1BUEkiLCJpYXQiOjE1OTUwNjQ5NzIsImV4cCI6MTU5NTMyNDE3MiwidXNlcm5hbWUiOiJkZW0yMTFvMSIsImlkIjo0fQ.y2NSVQe-TQ08RnXnF-o55h905G9WHo6GYHNaUWlKjDE"},"message":"成功"} 133 | 134 | # refresh a JWT 135 | curl -X POST -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IllpaS1SRVNULUFQSSJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3QiLCJqdGkiOiJZaWktUkVTVC1BUEkiLCJpYXQiOjE1OTUwNjQ5NzIsImV4cCI6MTU5NTMyNDE3MiwidXNlcm5hbWUiOiJkZW0yMTFvMSIsImlkIjo0fQ.y2NSVQe-TQ08RnXnF-o55h905G9WHo6GYHNaUWlKjDE' http://localhost:8000/v1/refresh-token 136 | # should return like: {"code":0,"data":{"user":{"id":4,"username":"dem211o1","avatar":"","email":"de21mo1@mail.com","status":1,"created_at":"2020-07-17T23:49:39+08:00","updated_at":"2020-07-17T23:49:39+08:00"},"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IllpaS1SRVNULUFQSSJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3QiLCJqdGkiOiJZaWktUkVTVC1BUEkiLCJpYXQiOjE1OTUwNjQ5NzIsImV4cCI6MTU5NTMyNDE3MiwidXNlcm5hbWUiOiJkZW0yMTFvMSIsImlkIjo0fQ.y2NSVQe-TQ08RnXnF-o55h905G9WHo6GYHNaUWlKjDE"},"message":"成功"} 137 | ``` 138 | --------------------------------------------------------------------------------