├── public
├── js
│ ├── app.js
│ └── article.js
├── robots.txt
├── img
│ ├── yii2-app.gif
│ └── yii2-vue.gif
├── css
│ ├── article.css
│ └── app.css
├── .htaccess
└── index.php
├── .dockerignore
├── tests
├── _data
│ ├── .gitkeep
│ └── user.php
├── Unit
│ ├── _bootstrap.php
│ ├── Factory
│ │ ├── ArticleMetaFactoryTest.php
│ │ └── SessionFactoryTest.php
│ ├── Helper
│ │ └── DbHelperTest.php
│ ├── Console
│ │ └── Job
│ │ │ └── HelloJobTest.php
│ └── Validator
│ │ └── UrlValidatorTest.php
├── Acceptance
│ ├── _bootstrap.php
│ └── HomeCest.php
├── Functional
│ ├── _bootstrap.php
│ └── Form
│ │ └── LoginFormTest.php
├── _output
│ └── .gitignore
├── _support
│ ├── _generated
│ │ └── .gitignore
│ ├── Helper
│ │ ├── Unit.php
│ │ ├── Acceptance.php
│ │ └── Functional.php
│ ├── UnitTester.php
│ ├── AcceptanceTester.php
│ └── FunctionalTester.php
├── TestCase.php
├── Fixture
│ └── UserFixture.php
├── functional.suite.yml
├── unit.suite.yml
├── _bootstrap.php
├── acceptance.suite.yml
├── config.php
└── codeception.yml
├── app
├── Exception
│ └── .gitkeep
├── Trait
│ └── .gitkeep
├── Console
│ ├── Model
│ │ └── .gitkeep
│ ├── Controller
│ │ ├── RequirementController.php
│ │ ├── GcController.php
│ │ ├── Controller.php
│ │ └── HelloController.php
│ └── Job
│ │ └── HelloJob.php
├── Http
│ ├── Api
│ │ ├── Controller
│ │ │ └── .gitkeep
│ │ ├── Backend
│ │ │ ├── Model
│ │ │ │ ├── AuthItem.php
│ │ │ │ ├── Setting.php
│ │ │ │ ├── Article.php
│ │ │ │ ├── ArticleCategory.php
│ │ │ │ ├── Permission.php
│ │ │ │ ├── Session.php
│ │ │ │ ├── Role.php
│ │ │ │ └── User.php
│ │ │ ├── Module
│ │ │ │ └── V1
│ │ │ │ │ ├── Controller
│ │ │ │ │ ├── MyController.php
│ │ │ │ │ ├── RoleController.php
│ │ │ │ │ ├── UserController.php
│ │ │ │ │ ├── ArticleController.php
│ │ │ │ │ ├── SessionController.php
│ │ │ │ │ ├── SettingController.php
│ │ │ │ │ ├── UploadController.php
│ │ │ │ │ ├── MySessionController.php
│ │ │ │ │ ├── PermissionController.php
│ │ │ │ │ └── ArticleCategoryController.php
│ │ │ │ │ └── Module.php
│ │ │ ├── Controller
│ │ │ │ ├── ActiveController.php
│ │ │ │ ├── RoleController.php
│ │ │ │ ├── Controller.php
│ │ │ │ ├── MyController.php
│ │ │ │ ├── MySessionController.php
│ │ │ │ ├── PermissionController.php
│ │ │ │ ├── AuthItemController.php
│ │ │ │ ├── ArticleCategoryController.php
│ │ │ │ ├── SettingController.php
│ │ │ │ ├── UploadController.php
│ │ │ │ ├── ArticleController.php
│ │ │ │ ├── SessionController.php
│ │ │ │ ├── UserController.php
│ │ │ │ └── ControllerTrait.php
│ │ │ ├── Module.php
│ │ │ └── Form
│ │ │ │ ├── UserForm.php
│ │ │ │ ├── LogoutForm.php
│ │ │ │ ├── MyInfoForm.php
│ │ │ │ ├── ChangePasswordForm.php
│ │ │ │ └── LoginForm.php
│ │ ├── Frontend
│ │ │ ├── Module
│ │ │ │ └── V1
│ │ │ │ │ ├── Controller
│ │ │ │ │ ├── ArticleController.php
│ │ │ │ │ └── ArticleCommentController.php
│ │ │ │ │ └── Module.php
│ │ │ ├── Controller
│ │ │ │ ├── ActiveController.php
│ │ │ │ ├── Controller.php
│ │ │ │ ├── ArticleCommentController.php
│ │ │ │ ├── ArticleController.php
│ │ │ │ └── ControllerTrait.php
│ │ │ └── Module.php
│ │ ├── Form
│ │ │ ├── ArticleDislikeForm.php
│ │ │ ├── UserTrait.php
│ │ │ ├── ArticleLikeForm.php
│ │ │ └── BaseArticleLikeForm.php
│ │ ├── Model
│ │ │ ├── ArticleComment.php
│ │ │ └── Article.php
│ │ └── Module.php
│ ├── Web
│ │ └── ErrorHandler.php
│ ├── Controller
│ │ └── Controller.php
│ ├── Rest
│ │ ├── CreateAction.php
│ │ ├── UpdateAction.php
│ │ ├── Controller.php
│ │ ├── OptionsAction.php
│ │ ├── Serializer.php
│ │ ├── SafeActionTrait.php
│ │ └── DeleteAction.php
│ ├── User
│ │ └── LogoutInterface.php
│ ├── Filter
│ │ ├── LanguagePickerInterface.php
│ │ ├── Auth
│ │ │ └── Authenticator.php
│ │ └── ContentNegotiator.php
│ ├── Form
│ │ ├── UploadForm.php
│ │ ├── VerifyEmailForm.php
│ │ ├── ResetPasswordForm.php
│ │ ├── ContactForm.php
│ │ ├── PasswordResetRequestForm.php
│ │ └── ResendVerificationEmailForm.php
│ ├── Asset
│ │ └── AppAsset.php
│ └── Widget
│ │ └── Alert.php
├── Form
│ ├── Form.php
│ ├── BaseForm.php
│ └── FormTrait.php
├── Queue
│ ├── JobPreventedException.php
│ ├── JobEvent.php
│ ├── Gii
│ │ └── Generator.php
│ ├── RetryableTrait.php
│ └── Job.php
├── Rbac
│ └── Rule
│ │ ├── Rule.php
│ │ ├── ArticleDeleteRule.php
│ │ ├── RoleDeleteRule.php
│ │ └── UserDeleteRule.php
├── Db
│ └── ActiveQuery.php
├── Model
│ ├── ActiveRecord.php
│ ├── SoftDeleteInterface.php
│ ├── StatusInterface.php
│ ├── SoftDeleteTrait.php
│ ├── StatusTrait.php
│ ├── ArticleMeta.php
│ ├── ArticleCategory.php
│ └── ArticleLike.php
├── Validator
│ ├── PasswordValidator.php
│ └── UrlValidator.php
├── Behavior
│ ├── TimestampBehavior.php
│ ├── CreatorBehavior.php
│ ├── ArticleCategoryBehavior.php
│ └── ArticleBehavior.php
├── Factory
│ ├── ArticleLikeFactory.php
│ ├── ArticleMetaFactory.php
│ └── SessionFactory.php
├── Job
│ └── MailJob.php
└── Helper
│ └── DbHelper.php
├── resources
├── docker
│ ├── php
│ │ ├── php.ini
│ │ └── image
│ │ │ └── Dockerfile
│ ├── redis
│ │ └── redis.conf
│ ├── cron
│ │ ├── image
│ │ │ ├── crontab
│ │ │ └── Dockerfile
│ │ └── crontab
│ ├── mysql
│ │ └── conf.d
│ │ │ └── mysqld.cnf
│ ├── queue
│ │ └── image
│ │ │ └── Dockerfile
│ └── nginx
│ │ ├── nginx.conf
│ │ └── conf.d
│ │ └── default.conf
├── mail
│ ├── hello.php
│ ├── emailVerify-text.php
│ ├── passwordResetToken-text.php
│ ├── layouts
│ │ ├── text.php
│ │ └── html.php
│ ├── emailVerify-html.php
│ └── passwordResetToken-html.php
├── environments
│ ├── dev
│ │ ├── config
│ │ │ ├── bootstrap-local.php
│ │ │ ├── params-local.php
│ │ │ ├── test-local.php
│ │ │ ├── console-local.php
│ │ │ ├── web-local.php
│ │ │ └── common-local.php
│ │ └── bin
│ │ │ ├── yii_test.bat
│ │ │ └── yii_test
│ ├── docker
│ │ ├── .env
│ │ ├── config
│ │ │ ├── bootstrap-local.php
│ │ │ ├── test-local.php
│ │ │ ├── params-local.php
│ │ │ ├── console-local.php
│ │ │ ├── web-local.php
│ │ │ └── common-local.php
│ │ └── bin
│ │ │ ├── yii_test.bat
│ │ │ └── yii_test
│ ├── prod
│ │ └── config
│ │ │ ├── bootstrap-local.php
│ │ │ ├── console-local.php
│ │ │ ├── params-local.php
│ │ │ ├── test-local.php
│ │ │ ├── web-local.php
│ │ │ └── common-local.php
│ └── dockerenv
│ │ ├── config
│ │ ├── bootstrap-local.php
│ │ ├── test-local.php
│ │ ├── params-local.php
│ │ ├── console-local.php
│ │ ├── web-local.php
│ │ └── common-local.php
│ │ └── bin
│ │ ├── yii_test.bat
│ │ └── yii_test
├── gii
│ └── generators
│ │ └── module
│ │ └── views
│ │ ├── form.php
│ │ ├── view.php
│ │ ├── controller.php
│ │ └── module.php
├── queue
│ └── gii
│ │ └── views
│ │ ├── form.php
│ │ └── job.php
├── views
│ ├── article
│ │ ├── comments.php
│ │ ├── index.php
│ │ └── view.php
│ └── site
│ │ ├── error.php
│ │ ├── about.php
│ │ ├── resetPassword.php
│ │ ├── resendVerificationEmail.php
│ │ ├── requestPasswordResetToken.php
│ │ ├── signup.php
│ │ ├── contact.php
│ │ ├── login.php
│ │ └── index.php
├── migrations
│ ├── m190822_022858_table_article_meta.php
│ ├── m190822_070400_data_article_category.php
│ ├── m190829_053119_table_article_like.php
│ ├── m190822_034841_table_article_category.php
│ ├── m190724_031648_data_user.php
│ ├── m190822_070404_data_article.php
│ ├── m190829_093604_article_comment.php
│ ├── m190722_062914_table_session.php
│ ├── m190617_031446_table_user.php
│ └── m190731_024219_table_article.php
├── messages
│ ├── zh-CN
│ │ └── app.php
│ └── zh-TW
│ │ └── app.php
└── console
│ └── views
│ └── migration.php
├── config
├── .gitignore
├── debug.php
├── bootstrap.php
├── test.php
├── gii.php
├── params.php
├── console.php
└── routes.php
├── .bowerrc
├── .styleci.yml
├── conf
├── crontab
├── queue.service
├── app.service
└── nginx.conf
├── .scrutinizer.yml
├── bin
├── init.bat
├── yii.bat
└── yii
├── Dockerfile
├── codeception.dist.yml
├── .gitignore
├── .travis.yml
├── .github
└── workflows
│ └── tencent.yml
├── LICENSE.md
├── docs
└── en
│ ├── INSTALLATION.md
│ └── DOCKER.md
└── composer.json
/public/js/app.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | vendor/
--------------------------------------------------------------------------------
/tests/_data/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/Exception/.gitkeep:
--------------------------------------------------------------------------------
1 | *
2 |
--------------------------------------------------------------------------------
/app/Trait/.gitkeep:
--------------------------------------------------------------------------------
1 | *
2 |
--------------------------------------------------------------------------------
/app/Console/Model/.gitkeep:
--------------------------------------------------------------------------------
1 | *
2 |
--------------------------------------------------------------------------------
/app/Http/Api/Controller/.gitkeep:
--------------------------------------------------------------------------------
1 | *
2 |
--------------------------------------------------------------------------------
/tests/Unit/_bootstrap.php:
--------------------------------------------------------------------------------
1 |
3 |
4 | Hello = $name ?>
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr2
2 |
3 | finder:
4 | exclude:
5 | - docs
6 | - vendor
--------------------------------------------------------------------------------
/resources/docker/cron/image/crontab:
--------------------------------------------------------------------------------
1 | * * * * * /usr/local/bin/php /app/bin/yii hello
2 |
--------------------------------------------------------------------------------
/public/img/yii2-app.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razonyang/yii2-app-template/HEAD/public/img/yii2-app.gif
--------------------------------------------------------------------------------
/public/img/yii2-vue.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razonyang/yii2-app-template/HEAD/public/img/yii2-vue.gif
--------------------------------------------------------------------------------
/app/Form/Form.php:
--------------------------------------------------------------------------------
1 | \yii\debug\Module::class,
5 | 'panels' => [
6 | 'queue' => \yii\queue\debug\Panel::class,
7 | ],
8 | ];
9 |
--------------------------------------------------------------------------------
/resources/docker/mysql/conf.d/mysqld.cnf:
--------------------------------------------------------------------------------
1 | [mysqld]
2 | sql-mode=ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
--------------------------------------------------------------------------------
/app/Http/Web/ErrorHandler.php:
--------------------------------------------------------------------------------
1 | ['gii'],
4 | 'modules' => [
5 | 'gii' => 'yii\gii\Module',
6 | ],
7 | ];
8 |
--------------------------------------------------------------------------------
/app/Http/Controller/Controller.php:
--------------------------------------------------------------------------------
1 | 'admin@example.com',
4 | 'supportEmail' => 'support@example.com',
5 |
6 | 'backend.url' => '',
7 | ];
8 |
--------------------------------------------------------------------------------
/resources/environments/docker/config/test-local.php:
--------------------------------------------------------------------------------
1 | [
4 | 'db' => [
5 | 'dsn' => 'mysql:host=db;dbname=yiitest',
6 | ],
7 | ],
8 | ];
9 |
--------------------------------------------------------------------------------
/resources/environments/prod/config/params-local.php:
--------------------------------------------------------------------------------
1 | 'admin@example.com',
4 | 'supportEmail' => 'support@example.com',
5 |
6 | 'backend.url' => '',
7 | ];
8 |
--------------------------------------------------------------------------------
/resources/environments/prod/config/test-local.php:
--------------------------------------------------------------------------------
1 | [
4 | 'db' => [
5 | 'dsn' => 'mysql:host=db;dbname=yii_test',
6 | ],
7 | ],
8 | ];
9 |
--------------------------------------------------------------------------------
/resources/environments/dev/config/test-local.php:
--------------------------------------------------------------------------------
1 | [
4 | 'db' => [
5 | 'dsn' => 'mysql:host=localhost;dbname=yiitest',
6 | ],
7 | ],
8 | ];
9 |
--------------------------------------------------------------------------------
/resources/environments/docker/config/params-local.php:
--------------------------------------------------------------------------------
1 | 'admin@example.com',
4 | 'supportEmail' => 'support@example.com',
5 |
6 | 'backend.url' => '',
7 | ];
8 |
--------------------------------------------------------------------------------
/resources/environments/dockerenv/config/test-local.php:
--------------------------------------------------------------------------------
1 | [
4 | 'db' => [
5 | 'dsn' => 'mysql:host=db;dbname=yiitest',
6 | ],
7 | ],
8 | ];
9 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Model/Article.php:
--------------------------------------------------------------------------------
1 | getenv('MAILER_USER'),
4 | 'supportEmail' => getenv('MAILER_USER'),
5 |
6 | 'backend.url' => getenv('BACKEND_URL'),
7 | ];
8 |
--------------------------------------------------------------------------------
/tests/Fixture/UserFixture.php:
--------------------------------------------------------------------------------
1 | amOnPage(Url::toRoute('/'));
10 | $I->see('Congratulation!');
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/functional.suite.yml:
--------------------------------------------------------------------------------
1 | path: Functional
2 | actor: FunctionalTester
3 | modules:
4 | enabled:
5 | # add a framework module here
6 | - \Helper\Functional
7 | - Yii2:
8 | configFile: "tests/config.php"
9 | step_decorators: ~
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Module/V1/Controller/PermissionController.php:
--------------------------------------------------------------------------------
1 | [
5 | 'request' => [
6 | // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
7 | 'cookieValidationKey' => '',
8 | ],
9 | ],
10 | ];
11 |
12 | return $config;
13 |
--------------------------------------------------------------------------------
/tests/acceptance.suite.yml:
--------------------------------------------------------------------------------
1 | path: Acceptance
2 | actor: AcceptanceTester
3 | modules:
4 | enabled:
5 | - PhpBrowser:
6 | url: http://localhost:8080
7 | - \Helper\Acceptance
8 | - Yii2:
9 | configFile: "tests/config.php"
10 | part: init
11 | step_decorators: ~
12 | coverage:
13 | enabled: false
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Model/Permission.php:
--------------------------------------------------------------------------------
1 | Item::TYPE_PERMISSION]
12 | ], parent::rules());
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/resources/mail/emailVerify-text.php:
--------------------------------------------------------------------------------
1 | urlManager->createAbsoluteUrl(['site/verify-email', 'token' => $user->verification_token]);
7 | ?>
8 | Hello = $user->username ?>,
9 |
10 | Follow the link below to verify your email:
11 |
12 | = $verifyLink ?>
13 |
--------------------------------------------------------------------------------
/app/Console/Job/HelloJob.php:
--------------------------------------------------------------------------------
1 | name;
14 | echo $message . PHP_EOL;
15 | Yii::info($message, __METHOD__);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/resources/mail/passwordResetToken-text.php:
--------------------------------------------------------------------------------
1 | urlManager->createAbsoluteUrl(['site/reset-password', 'token' => $user->password_reset_token]);
7 | ?>
8 | Hello = $user->username ?>,
9 |
10 | Follow the link below to reset your password:
11 |
12 | = $resetLink ?>
13 |
--------------------------------------------------------------------------------
/app/Rbac/Rule/ArticleDeleteRule.php:
--------------------------------------------------------------------------------
1 | user_id : false;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/bin/init.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem -------------------------------------------------------------
4 | rem Yii command line init script for Windows.
5 | rem -------------------------------------------------------------
6 |
7 | @setlocal
8 |
9 | set YII_PATH=%~dp0
10 |
11 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
12 |
13 | "%PHP_COMMAND%" "%YII_PATH%init" %*
14 |
15 | @endlocal
16 |
--------------------------------------------------------------------------------
/bin/yii.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem -------------------------------------------------------------
4 | rem Yii command line bootstrap script for Windows.
5 | rem -------------------------------------------------------------
6 |
7 | @setlocal
8 |
9 | set YII_PATH=%~dp0
10 |
11 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
12 |
13 | "%PHP_COMMAND%" "%YII_PATH%yii" %*
14 |
15 | @endlocal
16 |
--------------------------------------------------------------------------------
/app/Factory/ArticleLikeFactory.php:
--------------------------------------------------------------------------------
1 | $articleId,
13 | 'user_id' => $userId,
14 | ]);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/Http/Filter/Auth/Authenticator.php:
--------------------------------------------------------------------------------
1 | getIdentity();
11 |
12 | return $identity ?? parent::authenticate($user, $request, $response);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/resources/gii/generators/module/views/form.php:
--------------------------------------------------------------------------------
1 |
7 |
8 | field($generator, 'moduleClass');
10 | echo $form->field($generator, 'moduleID');
11 | echo $form->field($generator, 'viewPath');
12 | ?>
13 |
14 |
--------------------------------------------------------------------------------
/tests/config.php:
--------------------------------------------------------------------------------
1 | getLike();
11 | if (!$like) {
12 | return;
13 | }
14 |
15 | // deletes record.
16 | $like->delete();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/resources/mail/layouts/text.php:
--------------------------------------------------------------------------------
1 |
9 |
10 | beginPage() ?>
11 | beginBody() ?>
12 | = $content ?>
13 | endBody() ?>
14 | endPage() ?>
15 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Model/Session.php:
--------------------------------------------------------------------------------
1 | ['gii'],
4 | 'components' => [
5 | 'log' => [
6 | 'targets' => [
7 | 'file' => [
8 | 'levels' => ['info', 'trace'],
9 | ],
10 | ],
11 | ],
12 | ],
13 | 'modules' => [
14 | 'gii' => require __DIR__ . '/gii.php',
15 | ],
16 | ];
17 |
--------------------------------------------------------------------------------
/resources/environments/docker/bin/yii_test.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem -------------------------------------------------------------
4 | rem Yii command line bootstrap script for Windows.
5 | rem -------------------------------------------------------------
6 |
7 | @setlocal
8 |
9 | set YII_PATH=%~dp0
10 |
11 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
12 |
13 | "%PHP_COMMAND%" "%YII_PATH%yii_test" %*
14 |
15 | @endlocal
16 |
--------------------------------------------------------------------------------
/resources/environments/docker/config/console-local.php:
--------------------------------------------------------------------------------
1 | ['gii'],
4 | 'components' => [
5 | 'log' => [
6 | 'targets' => [
7 | 'file' => [
8 | 'levels' => ['info', 'trace'],
9 | ],
10 | ],
11 | ],
12 | ],
13 | 'modules' => [
14 | 'gii' => require __DIR__ . '/gii.php',
15 | ],
16 | ];
17 |
--------------------------------------------------------------------------------
/resources/environments/dockerenv/bin/yii_test.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem -------------------------------------------------------------
4 | rem Yii command line bootstrap script for Windows.
5 | rem -------------------------------------------------------------
6 |
7 | @setlocal
8 |
9 | set YII_PATH=%~dp0
10 |
11 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
12 |
13 | "%PHP_COMMAND%" "%YII_PATH%yii_test" %*
14 |
15 | @endlocal
16 |
--------------------------------------------------------------------------------
/resources/environments/dockerenv/config/console-local.php:
--------------------------------------------------------------------------------
1 | ['gii'],
4 | 'components' => [
5 | 'log' => [
6 | 'targets' => [
7 | 'file' => [
8 | 'levels' => ['info', 'trace'],
9 | ],
10 | ],
11 | ],
12 | ],
13 | 'modules' => [
14 | 'gii' => require __DIR__ . '/gii.php',
15 | ],
16 | ];
17 |
--------------------------------------------------------------------------------
/app/Http/Api/Model/ArticleComment.php:
--------------------------------------------------------------------------------
1 | upload();
18 | return [
19 | 'url' => $url,
20 | ];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/resources/docker/queue/image/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.2-cli
2 | RUN apt-get update
3 |
4 | # Intl
5 | RUN apt-get install -y \
6 | zlib1g-dev \
7 | libicu-dev \
8 | g++ \
9 | && docker-php-ext-configure intl \
10 | && docker-php-ext-install intl
11 |
12 | # MySQL
13 | RUN docker-php-ext-install pdo_mysql
14 |
15 | # PostgreSQL
16 | RUN apt-get update && apt-get install -y \
17 | libpq-dev \
18 | && docker-php-ext-install pdo_pgsql
--------------------------------------------------------------------------------
/resources/queue/gii/views/form.php:
--------------------------------------------------------------------------------
1 |
8 | = $form->field($generator, 'jobClass')->textInput(['autofocus' => true]) ?>
9 | = $form->field($generator, 'properties') ?>
10 | = $form->field($generator, 'retryable')->checkbox() ?>
11 | = $form->field($generator, 'ns') ?>
12 | = $form->field($generator, 'baseClass') ?>
13 |
--------------------------------------------------------------------------------
/resources/docker/cron/image/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM razonyang/php-cron:7.2
2 | RUN apt-get update
3 |
4 | # Intl
5 | RUN apt-get install -y \
6 | zlib1g-dev \
7 | libicu-dev \
8 | g++ \
9 | && docker-php-ext-configure intl \
10 | && docker-php-ext-install intl
11 |
12 | # MySQL
13 | RUN docker-php-ext-install pdo_mysql
14 |
15 | # PostgreSQL
16 | RUN apt-get update && apt-get install -y \
17 | libpq-dev \
18 | && docker-php-ext-install pdo_pgsql
--------------------------------------------------------------------------------
/app/Rbac/Rule/RoleDeleteRule.php:
--------------------------------------------------------------------------------
1 | getAuthManager();
14 | $roles = $auth->getRolesByUser($user);
15 | $roleName = $params['model']->name;
16 | return isset($roles[$roleName]) ? false : true;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/conf/queue.service:
--------------------------------------------------------------------------------
1 | # systemd service unit
2 |
3 | [Unit]
4 | Description=App Queue Worker %I
5 | After=network.target
6 | # the following two lines only apply if your queue backend is mysql
7 | # replace this with the service that powers your backend
8 | After=mysql.service
9 | Requires=mysql.service
10 |
11 | [Service]
12 | #User=nginx
13 | #Group=nginx
14 | ExecStart=/usr/bin/php /app/bin/yii queue/listen --verbose
15 | Restart=on-failure
16 |
17 | [Install]
18 | WantedBy=multi-user.target
19 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM yiisoftware/yii2-php:7.4-apache
2 |
3 | ENV APACHE_DOCUMENT_ROOT /app/public
4 |
5 | RUN sed -ri -e 's!/app/web!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
6 |
7 | COPY --chown=www-data:www-data . /app
8 |
9 | RUN git config --global url."git@github.com:".insteadOf "https://github.com/"
10 |
11 | RUN composer install --prefer-dist --no-dev --optimize-autoloader
12 |
13 | RUN /app/bin/init --env=DockerEnv --overwrite=No
14 |
15 | RUN chown -R www-data:www-data /app/runtime
16 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/RoleController.php:
--------------------------------------------------------------------------------
1 | getResponse()->setStatusCode($this->statusCode);
17 |
18 | parent::run($id);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/resources/mail/emailVerify-html.php:
--------------------------------------------------------------------------------
1 | urlManager->createAbsoluteUrl(['site/verify-email', 'token' => $user->verification_token]);
8 | ?>
9 |
10 |
Hello = Html::encode($user->username) ?>,
11 |
12 |
Follow the link below to verify your email:
13 |
14 |
= Html::a(Html::encode($verifyLink), $verifyLink) ?>
15 |
16 |
--------------------------------------------------------------------------------
/app/Http/Api/Frontend/Module.php:
--------------------------------------------------------------------------------
1 | getResponse();
20 | $response->format = 'json';
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/_data/user.php:
--------------------------------------------------------------------------------
1 | 'bayer.hudson',
6 | 'auth_key' => 'HP187Mvq7Mmm3CTU80dLkGmni_FUH_lR',
7 | //password_0
8 | 'password_hash' => '$2y$13$EjaPFBnZOQsHdGuHI.xvhuDp1fHpo8hKRSk6yshqa9c5EG8s3C3lO',
9 | 'password_reset_token' => 'ExzkCOaYc1L8IOBs4wdTGGbgNiG3Wz1I_1402312317',
10 | 'create_time' => '1402312317',
11 | 'update_time' => '1402312317',
12 | 'email' => 'nicole.paucek@schultz.info',
13 | ],
14 | ];
15 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/Controller.php:
--------------------------------------------------------------------------------
1 | OptionsAction::class,
17 | ];
18 |
19 | return $acitons;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/Http/Api/Frontend/Controller/Controller.php:
--------------------------------------------------------------------------------
1 | OptionsAction::class,
17 | ];
18 |
19 | return $acitons;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/resources/mail/passwordResetToken-html.php:
--------------------------------------------------------------------------------
1 | urlManager->createAbsoluteUrl(['site/reset-password', 'token' => $user->password_reset_token]);
8 | ?>
9 |
10 |
Hello = Html::encode($user->username) ?>,
11 |
12 |
Follow the link below to reset your password:
13 |
14 |
= Html::a(Html::encode($resetLink), $resetLink) ?>
15 |
16 |
--------------------------------------------------------------------------------
/resources/views/article/comments.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
19 |
--------------------------------------------------------------------------------
/app/Console/Controller/GcController.php:
--------------------------------------------------------------------------------
1 | logAndPrint(sprintf('Deleted %d expired user sessions', $deleted));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/Http/Asset/AppAsset.php:
--------------------------------------------------------------------------------
1 | run();
15 |
--------------------------------------------------------------------------------
/resources/environments/dockerenv/config/web-local.php:
--------------------------------------------------------------------------------
1 | [
5 | 'log' => [
6 | 'targets' => [
7 | 'file' => [
8 | 'levels' => ['info', 'trace'],
9 | ],
10 | ],
11 | ],
12 | 'request' => [
13 | // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
14 | 'cookieValidationKey' => '',
15 | ],
16 | ],
17 | ];
18 |
19 | return $config;
20 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Module/V1/Module.php:
--------------------------------------------------------------------------------
1 | getResponse();
26 | $response->format = 'json';
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/config/test.php:
--------------------------------------------------------------------------------
1 | 'app-tests',
5 | 'components' => [
6 | 'db' => [
7 | 'dsn' => 'mysql:host=localhost;dbname=yiitest',
8 | ],
9 | 'log' => [
10 | 'traceLevel' => 3,
11 | 'targets' => [
12 | 'file' => [
13 | 'class' => yii\log\FileTarget::class,
14 | 'logFile' => '@runtime/logs/test.log',
15 | 'levels' => ['error', 'warning', 'info', 'trace'],
16 | ],
17 | ],
18 | ],
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/app/Http/Api/Form/UserTrait.php:
--------------------------------------------------------------------------------
1 | user === null) {
17 | $this->user = Yii::$app->getUser()->getIdentity();
18 | }
19 |
20 | return $this->user;
21 | }
22 |
23 | public function setUser(IdentityInterface $user)
24 | {
25 | $this->user = $user;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Factory/ArticleMetaFactory.php:
--------------------------------------------------------------------------------
1 | $articleId,
13 | ]);
14 | }
15 |
16 | public static function create(int $articleId, string $content): ArticleMeta
17 | {
18 | return new ArticleMeta([
19 | 'article_id' => $articleId,
20 | 'content' => $content,
21 | ]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Model/SoftDeleteTrait.php:
--------------------------------------------------------------------------------
1 | updateAttributes([
13 | $this->softDeleteField => $this->softDeleteValue,
14 | ]);
15 | }
16 |
17 | /**
18 | * @return bool
19 | */
20 | public function isDeleted(): bool
21 | {
22 | $field = $this->softDeleteField;
23 | return $this->$field == $this->softDeleteValue;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Rbac/Rule/UserDeleteRule.php:
--------------------------------------------------------------------------------
1 | id == $user) {
18 | throw new ForbiddenHttpException(Yii::t('app', 'You cannot delete your own account'));
19 | }
20 |
21 | return true;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/MyController.php:
--------------------------------------------------------------------------------
1 | handle();
14 | }
15 |
16 | public function actionPassword()
17 | {
18 | $form = new ChangePasswordForm();
19 | $form->load(Yii::$app->getRequest()->post(), '');
20 | return $form->handle();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Http/Api/Form/ArticleLikeForm.php:
--------------------------------------------------------------------------------
1 | getLike();
13 | if ($like) {
14 | return;
15 | }
16 |
17 | // creates new like record
18 | $like = ArticleLikeFactory::create((int) $this->id, (int) $this->getUser()->getId());
19 | if (!$like->save()) {
20 | $this->addErrors($like->getErrors());
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Queue/JobEvent.php:
--------------------------------------------------------------------------------
1 | andWhere(['us.user_id' => Yii::$app->getUser()->getId()]);
13 | }
14 |
15 | public function findModelById($id, $action)
16 | {
17 | return Session::findOne([
18 | 'id' => $id,
19 | 'user_id' => Yii::$app->getUser()->getId(),
20 | ]);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Queue/Gii/Generator.php:
--------------------------------------------------------------------------------
1 | '@resources/queue/gii/views',
18 | ];
19 |
20 | public $formView = '@resources/queue/gii/views/form.php';
21 |
22 | public function formView()
23 | {
24 | return $this->formView;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/PermissionController.php:
--------------------------------------------------------------------------------
1 | $actions['index'],
17 | 'options' => $actions['options'],
18 | ];
19 | }
20 |
21 | protected function getType()
22 | {
23 | return Item::TYPE_PERMISSION;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/resources/views/site/error.php:
--------------------------------------------------------------------------------
1 | title = $name;
11 | ?>
12 |
13 |
14 |
= Html::encode($this->title) ?>
15 |
16 |
17 | = nl2br(Html::encode($message)) ?>
18 |
19 |
20 |
21 | The above error occurred while the Web server was processing your request.
22 |
23 |
24 | Please contact us if you think this is a server error. Thank you.
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/Console/Controller/Controller.php:
--------------------------------------------------------------------------------
1 | log($message, $level, $category);
20 | $this->stdout($message . PHP_EOL);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/_support/UnitTester.php:
--------------------------------------------------------------------------------
1 | run();
21 | exit($exitCode);
22 |
--------------------------------------------------------------------------------
/tests/_support/AcceptanceTester.php:
--------------------------------------------------------------------------------
1 | createTable($this->tableName, [
14 | 'article_id' => $this->integer()->notNull()->unique()->comment('Article ID'),
15 | 'content' => $this->text()->notNull()->comment('Content'),
16 | ], $this->tableOptions());
17 | }
18 |
19 | public function safeDown()
20 | {
21 | $this->dropTable($this->tableName);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Form/FormTrait.php:
--------------------------------------------------------------------------------
1 | validate()) {
14 | return $this;
15 | }
16 |
17 | $data = $this->handleInternal();
18 | return $data;
19 | } catch (\Throwable $e) {
20 | if ($this->hasErrors()) {
21 | return $this;
22 | }
23 |
24 | throw $e;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/resources/gii/generators/module/views/view.php:
--------------------------------------------------------------------------------
1 |
5 |
6 |
= "= " ?>$this->context->action->uniqueId ?>
7 |
8 | This is the view content for action "= "= " ?>$this->context->action->id ?>".
9 | The action belongs to the controller "= "= " ?>get_class($this->context) ?>"
10 | in the "= "= " ?>$this->context->module->id ?>" module.
11 |
12 |
13 | You may customize this page by editing the following file:
14 | = "= " ?>__FILE__ ?>
15 |
16 |
17 |
--------------------------------------------------------------------------------
/resources/migrations/m190822_070400_data_article_category.php:
--------------------------------------------------------------------------------
1 | batchInsert($this->tableName, $columns, $rows);
20 | }
21 |
22 | public function safeDown()
23 | {
24 | $this->truncateTable($this->tableName);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/resources/views/site/about.php:
--------------------------------------------------------------------------------
1 | title = Yii::t('app', 'About Us');
8 | $this->params['breadcrumbs'][] = $this->title;
9 | ?>
10 |
11 |
= Html::encode($this->title) ?>
12 |
13 |
--------------------------------------------------------------------------------
/resources/gii/generators/module/views/controller.php:
--------------------------------------------------------------------------------
1 |
11 |
12 | namespace = $generator->getControllerNamespace() ?>;
13 |
14 | use yii\web\Controller;
15 |
16 | /**
17 | * Default controller for the `= $generator->moduleID ?>` module
18 | */
19 | class DefaultController extends Controller
20 | {
21 | /**
22 | * Renders the index view for the module
23 | * @return string
24 | */
25 | public function actionIndex()
26 | {
27 | return $this->render('index');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Form/UserForm.php:
--------------------------------------------------------------------------------
1 | user === null) {
25 | $this->user = Yii::$app->getUser()->getIdentity();
26 | }
27 |
28 | return $this->user;
29 | }
30 |
31 | public function setUser(User $user): void
32 | {
33 | $this->user = $user;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/codeception.dist.yml:
--------------------------------------------------------------------------------
1 | paths:
2 | tests: tests
3 | output: tests/_output
4 | data: tests/_data
5 | support: tests/_support
6 | envs: tests/_envs
7 | actor_suffix: Tester
8 | bootstrap: _bootstrap.php
9 | settings:
10 | colors: true
11 | memory_limit: 1024M
12 | extensions:
13 | enabled:
14 | - Codeception\Extension\RunFailed
15 | coverage:
16 | enabled: true
17 | remote: false
18 | include:
19 | - app/*
20 | exclude:
21 | - app/Http/Controller/*
22 | - app/Http/Api/Controller/*
23 | - app/Http/Api/Backend/Controller/*
24 | - app/Http/Api/Backend/Module/V1/Controller/*
25 | - app/Http/Api/Frontend/Controller/*
26 | - app/Http/Api/Frontend/Module/V1/Controller/*
27 |
--------------------------------------------------------------------------------
/app/Http/Api/Form/BaseArticleLikeForm.php:
--------------------------------------------------------------------------------
1 | like === null) {
28 | $this->like = ArticleLike::findByArticleIdAndUserId($this->id, $this->getUser()->getId());
29 | }
30 |
31 | return $this->like;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Unit/Factory/ArticleMetaFactoryTest.php:
--------------------------------------------------------------------------------
1 | assertSame($articleId, $model->article_id);
18 | $this->assertSame($content, $model->content);
19 | }
20 |
21 | public function dataCreate(): array
22 | {
23 | return [
24 | [1, 'foo'],
25 | [2, 'bar'],
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/conf/app.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=App Docker Compose Service
3 | Requires=docker.service
4 | After=docker.service
5 |
6 | [Service]
7 | Restart=always
8 |
9 | WorkingDirectory=/app
10 |
11 | # Remove old containers, images and volumes
12 | ExecStartPre=/usr/bin/docker-compose down -v
13 | ExecStartPre=/usr/bin/docker-compose rm -v
14 | ExecStartPre=-/bin/bash -c 'docker volume rm $(docker volume ls -q)'
15 | ExecStartPre=-/bin/bash -c 'docker rmi $(docker images | grep "" | awk \'{print $3}\')'
16 | ExecStartPre=-/bin/bash -c 'docker rm -v $(docker ps -aq)'
17 |
18 | # Compose up
19 | ExecStart=/usr/bin/docker-compose up --scale queue=5
20 |
21 | # Compose down, remove containers and volumes
22 | ExecStop=/usr/bin/docker-compose down -v
23 |
24 | [Install]
25 | WantedBy=multi-user.target
--------------------------------------------------------------------------------
/resources/docker/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | user nginx;
2 | worker_processes 1;
3 |
4 | error_log /var/log/nginx/error.log warn;
5 | pid /var/run/nginx.pid;
6 |
7 |
8 | events {
9 | worker_connections 1024;
10 | }
11 |
12 | http {
13 | include /etc/nginx/mime.types;
14 | default_type application/octet-stream;
15 |
16 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
17 | '$status $body_bytes_sent "$http_referer" '
18 | '"$http_user_agent" "$http_x_forwarded_for"';
19 |
20 | access_log /var/log/nginx/access.log main;
21 |
22 | sendfile on;
23 | #tcp_nopush on;
24 |
25 | keepalive_timeout 65;
26 |
27 | #gzip on;
28 |
29 | include /etc/nginx/conf.d/*.conf;
30 | }
31 |
--------------------------------------------------------------------------------
/app/Model/StatusTrait.php:
--------------------------------------------------------------------------------
1 | status === StatusInterface::STATUS_ACTIVE;
9 | }
10 |
11 | public function isDisabled(): bool
12 | {
13 | return $this->status === StatusInterface::STATUS_DISABLED;
14 | }
15 |
16 | public function getStatusName(): string
17 | {
18 | switch ($this->status) {
19 | case StatusInterface::STATUS_ACTIVE:
20 | return \Yii::t('app', 'Active');
21 | case StatusInterface::STATUS_DISABLED:
22 | return \Yii::t('app', 'Disable');
23 | default:
24 | return \Yii::t('app', 'Unknown');
25 | }
26 |
27 | return 'Unknown';
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/resources/mail/layouts/html.php:
--------------------------------------------------------------------------------
1 |
8 | beginPage() ?>
9 |
10 |
11 |
12 |
13 | = Html::encode($this->title) ?>
14 | head() ?>
15 |
16 |
17 | beginBody() ?>
18 | = $content ?>
19 | endBody() ?>
20 |
21 |
22 | endPage() ?>
23 |
--------------------------------------------------------------------------------
/tests/Unit/Helper/DbHelperTest.php:
--------------------------------------------------------------------------------
1 | expectException(\Throwable::class);
16 | }
17 | DbHelper::transaction($callback, $parameters);
18 | }
19 |
20 | public function dataTransaction(): array
21 | {
22 | return [
23 | [function () {
24 | }, [], true],
25 | [function () {
26 | throw new \Exception('fail');
27 | }, [], false],
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # yii console commands
2 | /bin/yii_test
3 | /bin/yii_test.bat
4 |
5 | # phpstorm project files
6 | .idea
7 |
8 | # netbeans project files
9 | nbproject
10 |
11 | # zend studio for eclipse project files
12 | .buildpath
13 | .project
14 | .settings
15 |
16 | # windows thumbnail cache
17 | Thumbs.db
18 |
19 | # composer vendor dir
20 | /vendor
21 |
22 | # composer itself is not needed
23 | # composer.lock
24 | composer.phar
25 |
26 | # Mac DS_Store Files
27 | .DS_Store
28 |
29 | # phpunit itself is not needed
30 | phpunit.phar
31 | # local phpunit config
32 | /phpunit.xml
33 | .phpunit.result.cache
34 |
35 | # ignore generated files
36 | /public/index-test.php
37 | /public/assets/
38 | /public/resources/
39 |
40 | # runtime
41 | /runtime/
42 |
43 | # docker
44 | /.env
45 | /docker-compose.yml
46 |
47 | # vim
48 | tags
49 | .tags
--------------------------------------------------------------------------------
/app/Behavior/CreatorBehavior.php:
--------------------------------------------------------------------------------
1 | attributes = [
18 | $this->creatorAttribute => [
19 | ActiveRecord::EVENT_BEFORE_VALIDATE => function (ModelEvent $event, $attribute) {
20 | /** @var \yii\db\ActiveRecord $model */
21 | $model = $event->sender;
22 | return $model->getIsNewRecord() ? Yii::$app->getUser()->getId() : $model->$attribute;
23 | },
24 | ],
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/resources/docker/php/image/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.2-fpm
2 | RUN apt-get update
3 |
4 | # GD
5 | RUN apt-get install -y \
6 | libfreetype6-dev \
7 | libjpeg62-turbo-dev \
8 | libpng-dev \
9 | && docker-php-ext-install -j$(nproc) iconv \
10 | && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
11 | && docker-php-ext-install -j$(nproc) gd
12 |
13 | # Intl
14 | RUN apt-get install -y \
15 | zlib1g-dev \
16 | libicu-dev \
17 | g++ \
18 | && docker-php-ext-configure intl \
19 | && docker-php-ext-install intl
20 |
21 | # MySQL
22 | RUN docker-php-ext-install pdo_mysql
23 |
24 | # PostgreSQL
25 | RUN apt-get update && apt-get install -y \
26 | libpq-dev \
27 | && docker-php-ext-install pdo_pgsql
28 |
29 | # ZIP
30 | RUN docker-php-ext-install zip
31 |
--------------------------------------------------------------------------------
/resources/migrations/m190829_053119_table_article_like.php:
--------------------------------------------------------------------------------
1 | createTable($this->tableName, [
14 | 'article_id' => $this->integer()->notNull()->comment('Article ID'),
15 | 'user_id' => $this->integer()->notNull()->comment('User ID'),
16 | 'create_time' => $this->createTimestamp(),
17 | ], $this->tableOptions());
18 |
19 | $this->addPrimaryKey('article_like_pk', $this->tableName, ['article_id', 'user_id']);
20 | }
21 |
22 | public function safeDown()
23 | {
24 | $this->dropTable($this->tableName);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Queue/RetryableTrait.php:
--------------------------------------------------------------------------------
1 | getMaxAttemptTimes();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/config/gii.php:
--------------------------------------------------------------------------------
1 | \yii\gii\Module::class,
5 | 'generators' => [
6 | 'model' => [
7 | 'class' => \yii\gii\generators\model\Generator::class,
8 | 'ns' => 'App\Model',
9 | 'baseClass' => 'App\Model\ActiveRecord',
10 | 'useTablePrefix' => true,
11 | 'generateLabelsFromComments' => true,
12 | 'queryNs' => 'App\Model',
13 | 'singularize' => true,
14 | 'standardizeCapitals' => true,
15 | 'enableI18N' => true,
16 | 'queryBaseClass' => \App\Db\ActiveQuery::class,
17 | ],
18 | 'module' => [
19 | 'class' => \App\Gii\Generator\Module\Generator::class,
20 | ],
21 | 'job' => [
22 | 'class' => \App\Queue\Gii\Generator::class,
23 | ],
24 | ],
25 | ];
26 |
--------------------------------------------------------------------------------
/app/Http/Rest/Serializer.php:
--------------------------------------------------------------------------------
1 | linksEnvelope => Link::serialize($pagination->getLinks(true)),
20 | $this->metaEnvelope => [
21 | 'limit' => $pagination->getPageSize(),
22 | 'page' => $pagination->getPage() + 1,
23 | 'page_count' => $pagination->getPageCount(),
24 | 'total_count' => $pagination->totalCount,
25 | ],
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Unit/Console/Job/HelloJobTest.php:
--------------------------------------------------------------------------------
1 | $name,
16 | ]);
17 |
18 | $method = new \ReflectionMethod(HelloJob::class, 'run');
19 | $method->setAccessible(true);
20 | ob_start();
21 | $method->invoke($job);
22 | $output = ob_get_contents();
23 | ob_end_flush();
24 | $this->assertContains($name, $output);
25 | }
26 |
27 | public function dataRun(): array
28 | {
29 | return [
30 | ['foo'],
31 | ['bar'],
32 | ];
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/resources/queue/gii/views/job.php:
--------------------------------------------------------------------------------
1 |
19 | namespace = $ns ?>;
20 |
21 | /**
22 | * Class = $jobClass ?>.
23 | */
24 | class = $jobClass ?> extends = $baseClass ?> = $implements ?>
25 |
26 | {
27 | retryable): ?>
28 | use = $generator->retryableTrait ?>;
29 |
30 |
31 |
32 | public $= $property ?>;
33 |
34 |
35 | protected function run()
36 | {
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Form/LogoutForm.php:
--------------------------------------------------------------------------------
1 | getUser();
14 | if (!$user) {
15 | return;
16 | }
17 |
18 | if ($user instanceof LogoutInterface) {
19 | $user->logout();
20 | return;
21 | }
22 |
23 | Yii::$app->getUser()->logout();
24 | }
25 |
26 | private $user;
27 |
28 | /**
29 | * @return null|IdentityInterface
30 | */
31 | public function getUser()
32 | {
33 | if ($this->user === null) {
34 | $this->user = Yii::$app->getUser()->getIdentity();
35 | }
36 |
37 | return $this->user;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Http/Rest/SafeActionTrait.php:
--------------------------------------------------------------------------------
1 | enableTransaction) {
21 | return call_user_func_array($run, $args);
22 | }
23 |
24 | return DbHelper::transaction($run, $args, $this->getDb());
25 | }
26 |
27 | public function getDb(): Connection
28 | {
29 | /** @var ActiveRecord $modelClass */
30 | $modelClass = $this->modelClass;
31 | /** @var Connection $db */
32 | return $modelClass::getDb();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/resources/environments/dev/bin/yii_test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
26 | exit($exitCode);
27 |
--------------------------------------------------------------------------------
/resources/environments/docker/bin/yii_test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
26 | exit($exitCode);
27 |
--------------------------------------------------------------------------------
/resources/environments/dockerenv/bin/yii_test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
26 | exit($exitCode);
27 |
--------------------------------------------------------------------------------
/resources/gii/generators/module/views/module.php:
--------------------------------------------------------------------------------
1 | moduleClass;
10 | $pos = strrpos($className, '\\');
11 | $ns = ltrim(substr($className, 0, $pos), '\\');
12 | $className = substr($className, $pos + 1);
13 |
14 | echo "
16 |
17 | namespace = $ns ?>;
18 |
19 | /**
20 | * = $generator->moduleID ?> module definition class
21 | */
22 | class = $className ?> extends \yii\base\Module
23 | {
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public $controllerNamespace = '= $generator->getControllerNamespace() ?>';
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | public function init()
33 | {
34 | parent::init();
35 |
36 | // custom initialization code goes here
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/resources/messages/zh-CN/app.php:
--------------------------------------------------------------------------------
1 | '关于我们',
5 | 'Article' => '文章',
6 |
7 | 'Console' => '控制台',
8 | 'Contact Us' => '联系我们',
9 | 'Content' => '内容',
10 |
11 | 'Description' => '描述',
12 |
13 | 'Email' => '电子邮箱',
14 |
15 | 'First Name' => '名称',
16 |
17 | 'Home' => '主页',
18 |
19 | 'Last Name' => '姓氏',
20 | 'Login' => '登录',
21 | 'Logout' => '注销',
22 |
23 | 'Name' => '名称',
24 | 'New Password' => '新密码',
25 |
26 | 'Original password is incorrect' => '原密码不正确',
27 | 'Original Password' => '原密码',
28 |
29 | 'Password' => '密码',
30 | 'Permission' => '权限',
31 |
32 | 'Real Name' => '真实姓名',
33 | 'Reset Password' => '重置密码',
34 | 'Role' => '角色',
35 |
36 | 'Setting' => '设置',
37 | 'Sign Up' => '注册',
38 | 'Subject' => '主题',
39 |
40 | 'User' => '用户',
41 | 'Username' => '用户名',
42 |
43 | 'Verification Code' => '验证码',
44 | ];
45 |
--------------------------------------------------------------------------------
/resources/messages/zh-TW/app.php:
--------------------------------------------------------------------------------
1 | '關於我們',
5 | 'Article' => '文章',
6 |
7 | 'Console' => '控制臺',
8 | 'Contact Us' => '聯系我們',
9 | 'Content' => '內容',
10 |
11 | 'Description' => '描述',
12 |
13 | 'Email' => '電子郵箱',
14 |
15 | 'First Name' => '名稱',
16 |
17 | 'Home' => '主頁',
18 |
19 | 'Last Name' => '姓氏',
20 | 'Login' => '登錄',
21 | 'Logout' => '注銷',
22 |
23 | 'Name' => '名稱',
24 | 'New Password' => '新密碼',
25 |
26 | 'Original password is incorrect' => '原密碼不正確',
27 | 'Original Password' => '原密碼',
28 |
29 | 'Password' => '密碼',
30 | 'Permission' => '權限',
31 |
32 | 'Real Name' => '真實姓名',
33 | 'Reset Password' => '重置密碼',
34 | 'Role' => '角色',
35 |
36 | 'Setting' => '設置',
37 | 'Sign Up' => '注冊',
38 | 'Subject' => '主題',
39 |
40 | 'User' => '用戶',
41 | 'Username' => '用戶名',
42 |
43 | 'Verification Code' => '驗證碼',
44 | ];
45 |
--------------------------------------------------------------------------------
/app/Http/Api/Model/Article.php:
--------------------------------------------------------------------------------
1 | function ($model) {
15 | return $model->category->name ?? '';
16 | },
17 | 'title',
18 | 'author',
19 | 'cover',
20 | 'status',
21 | 'summary',
22 | 'release_time',
23 | 'create_time',
24 | 'update_time',
25 | ];
26 | }
27 |
28 | public function extraFields()
29 | {
30 | return [
31 | 'version',
32 | 'likes_count',
33 | 'content' => function ($model) {
34 | return $model->meta->content ?? '';
35 | },
36 | ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/resources/migrations/m190822_034841_table_article_category.php:
--------------------------------------------------------------------------------
1 | createTable($this->tableName, [
14 | 'id' => $this->primaryKey(),
15 | 'creator' => $this->integer()->notNull()->comment('Creator ID'),
16 | 'name' => $this->string(32)->notNull()->comment('Name'),
17 | 'create_time' => $this->createTimestamp(),
18 | 'update_time' => $this->updateTimestamp(),
19 | ], $this->tableOptions());
20 |
21 | $this->createIndex('article_category_idx_creator', $this->tableName, ['creator']);
22 | }
23 |
24 | public function safeDown()
25 | {
26 | $this->dropTable($this->tableName);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Behavior/ArticleCategoryBehavior.php:
--------------------------------------------------------------------------------
1 | 'beforeDelete',
16 | ];
17 | }
18 |
19 | public function beforeDelete(Event $event)
20 | {
21 | /** @var ArticleCategory $model */
22 | $model = $event->sender;
23 | $count = Article::find()
24 | ->where([
25 | 'category_id' => $model->id,
26 | 'is_deleted' => 0,
27 | ])
28 | ->count();
29 | if ($count > 0) {
30 | throw new ForbiddenHttpException('Unable to delete category since there are articles belong to it');
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/codeception.yml:
--------------------------------------------------------------------------------
1 | # suite config
2 | suites:
3 | acceptance:
4 | actor: AcceptanceTester
5 | path: .
6 | modules:
7 | enabled:
8 | - WebDriver:
9 | url: http://localhost:8080
10 | browser: chrome
11 | - \Helper\Acceptance
12 |
13 | # add Codeception\Step\Retry trait to AcceptanceTester to enable retries
14 | step_decorators:
15 | - Codeception\Step\ConditionalAssertion
16 | - Codeception\Step\TryTo
17 | - Codeception\Step\Retry
18 |
19 | extensions:
20 | enabled: [Codeception\Extension\RunFailed]
21 |
22 | params:
23 | - env
24 |
25 | gherkin: []
26 |
27 | # additional paths
28 | paths:
29 | tests: tests
30 | output: tests/_output
31 | data: tests/_data
32 | support: tests/_support
33 | envs: tests/_envs
34 |
35 | settings:
36 | shuffle: false
37 | lint: true
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 |
4 | dist: xenial
5 |
6 | php:
7 | - 7.1
8 | - 7.2
9 | - 7.3
10 |
11 | # faster builds on new travis setup not using sudo
12 | sudo: false
13 |
14 | services:
15 | - mysql
16 | - redis-server
17 |
18 | # cache vendor dirs
19 | cache:
20 | directories:
21 | - $HOME/.composer/cache
22 |
23 | install:
24 | - travis_retry composer self-update && composer --version
25 | - export PATH="$HOME/.composer/vendor/bin:$PATH"
26 | - travis_retry composer install --prefer-dist --no-interaction
27 |
28 | before_script:
29 | - mysql -e 'CREATE DATABASE yiitest;'
30 | - php bin/init --env=Development --overwrite=y
31 | - php bin/yii_test migrate/fresh --interactive=0
32 | - php bin/yii_test serve &
33 |
34 | script:
35 | - vendor/bin/codecept run --coverage --coverage-xml
36 |
37 | after_script:
38 | - wget -c https://scrutinizer-ci.com/ocular.phar
39 | - php ocular.phar code-coverage:upload --format=php-clover tests/_output/coverage.xml
40 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Form/MyInfoForm.php:
--------------------------------------------------------------------------------
1 | getUser();
11 | return [
12 | 'id' => $user->id,
13 | 'username' => $user->username,
14 | 'name' => $user->name,
15 | 'first_name' => $user->first_name,
16 | 'last_name' => $user->last_name,
17 | 'email' => $user->email,
18 | 'avatar' => $user->avatar,
19 | 'language' => $user->language,
20 | 'permissions' => $this->getPermissions(),
21 | ];
22 | }
23 |
24 | protected function getPermissions()
25 | {
26 | $user = $this->getUser();
27 | $auth = Yii::$app->getAuthManager();
28 | $permissions = $auth->getPermissionsByUser($user->id);
29 | $names = array_column($permissions, 'name');
30 | return $names;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/resources/views/site/resetPassword.php:
--------------------------------------------------------------------------------
1 | title = Yii::t('app', 'Reset Password');
11 | $this->params['breadcrumbs'][] = $this->title;
12 | ?>
13 |
14 |
= Html::encode($this->title) ?>
15 |
16 |
Please choose your new password:
17 |
18 |
19 |
20 | 'reset-password-form']); ?>
21 |
22 | = $form->field($model, 'password')->passwordInput(['autofocus' => true]) ?>
23 |
24 |
25 | = Html::submitButton('Save', ['class' => 'btn btn-primary']) ?>
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/resources/environments/dev/config/web-local.php:
--------------------------------------------------------------------------------
1 | [
5 | 'log' => [
6 | 'targets' => [
7 | 'file' => [
8 | 'levels' => ['info', 'trace'],
9 | ],
10 | ],
11 | ],
12 | 'request' => [
13 | // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
14 | 'cookieValidationKey' => '',
15 | ],
16 | ],
17 | ];
18 |
19 | if (YII_ENV_DEV) {
20 | $allowedIPs = ['127.0.0.1', '::1'];
21 |
22 | // configuration adjustments for 'dev' environment
23 | $config['bootstrap'][] = 'debug';
24 | $config['modules']['debug'] = array_merge(require __DIR__ . '/debug.php', [
25 | 'allowedIPs' => $allowedIPs
26 | ]);
27 |
28 | $config['bootstrap'][] = 'gii';
29 | $config['modules']['gii'] = array_merge(require __DIR__ . '/gii.php', [
30 | 'allowedIPs' => $allowedIPs
31 | ]);
32 | }
33 |
34 | return $config;
35 |
--------------------------------------------------------------------------------
/resources/environments/docker/config/web-local.php:
--------------------------------------------------------------------------------
1 | [
5 | 'log' => [
6 | 'targets' => [
7 | 'file' => [
8 | 'levels' => ['info', 'trace'],
9 | ],
10 | ],
11 | ],
12 | 'request' => [
13 | // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
14 | 'cookieValidationKey' => '',
15 | ],
16 | ],
17 | ];
18 |
19 | if (YII_ENV_DEV) {
20 | $allowedIPs = ['127.0.0.1', '::1'];
21 |
22 | // configuration adjustments for 'dev' environment
23 | $config['bootstrap'][] = 'debug';
24 | $config['modules']['debug'] = array_merge(require __DIR__ . '/debug.php', [
25 | 'allowedIPs' => $allowedIPs
26 | ]);
27 |
28 | $config['bootstrap'][] = 'gii';
29 | $config['modules']['gii'] = array_merge(require __DIR__ . '/gii.php', [
30 | 'allowedIPs' => $allowedIPs
31 | ]);
32 | }
33 |
34 | return $config;
35 |
--------------------------------------------------------------------------------
/app/Job/MailJob.php:
--------------------------------------------------------------------------------
1 | mailer = Instance::ensure($this->mailer, MailerInterface::class);
30 | foreach ($this->messages as &$message) {
31 | $message = Instance::ensure($message, MessageInterface::class);
32 | }
33 | unset($message);
34 | }
35 |
36 | protected function run()
37 | {
38 | $sent = $this->mailer->sendMultiple($this->messages);
39 | return $sent;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/views/site/resendVerificationEmail.php:
--------------------------------------------------------------------------------
1 | title = 'Resend verification email';
10 | $this->params['breadcrumbs'][] = $this->title;
11 | ?>
12 |
13 |
= Html::encode($this->title) ?>
14 |
15 |
Please fill out your email. A verification email will be sent there.
16 |
17 |
18 |
19 | 'resend-verification-email-form']); ?>
20 |
21 | = $form->field($model, 'email')->textInput(['autofocus' => true]) ?>
22 |
23 |
24 | = Html::submitButton('Send', ['class' => 'btn btn-primary']) ?>
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/conf/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | charset utf-8;
3 | client_max_body_size 128M;
4 | sendfile off;
5 |
6 | listen 80; ## listen for ipv4
7 | # listen [::]:80 default_server ipv6only=on; ## listen for ipv6
8 |
9 | server_name yii.test;
10 | root /app/public/;
11 | index index.php;
12 |
13 | access_log /app/runtime/logs/app-access.log;
14 | error_log /app/runtime/logs/app-error.log;
15 |
16 | location / {
17 | try_files $uri $uri/ /index.php$is_args$args;
18 | }
19 |
20 | # uncomment to avoid processing of calls to non-existing static files by Yii
21 | location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
22 | try_files $uri =404;
23 | }
24 | error_page 404 /404.html;
25 |
26 | location ~ \.php$ {
27 | include fastcgi_params;
28 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
29 | fastcgi_pass php-fpm;
30 | try_files $uri =404;
31 | }
32 |
33 | location ~ /\.(ht|svn|git) {
34 | deny all;
35 | }
36 | }
--------------------------------------------------------------------------------
/resources/views/site/requestPasswordResetToken.php:
--------------------------------------------------------------------------------
1 | title = 'Request password reset';
11 | $this->params['breadcrumbs'][] = $this->title;
12 | ?>
13 |
14 |
= Html::encode($this->title) ?>
15 |
16 |
Please fill out your email. A link to reset password will be sent there.
17 |
18 |
19 |
20 | 'request-password-reset-form']); ?>
21 |
22 | = $form->field($model, 'email')->textInput(['autofocus' => true]) ?>
23 |
24 |
25 | = Html::submitButton('Send', ['class' => 'btn btn-primary']) ?>
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/resources/console/views/migration.php:
--------------------------------------------------------------------------------
1 |
15 | use App\Db\Migration;
16 |
17 | /**
18 | * Class = $className . "\n" ?>
19 | */
20 | class = $className ?> extends Migration
21 | {
22 | public function safeUp()
23 | {
24 |
25 | }
26 |
27 | public function safeDown()
28 | {
29 | echo "= $className ?> cannot be reverted.\n";
30 |
31 | return false;
32 | }
33 |
34 | /*
35 | // Use up()/down() to run migration code without a transaction.
36 | public function up()
37 | {
38 |
39 | }
40 |
41 | public function down()
42 | {
43 | echo "= $className ?> cannot be reverted.\n";
44 |
45 | return false;
46 | }
47 | */
48 | }
49 |
--------------------------------------------------------------------------------
/app/Console/Controller/HelloController.php:
--------------------------------------------------------------------------------
1 | stdout('Hello ' . $name . PHP_EOL);
12 | }
13 |
14 | public function actionMail($to, $name = 'Foo')
15 | {
16 | $mailer = Yii::$app->getMailer();
17 | $sent = $mailer->compose('hello', ['name' => $name])
18 | ->setTo($to)
19 | ->setFrom(Yii::$app->get('settingManager')->get('mailer.username'))
20 | ->setSubject('Hello')
21 | ->send();
22 | if (!$sent) {
23 | $this->stdout('Send failure' . PHP_EOL);
24 | return;
25 | }
26 |
27 | $this->stdout('Sent successfully' . PHP_EOL);
28 | }
29 |
30 | public function actionJob($name = 'World')
31 | {
32 | $job = new HelloJob(['name' => $name]);
33 | $id = Yii::$app->get('queue')->push($job);
34 | $this->stdout(HelloJob::class . '#' . $id . PHP_EOL);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Unit/Validator/UrlValidatorTest.php:
--------------------------------------------------------------------------------
1 | setAccessible(true);
17 | $result = $method->invoke($validator, $url);
18 | if ($valid) {
19 | $this->assertNull($result);
20 | } else {
21 | $this->assertTrue(is_array($result));
22 | $this->assertCount(2, $result);
23 | }
24 | }
25 |
26 | public function dataProviderUrl(): array
27 | {
28 | return [
29 | ['http://localhost', true],
30 | ['http://localhost:8080', true],
31 | ['localhost:8080', false],
32 | ['ftp://localhost:8080', false],
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/resources/environments/dev/config/common-local.php:
--------------------------------------------------------------------------------
1 | [
4 | 'db' => [
5 | 'dsn' => 'mysql:host=localhost;dbname=yii',
6 | 'username' => 'root',
7 | 'password' => '',
8 | 'enableSchemaCache' => false,
9 | ],
10 | 'mailer' => [
11 | 'useFileTransport' => true,
12 | 'transport' => [
13 | 'host' => 'smtp.example.com',
14 | 'port' => 25,
15 | 'encryption' => 'ssl',
16 | // 'username' => 'admin@example.com',
17 | 'password' => '',
18 | ],
19 | ],
20 | 'redis' => [
21 | 'hostname' => 'localhost',
22 | 'port' => 6379,
23 | 'database' => 0,
24 | // 'password' => '',
25 | ],
26 | 'uploader' => [
27 | 'host' => 'http://localhost:8080/resources',
28 | 'filesystem' => [
29 | 'class' => \creocoder\flysystem\LocalFilesystem::class,
30 | 'path' => '@webroot/resources',
31 | ],
32 | ],
33 | ],
34 | ];
35 |
--------------------------------------------------------------------------------
/resources/environments/docker/config/common-local.php:
--------------------------------------------------------------------------------
1 | [
4 | 'db' => [
5 | 'dsn' => 'mysql:host=db;dbname=yii',
6 | 'username' => 'root',
7 | 'password' => 'foobar',
8 | 'enableSchemaCache' => false,
9 | ],
10 | 'mailer' => [
11 | 'useFileTransport' => true,
12 | 'transport' => [
13 | 'host' => 'smtp.example.com',
14 | 'port' => 25,
15 | 'encryption' => 'ssl',
16 | // 'username' => 'admin@example.com',
17 | 'password' => '',
18 | ],
19 | ],
20 | 'redis' => [
21 | 'hostname' => 'redis',
22 | 'port' => 6379,
23 | 'database' => 0,
24 | 'password' => 'foobar',
25 | ],
26 | 'uploader' => [
27 | 'host' => 'http://localhost:8080/resources',
28 | 'filesystem' => [
29 | 'class' => \creocoder\flysystem\LocalFilesystem::class,
30 | 'path' => '@webroot/resources',
31 | ],
32 | ],
33 | ],
34 | ];
35 |
--------------------------------------------------------------------------------
/resources/environments/prod/config/common-local.php:
--------------------------------------------------------------------------------
1 | [
4 | 'db' => [
5 | 'dsn' => 'mysql:host=localhost;dbname=yii',
6 | 'username' => 'root',
7 | 'password' => '',
8 | 'enableSchemaCache' => false,
9 | ],
10 | 'mailer' => [
11 | 'useFileTransport' => true,
12 | 'transport' => [
13 | 'host' => 'smtp.example.com',
14 | 'port' => 25,
15 | 'encryption' => 'ssl',
16 | // 'username' => 'admin@example.com',
17 | 'password' => '',
18 | ],
19 | ],
20 | 'redis' => [
21 | 'hostname' => 'localhost',
22 | 'port' => 6379,
23 | 'database' => 0,
24 | 'password' => '',
25 | ],
26 | 'uploader' => [
27 | 'host' => 'http://localhost:8080/resources',
28 | 'filesystem' => [
29 | 'class' => \creocoder\flysystem\LocalFilesystem::class,
30 | 'path' => '@webroot/resources',
31 | ],
32 | ],
33 | ],
34 | ];
35 |
--------------------------------------------------------------------------------
/tests/Unit/Factory/SessionFactoryTest.php:
--------------------------------------------------------------------------------
1 | assertSame($userId, $session->user_id);
19 | $this->assertTrue($duration + time() >= $session->expire_time);
20 | $this->assertTrue($refeshDuration + time() >= $session->refresh_token_expire_time);
21 | $this->assertEquals($request->getUserIP(), $session->ip_address);
22 | $this->assertEquals($request->getUserAgent(), $session->user_agent);
23 | }
24 |
25 | public function dataCreate(): array
26 | {
27 | return [
28 | [1, 2, 3, new \yii\web\Request()],
29 | [4, 5, 6, new \yii\web\Request()],
30 | ];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Helper/DbHelper.php:
--------------------------------------------------------------------------------
1 | getDb() will be used if no specified.
15 | * @param null|string $isolationLevel
16 | *
17 | * @throws \Throwable
18 | */
19 | public static function transaction(callable $callback, array $parameters = [], ?Connection $db = null, ?string $isolationLevel = null)
20 | {
21 | $db = $db ?? Yii::$app->getDb();
22 | $transaction = $db->beginTransaction($isolationLevel);
23 | try {
24 | $result = call_user_func_array($callback, $parameters);
25 |
26 | $transaction->commit();
27 |
28 | return $result;
29 | } catch (\Throwable $e) {
30 | Yii::error($e, __METHOD__);
31 | $transaction->rollBack();
32 | throw $e;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/resources/migrations/m190724_031648_data_user.php:
--------------------------------------------------------------------------------
1 | getSecurity()->generatePasswordHash('123456'), '', 'admin@example.com',
23 | StatusInterface::STATUS_ACTIVE, $now, $now,
24 | ],
25 | ];
26 | $this->batchInsert(User::tableName(), $columns, $rows);
27 |
28 | $auth = Yii::$app->getAuthManager();
29 | $auth->assign($auth->getRole('Administrator'), 1);
30 | }
31 |
32 | public function safeDown()
33 | {
34 | $this->delete(User::tableName(), ['username' => 'admin']);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/Model/ArticleMeta.php:
--------------------------------------------------------------------------------
1 | Yii::t('app', 'Article ID'),
44 | 'content' => Yii::t('app', 'Content'),
45 | ];
46 | }
47 |
48 | public function getArticle()
49 | {
50 | return $this->hasOne(Article::class, ['id' => 'article_id']);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/Http/Rest/DeleteAction.php:
--------------------------------------------------------------------------------
1 | safeRun($id, [$this, 'delete']);
18 | }
19 |
20 | public function delete($id)
21 | {
22 | $model = $this->findModel($id);
23 |
24 | if ($this->checkAccess) {
25 | call_user_func($this->checkAccess, $this->id, $model);
26 | }
27 |
28 | if ($this->deleteModel($model) === false) {
29 | throw new ServerErrorHttpException('Failed to delete the object for unknown reason.');
30 | }
31 |
32 | Yii::$app->getResponse()->setStatusCode(204);
33 | }
34 |
35 | /**
36 | * @param ActiveRecord $model
37 | * @return int|false
38 | */
39 | private function deleteModel($model)
40 | {
41 | if ($model instanceof SoftDeleteInterface) {
42 | return $model->softDelete();
43 | }
44 |
45 | return $model->delete();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/AuthItemController.php:
--------------------------------------------------------------------------------
1 | alias('ai')
14 | ->orderBy(['ai.name' => SORT_ASC])
15 | ->andWhere(['ai.type' => $this->getType()]);
16 | }
17 |
18 | public function searchModel()
19 | {
20 | return (new DynamicModel(['name' => '', 'description' => '']))
21 | ->addRule(['name', 'description'], 'string');
22 | }
23 |
24 | public function applyFilter($query, $model, $filter)
25 | {
26 | foreach (['name', 'description'] as $name) {
27 | if (!empty($model->$name)) {
28 | $query->andWhere(['LIKE', 'ai.' . $name, $model->$name]);
29 | }
30 | }
31 | }
32 |
33 | public function findModelById($id, $action)
34 | {
35 | return $action->modelClass::findOne([
36 | 'name' => $id,
37 | 'type' => $this->getType(),
38 | ]);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/resources/environments/dockerenv/config/common-local.php:
--------------------------------------------------------------------------------
1 | [
4 | 'db' => [
5 | 'dsn' => 'mysql:host=' . getenv('DB_HOST') . ';dbname=' . getenv('DB_NAME'),
6 | 'username' => getenv('DB_USER'),
7 | 'password' => getenv('DB_PASSWORD'),
8 | 'enableSchemaCache' => true,
9 | ],
10 | 'mailer' => [
11 | 'useFileTransport' => true,
12 | 'transport' => [
13 | 'host' => getenv('MAILER_HOST'),
14 | 'port' => getenv('MAILER_PORT'),
15 | 'encryption' => 'ssl',
16 | 'username' => getenv('MAILER_USER'),
17 | 'password' => getenv('MAILER_PASSWORD'),
18 | ],
19 | ],
20 | 'redis' => [
21 | 'hostname' => getenv("REDIS_HOST"),
22 | 'port' => 6379,
23 | 'database' => getenv("REDIS_DB"),
24 | ],
25 | 'uploader' => [
26 | 'host' => getenv("UPLOADER_HOST"),
27 | 'filesystem' => [
28 | 'class' => \creocoder\flysystem\LocalFilesystem::class,
29 | 'path' => '@webroot/resources',
30 | ],
31 | ],
32 | ],
33 | ];
34 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/ArticleCategoryController.php:
--------------------------------------------------------------------------------
1 | alias('c')
15 | ->orderBy([
16 | 'c.name' => SORT_ASC
17 | ]);
18 | }
19 |
20 | public function searchModel()
21 | {
22 | return (new DynamicModel(['id' => '', 'name' => '']))
23 | ->addRule(['name'], 'string')
24 | ->addRule(['id'], 'number');
25 | }
26 |
27 | protected function applyFilter($query, $model, $filter)
28 | {
29 | foreach (['name'] as $name) {
30 | if (!empty($model->$name)) {
31 | $query->andWhere(['LIKE', 'c.' . $name, $model->$name]);
32 | }
33 | }
34 | foreach (['id'] as $name) {
35 | if (is_numeric($model->$name)) {
36 | $query->andWhere(['c.' . $name => $model->$name]);
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/resources/migrations/m190822_070404_data_article.php:
--------------------------------------------------------------------------------
1 | insert($this->tableName, [
17 | 'id' => 1,
18 | 'title' => 'Hello World',
19 | 'creator' => 1,
20 | 'author' => 'Administrator',
21 | 'category_id' => 1,
22 | 'summary' => 'Congratulation! You have set up Yii2 and Vue application successfully.',
23 | 'cover' => '',
24 | 'release_time' => $now,
25 | 'create_time' => $now,
26 | 'update_time' => $now,
27 | ]);
28 |
29 | $this->insert($this->metaTableName, [
30 | 'article_id' => 1,
31 | 'content' => 'Congratulation! You have set up Yii2 and Vue application successfully.
',
32 | ]);
33 | }
34 |
35 | public function safeDown()
36 | {
37 | $this->truncateTable($this->tableName);
38 | $this->truncateTable($this->metaTableName);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/resources/views/site/signup.php:
--------------------------------------------------------------------------------
1 | title = Yii::t('app', 'Sign Up');
11 | $this->params['breadcrumbs'][] = $this->title;
12 | ?>
13 |
14 |
= Html::encode($this->title) ?>
15 |
16 |
Please fill out the following fields to signup:
17 |
18 |
19 |
20 | 'form-signup']); ?>
21 |
22 | = $form->field($model, 'username')->textInput(['autofocus' => true]) ?>
23 |
24 | = $form->field($model, 'email') ?>
25 |
26 | = $form->field($model, 'password')->passwordInput() ?>
27 |
28 | = $form->field($model, 'first_name') ?>
29 |
30 | = $form->field($model, 'last_name') ?>
31 |
32 |
33 | = Html::submitButton('Signup', ['class' => 'btn btn-primary', 'name' => 'signup-button']) ?>
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/resources/docker/nginx/conf.d/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | charset utf-8;
3 | client_max_body_size 128M;
4 | sendfile off;
5 |
6 | listen 80; ## listen for ipv4
7 | # listen [::]:80 default_server ipv6only=on; ## listen for ipv6
8 |
9 | server_name default_server;
10 | root /app/public/;
11 | index index.php;
12 |
13 | access_log /app/runtime/logs/access.log;
14 | error_log /app/runtime/logs/error.log;
15 |
16 | location / {
17 | try_files $uri $uri/ /index.php$is_args$args;
18 | }
19 |
20 | # uncomment to avoid processing of calls to non-existing static files by Yii
21 | location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
22 | try_files $uri =404;
23 | }
24 | error_page 404 /404.html;
25 |
26 | location ~ \.php$ {
27 | include fastcgi_params;
28 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
29 | proxy_set_header Host $host;
30 | proxy_set_header X-Real-IP $remote_addr;
31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
32 | proxy_set_header X-Forwarded-Host $server_name;
33 | fastcgi_pass php-fpm:9000;
34 | try_files $uri =404;
35 | }
36 |
37 | location ~ /\.(ht|svn|git) {
38 | deny all;
39 | }
40 | }
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/SettingController.php:
--------------------------------------------------------------------------------
1 | $actions['index'],
17 | 'view' => $actions['view'],
18 | 'update' => $actions['update'],
19 | 'options' => $actions['options'],
20 | ];
21 | }
22 |
23 | public function searchModel()
24 | {
25 | return (new DynamicModel(['id' => '', 'description' => '']))
26 | ->addRule(['id', 'description'], 'string');
27 | }
28 |
29 | protected function getQuery($action)
30 | {
31 | return parent::getQuery($action)
32 | ->alias('s')
33 | ->orderBy(['id' => SORT_ASC]);
34 | }
35 |
36 | protected function applyFilter($query, $model, $filter)
37 | {
38 | foreach (['id', 'description'] as $name) {
39 | if (!empty($model->$name)) {
40 | $query->andWhere(['LIKE', 's.' . $name, $model->$name]);
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/UploadController.php:
--------------------------------------------------------------------------------
1 | getConfig('image'));
13 | $form->load(Yii::$app->getRequest()->post(), '');
14 | $form->file = UploadedFile::getInstanceByName('file');
15 | return $form->handle();
16 | }
17 |
18 | public function actionVideos()
19 | {
20 | $form = new UploadForm($this->getConfig('video'));
21 | $form->load(Yii::$app->getRequest()->post(), '');
22 | $form->file = UploadedFile::getInstanceByName('file');
23 | return $form->handle();
24 | }
25 |
26 | private function getConfig(string $type): array
27 | {
28 | /** @var \RazonYang\Yii2\Setting\ManagerInterface $setting */
29 | $setting = Yii::$app->get('settingManager');
30 | $prefix = 'upload.' . $type . '.';
31 | $parameters = ['maxSize', 'extensions'];
32 | $config = [];
33 | foreach ($parameters as $parameter) {
34 | $config[$parameter] = $setting->get($prefix.$parameter);
35 | }
36 | return $config;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Http/Api/Frontend/Controller/ArticleCommentController.php:
--------------------------------------------------------------------------------
1 | alias('a')
24 | ->orderBy([
25 | 'a.create_time' => SORT_DESC
26 | ])
27 | ->andWhere([
28 | 'a.is_deleted' => 0,
29 | ]);
30 | }
31 |
32 | public function searchModel()
33 | {
34 | return (new DynamicModel(['article_id' => null]))
35 | ->addRule(['article_id'], 'number');
36 | }
37 |
38 | protected function applyFilter($query, $model, $filter)
39 | {
40 | foreach (['article_id'] as $name) {
41 | if (!empty($model->$name)) {
42 | $query->andWhere(['a.' . $name => (int) $model->$name]);
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/tencent.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a docker container, publish it to Tencent Kubernetes Engine (TKE) registry.
2 | #
3 | # To configure this workflow:
4 | #
5 | # 1. Set up secrets in your workspace:
6 | # - TENCENT_CLOUD_ACCOUNT_ID with Tencent Cloud account id
7 | # - TKE_REGISTRY_PASSWORD with TKE registry password
8 | #
9 | # 2. Change the values for the TKE_IMAGE_URL environment variables (below).
10 |
11 | name: Tencent Kubernetes Engine
12 |
13 | on:
14 | push:
15 | branches:
16 | - master
17 |
18 | # Environment variables available to all jobs and steps in this workflow
19 | env:
20 | TKE_IMAGE_URL: hkccr.ccs.tencentyun.com/razonyang/yii2-app-template
21 |
22 | jobs:
23 | setup-build-publish:
24 | name: Setup, Build and Publish
25 | runs-on: ubuntu-latest
26 | steps:
27 |
28 | - name: Checkout
29 | uses: actions/checkout@v2
30 |
31 | # Build
32 | - name: Build Docker image
33 | run: |
34 | docker build -t ${TKE_IMAGE_URL}:${GITHUB_SHA} -t ${TKE_IMAGE_URL}:latest .
35 | - name: Login TKE Registry
36 | run: |
37 | docker login -u ${{ secrets.TENCENT_CLOUD_ACCOUNT_ID }} -p ${{ secrets.TKE_REGISTRY_PASSWORD }} ${TKE_IMAGE_URL}
38 | # Push the Docker image to TKE Registry
39 | - name: Publish
40 | run: |
41 | docker push ${TKE_IMAGE_URL}:${GITHUB_SHA}
42 | docker push ${TKE_IMAGE_URL}:latest
43 |
--------------------------------------------------------------------------------
/app/Validator/UrlValidator.php:
--------------------------------------------------------------------------------
1 | message === null) {
30 | $this->message = Yii::t('yii', '{attribute} is not a valid URL.');
31 | }
32 | }
33 |
34 | protected function validateValue($value)
35 | {
36 | $components = parse_url($value);
37 | foreach ($this->components as $name) {
38 | if (!isset($components[$name])) {
39 | return [$this->message, []];
40 | }
41 | }
42 |
43 | if (!in_array($components[self::COMPONENT_SCHEME], $this->schemes)) {
44 | return [$this->message, []];
45 | }
46 |
47 | return null;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/config/params.php:
--------------------------------------------------------------------------------
1 | 'admin@example.com',
6 | 'supportEmail' => 'support@example.com',
7 |
8 | 'site.since' => 2008,
9 |
10 | // user
11 | 'user.session.duration' => 7200, // session's duration in seconds, default to 2 hours.
12 | 'user.session.refreshTokenDuration' => 403200, // refresh token's duration in seconds, default to a week.
13 | 'user.session.durationAfterRefresh' => 300, // old session's remaining durantion after refreshing session, default to 5 minutes.
14 | 'user.passwordResetTokenExpire' => 7200,
15 |
16 | /* ============================= backend ============================== */
17 | 'backend.url' => 'http://localhost',
18 |
19 | /* =============================== API =============================== */
20 | // CORS
21 | 'api.cors.origin' => ['*'],
22 | 'api.cors.methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
23 | 'api.cors.headers' => ['Content-Type', 'Authorization'],
24 | 'api.cors.allowCredentials' => null,
25 | 'api.cors.maxAge' => 86400,
26 | 'api.cors.exposeHeaders' => [],
27 |
28 | // rate limiter
29 | 'api.rateLimiter.capacity' => 5000,
30 | 'api.rateLimiter.rate' => 0.72,
31 | 'api.rateLimiter.limitPeriod' => 3600,
32 | 'api.rateLimiter.ttl' => 3600,
33 | 'api.rateLimiter.prefix' => 'rate_limiter:',
34 | ];
35 |
--------------------------------------------------------------------------------
/resources/migrations/m190829_093604_article_comment.php:
--------------------------------------------------------------------------------
1 | createTable($this->tableName, [
14 | 'id' => $this->bigPrimaryKey(),
15 | 'reply_to' => $this->bigInteger()->notNull()->defaultValue(0)->comment('Reply To'),
16 | 'article_id' => $this->integer()->notNull()->comment('Article ID'),
17 | 'user_id' => $this->integer()->notNull()->comment('User ID'),
18 | 'content' => $this->text()->notNull()->comment('Content'),
19 | 'is_deleted' => $this->softDelete(),
20 | 'create_time' => $this->createTimestamp(),
21 | 'update_time' => $this->updateTimestamp(),
22 | ], $this->tableOptions());
23 |
24 | $this->createIndex('article_comment_idx_reply_to', $this->tableName, 'reply_to');
25 | $this->createIndex('article_comment_idx_article_id', $this->tableName, 'article_id');
26 | $this->createIndex('article_comment_idx_user_id', $this->tableName, 'user_id');
27 | $this->createIndex('article_comment_idx_create_time', $this->tableName, 'create_time');
28 | }
29 |
30 | public function safeDown()
31 | {
32 | $this->dropTable($this->tableName);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/ArticleController.php:
--------------------------------------------------------------------------------
1 | alias('a')
15 | ->orderBy([
16 | 'a.id' => SORT_DESC
17 | ])
18 | ->andWhere([
19 | 'a.is_deleted' => 0,
20 | ]);
21 | }
22 |
23 | public function searchModel()
24 | {
25 | return (new DynamicModel(['id' => '', 'title' => '', 'author' => '', 'summary' => '', 'status' => '', 'category_id' => '']))
26 | ->addRule(['title', 'author', 'summary'], 'string')
27 | ->addRule(['id', 'status', 'category_id'], 'number');
28 | }
29 |
30 | protected function applyFilter($query, $model, $filter)
31 | {
32 | foreach (['title', 'author', 'summary'] as $name) {
33 | if (!empty($model->$name)) {
34 | $query->andWhere(['LIKE', 'a.' . $name, $model->$name]);
35 | }
36 | }
37 | foreach (['id', 'status', 'category_id'] as $name) {
38 | if (is_numeric($model->$name)) {
39 | $query->andWhere(['a.' . $name => $model->$name]);
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/Http/Form/VerifyEmailForm.php:
--------------------------------------------------------------------------------
1 | user = User::findByVerificationToken($token);
34 | if (!$this->user) {
35 | throw new InvalidArgumentException('Wrong verify email token.');
36 | }
37 | parent::__construct($config);
38 | }
39 |
40 | /**
41 | * Verify email
42 | *
43 | * @return User|null the saved model or null if saving fails
44 | */
45 | public function verifyEmail()
46 | {
47 | $user = $this->user;
48 | $user->status = User::STATUS_ACTIVE;
49 | return $user->save(false) ? $user : null;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/Behavior/ArticleBehavior.php:
--------------------------------------------------------------------------------
1 | 'saveMeta',
16 | Article::EVENT_AFTER_UPDATE => 'saveMeta',
17 | Article::EVENT_AFTER_DELETE => 'afterDelete',
18 | ];
19 | }
20 |
21 | public function saveMeta(Event $event)
22 | {
23 | /** @var Article $model */
24 | $model = $event->sender;
25 |
26 | $meta = ArticleMetaFactory::findByArticleId($model->id);
27 | if (!$meta) {
28 | $meta = ArticleMetaFactory::create($model->id, $model->content);
29 | } else {
30 | $meta->content = $model->content;
31 | }
32 | if (!$meta->save()) {
33 | Yii::error($meta->getErrors(), __METHOD__);
34 | throw new \Exception('Unable to save article meta');
35 | }
36 | }
37 |
38 | public function afterDelete(Event $event)
39 | {
40 | /** @var Article $model */
41 | $model = $event->sender;
42 |
43 | $meta = ArticleMetaFactory::findByArticleId($model->id);
44 | if ($meta && $meta->delete() === false) {
45 | throw new \Exception('Unable to delete article meta');
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/config/console.php:
--------------------------------------------------------------------------------
1 | 'app-console',
5 | 'bootstrap' => ['log', 'queue'],
6 | 'controllerNamespace' => 'App\Console\Controller',
7 | 'aliases' => [
8 | '@bower' => '@vendor/bower-asset',
9 | '@npm' => '@vendor/npm-asset',
10 | ],
11 | 'controllerMap' => [
12 | 'migrate' => [
13 | 'class' => \yii\console\controllers\MigrateController::class,
14 | 'templateFile' => '@resources/console/views/migration.php',
15 | 'migrationPath' => [
16 | '@resources/migrations',
17 | '@yii/rbac/migrations',
18 | '@yii/log/migrations/',
19 | ],
20 | 'migrationNamespaces' => [
21 | 'RazonYang\Yii2\Log\Db\Migration',
22 | 'RazonYang\Yii2\Setting\Migration',
23 | ],
24 | ],
25 | 'serve' => [
26 | 'class' => \yii\console\controllers\ServeController::class,
27 | 'docroot' => '@webroot',
28 | 'port' => 8080,
29 | ],
30 | ],
31 | 'components' => [
32 | 'log' => [
33 | 'traceLevel' => YII_DEBUG ? 3 : 0,
34 | 'targets' => [
35 | 'file' => [
36 | 'class' => yii\log\FileTarget::class,
37 | 'logFile' => '@runtime/logs/console.log',
38 | 'levels' => ['error', 'warning'],
39 | ],
40 | ],
41 | ],
42 | ],
43 | ];
44 |
--------------------------------------------------------------------------------
/resources/migrations/m190722_062914_table_session.php:
--------------------------------------------------------------------------------
1 | createTable($this->tableName, [
14 | 'id' => $this->uuid(),
15 | 'user_id' => $this->integer()->notNull()->comment('User ID'),
16 | 'token' => $this->char(32)->notNull()->unique()->comment('Access Token'),
17 | 'refresh_token' => $this->char(64)->notNull()->unique()->comment('Refresh Token'),
18 | 'ip_address' => $this->ipAddress(),
19 | 'user_agent' => $this->string()->notNull()->comment('User Agent'),
20 |
21 | 'expire_time' => $this->timestamp('Expire Time'),
22 | 'refresh_token_expire_time' => $this->timestamp('Refresh Token Expire Time'),
23 | 'create_time' => $this->createTimestamp(),
24 | 'update_time' => $this->updateTimestamp(),
25 | ], $this->tableOptions());
26 |
27 | $this->addPrimaryKey('session_pk', $this->tableName, ['id']);
28 | $this->createIndex('session_idx_user_id_expire_time', $this->tableName, ['user_id', 'expire_time']);
29 | $this->createIndex('session_idx_create_time', $this->tableName, ['create_time']);
30 | }
31 |
32 | public function safeDown()
33 | {
34 | $this->dropTable($this->tableName);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/resources/views/site/contact.php:
--------------------------------------------------------------------------------
1 | title = Yii::t('app', 'Contact Us');
11 | $this->params['breadcrumbs'][] = $this->title;
12 | ?>
13 |
14 |
= Html::encode($this->title) ?>
15 |
16 |
17 |
18 | If you have business inquiries or other questions, please fill out the following form to contact us. Thank you.
19 |
20 |
21 |
22 |
23 | 'contact-form']); ?>
24 |
25 | = $form->field($model, 'name')->textInput(['autofocus' => true]) ?>
26 |
27 | = $form->field($model, 'email') ?>
28 |
29 | = $form->field($model, 'subject') ?>
30 |
31 | = $form->field($model, 'body')->textarea(['rows' => 6]) ?>
32 |
33 | = $form->field($model, 'verifyCode')->widget(Captcha::className(), [
34 | 'template' => '
',
35 | ]) ?>
36 |
37 |
38 | = Html::submitButton('Submit', ['class' => 'btn btn-primary', 'name' => 'contact-button']) ?>
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Form/ChangePasswordForm.php:
--------------------------------------------------------------------------------
1 | 'password', 'operator' => '!='],
27 | ['new_password', PasswordValidator::class],
28 | ]);
29 | }
30 |
31 | public function validatePassword($attribute)
32 | {
33 | $user = $this->getUser();
34 | if (!$user || !$user->validatePassword($this->$attribute)) {
35 | $this->addError($attribute, Yii::t('app', 'Original password is incorrect'));
36 | }
37 | }
38 |
39 | protected function handleInternal()
40 | {
41 | $user = $this->getUser();
42 | $user->setPassword($this->new_password);
43 | if (!$user->save()) {
44 | Yii::error($user->getErrors());
45 | throw new \RuntimeException('Unable to change password');
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/resources/views/article/index.php:
--------------------------------------------------------------------------------
1 | title = Yii::t('app', 'Article');
13 | $this->params['breadcrumbs'][] = $this->title;
14 |
15 | $this->registerCssFile('/css/article.css', ['depends' => [AppAsset::class]]);
16 | ?>
17 |
18 |
19 |
20 |
21 |
24 |
25 |
29 |
30 |
= Html::encode($article->summary) ?>
31 |
32 |
33 | Created by = Html::encode($article->author) ?> and released at = Yii::$app->formatter->asDate($article->release_time) ?>
34 | = $article->views ?>
35 | = $likesCount[$article->id] ?? 0 ?>
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | = LinkPager::widget([
45 | 'pagination' => $pagination,
46 | ]);?>
--------------------------------------------------------------------------------
/resources/views/site/login.php:
--------------------------------------------------------------------------------
1 | title = Yii::t('app', 'Login');
11 | $this->params['breadcrumbs'][] = $this->title;
12 | ?>
13 |
14 |
= Html::encode($this->title) ?>
15 |
16 |
Please fill out the following fields to login:
17 |
18 |
19 |
20 | 'login-form']); ?>
21 |
22 | = $form->field($model, 'username')->textInput(['autofocus' => true]) ?>
23 |
24 | = $form->field($model, 'password')->passwordInput() ?>
25 |
26 | = $form->field($model, 'rememberMe')->checkbox() ?>
27 |
28 |
29 | No account? = Html::a('Create one!', ['/signup']) ?>
30 |
31 | If you forgot your password you can = Html::a('reset it', ['/request-password-reset']) ?>.
32 |
33 | Need new verification email? = Html::a('Resend', ['/resend-verification-email']) ?>
34 |
35 |
36 |
37 | = Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software LLC (http://www.yiisoft.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 |
--------------------------------------------------------------------------------
/app/Queue/Job.php:
--------------------------------------------------------------------------------
1 | queue;
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public function execute($queue)
29 | {
30 | $this->queue = $queue;
31 |
32 | if (!$this->beforeRun()) {
33 | throw new JobPreventedException();
34 | }
35 |
36 | $result = $this->run();
37 | $this->afterRun($result);
38 | }
39 |
40 | /**
41 | * Invoke before running job.
42 | *
43 | * @return bool whether the job is valid.
44 | */
45 | protected function beforeRun(): bool
46 | {
47 | $event = new JobEvent();
48 | $this->trigger(JobEvent::BEFORE_RUN, $event);
49 | return $event->isValid;
50 | }
51 |
52 | /**
53 | * Invoke after finishing job.
54 | */
55 | protected function afterRun($result)
56 | {
57 | $event = new JobEvent([
58 | 'result' => $result
59 | ]);
60 | $this->trigger(JobEvent::AFTER_RUN, $event);
61 | }
62 |
63 | /**
64 | * Run job.
65 | *
66 | * @return mixed job result.
67 | */
68 | abstract protected function run();
69 | }
70 |
--------------------------------------------------------------------------------
/resources/migrations/m190617_031446_table_user.php:
--------------------------------------------------------------------------------
1 | createTable($this->tableName, [
14 | 'id' => $this->primaryKey(),
15 | 'username' => $this->string()->notNull()->unique(),
16 | 'first_name' => $this->string()->notNull()->defaultValue('')->comment('First Name'),
17 | 'last_name' => $this->string()->notNull()->defaultValue('')->comment('Last Name'),
18 | 'avatar' => $this->string()->notNull()->defaultValue('')->comment('Avatar'),
19 | 'auth_key' => $this->string(32)->notNull(),
20 | 'password_hash' => $this->string()->notNull(),
21 | 'password_reset_token' => $this->string()->unique(),
22 | 'email' => $this->string()->notNull()->unique(),
23 | 'verification_token' => $this->string()->defaultValue(null),
24 | 'language' => $this->string(5)->notNull()->defaultValue('en'),
25 |
26 | 'status' => $this->status(),
27 | 'is_deleted' => $this->softDelete(),
28 | 'create_time' => $this->createTimestamp(),
29 | 'update_time' => $this->updateTimestamp(),
30 | ], $this->tableOptions());
31 |
32 | $this->createIndex('user_idx_status', $this->tableName, ['status']);
33 | $this->createIndex('user_idx_is_deleted', $this->tableName, ['is_deleted']);
34 | }
35 |
36 | public function down()
37 | {
38 | $this->dropTable($this->tableName);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/SessionController.php:
--------------------------------------------------------------------------------
1 | $actions['index'],
17 | 'delete' => $actions['delete'],
18 | 'view' => $actions['view'],
19 | 'options' => $actions['options'],
20 | ];
21 | }
22 |
23 | public function getPermission($action)
24 | {
25 | // return 'userSession' . ucfirst($action);
26 | }
27 |
28 | public function searchModel()
29 | {
30 | return (new \yii\base\DynamicModel(['id', 'username' => null, 'email' => null, 'status' => null]))
31 | ->addRule(['id', 'status'], 'integer')
32 | ->addRule(['username', 'email'], 'trim')
33 | ->addRule(['username', 'email'], 'string');
34 | }
35 |
36 | protected function getQuery($action)
37 | {
38 | return parent::getQuery($action)
39 | ->alias('us')
40 | ->orderBy(['us.create_time' => SORT_DESC]);
41 | }
42 |
43 | protected function applyFilter($query, $model, $filter)
44 | {
45 | foreach (['id', 'username', 'email'] as $name) {
46 | if (!empty($model->$name)) {
47 | $query->andFilterWhere(['LIKE', 'u.' . $name, $model->$name]);
48 | }
49 | }
50 |
51 | if (is_numeric($model->status)) {
52 | $query->andWhere(['u.status' => intval($model->status)]);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/Http/Form/ResetPasswordForm.php:
--------------------------------------------------------------------------------
1 | _user = User::findByPasswordResetToken($token);
34 | if (!$this->_user) {
35 | throw new InvalidArgumentException('Wrong password reset token.');
36 | }
37 | parent::__construct($config);
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function rules()
44 | {
45 | return [
46 | ['password', 'required'],
47 | ['password', 'string', 'min' => 6],
48 | ];
49 | }
50 |
51 | /**
52 | * Resets password.
53 | *
54 | * @return bool if password was reset.
55 | */
56 | public function resetPassword()
57 | {
58 | $user = $this->_user;
59 | $user->setPassword($this->password);
60 | $user->removePasswordResetToken();
61 |
62 | return $user->save(false);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/resources/views/site/index.php:
--------------------------------------------------------------------------------
1 | getUrlManager();
9 |
10 | $this->title = Yii::t('app', 'Home');
11 | ?>
12 |
13 |
14 |
15 |
Congratulation!
16 |
17 |
You have set up Yii2 and Vue application successfully.
18 |
19 | Yii2 App Template
20 | Yii2 Vue Admin
21 |
22 |
23 | = Carousel::widget([
24 | 'controls' => [
25 | '
Previous',
26 | '
Next'
27 | ],
28 | 'items' => [
29 | [
30 | 'content' => Html::img($urlManager->createUrl(['/img/yii2-app.gif'])),
31 | 'caption' => '
Install Yii2 Application via Docker
',
32 | 'options' => [],
33 | ],
34 | [
35 | 'content' => Html::img($urlManager->createUrl(['/img/yii2-vue.gif'])),
36 | 'caption' => '
Install Yii2 Vue Admin
',
37 | 'options' => [],
38 | ],
39 | ],
40 | 'clientOptions' => [
41 | 'interval' => false,
42 | ],
43 | ]) ?>
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Model/Role.php:
--------------------------------------------------------------------------------
1 | Item::TYPE_ROLE],
16 | ['type', 'in', 'range' => [Item::TYPE_ROLE]],
17 | ['permission_names', 'required'],
18 | [
19 | 'permission_names', 'each',
20 | 'rule' => ['exist', 'targetClass' => AuthItem::class, 'targetAttribute' => 'name', 'filter' => ['type' => Item::TYPE_PERMISSION]],
21 | ],
22 | ], parent::rules());
23 | }
24 |
25 | public function afterSave($insert, $changeAttributes)
26 | {
27 | parent::afterSave($insert, $changeAttributes);
28 | $this->savePermissions();
29 | }
30 |
31 | private function savePermissions()
32 | {
33 | if (empty($this->permission_names)) {
34 | return;
35 | }
36 |
37 | $auth = Yii::$app->getAuthManager();
38 | $role = $auth->getRole($this->name);
39 | $auth->removeChildren($role);
40 | foreach ($this->permission_names as $permissionName) {
41 | $auth->addChild($role, $auth->getPermission($permissionName));
42 | }
43 | }
44 |
45 | public function extraFields()
46 | {
47 | return [
48 | 'permission_names' => 'permissionNames',
49 | ];
50 | }
51 |
52 | public function getPermissionNames(): array
53 | {
54 | $permissions = Yii::$app->getAuthManager()->getPermissionsByRole($this->name);
55 | return array_column($permissions, 'name');
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Form/LoginForm.php:
--------------------------------------------------------------------------------
1 | getDb()->beginTransaction();
18 | try {
19 | $user = $this->getUser();
20 |
21 | $session = SessionFactory::create(
22 | $user->id,
23 | Yii::$app->params['user.session.duration'],
24 | Yii::$app->params['user.session.refreshTokenDuration'],
25 | Yii::$app->getRequest()
26 | );
27 | if (!$session->save()) {
28 | Yii::error($session->getErrors(), __METHOD__);
29 | throw new \RuntimeException('Unable to save session' . \yii\helpers\VarDumper::dumpAsString($session->getErrors()));
30 | }
31 |
32 | Yii::$app->user->login($user, $this->rememberMe ? 3600 * 24 * 30 : 0);
33 | $transaction->commit();
34 |
35 | return [
36 | 'id' => $user->id,
37 | 'username' => $user->username,
38 | 'email' => $user->email,
39 | 'token' => $session->token,
40 | 'expires_in' => $session->getExpiresIn(),
41 | 'refresh_token' => $session->refresh_token,
42 | 'refresh_token_expire_in' => $session->getRefreshTokenExpiresIn(),
43 | ];
44 | } catch (\Throwable $e) {
45 | $transaction->rollBack();
46 | throw $e;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/Model/ArticleCategory.php:
--------------------------------------------------------------------------------
1 | 32],
46 | ];
47 | }
48 |
49 | /**
50 | * {@inheritdoc}
51 | */
52 | public function attributeLabels()
53 | {
54 | return [
55 | 'id' => Yii::t('app', 'ID'),
56 | 'creator' => Yii::t('app', 'Creator'),
57 | 'name' => Yii::t('app', 'Name'),
58 | 'create_time' => Yii::t('app', 'Create Time'),
59 | 'update_time' => Yii::t('app', 'Update Time'),
60 | ];
61 | }
62 |
63 | public function fields()
64 | {
65 | return [
66 | 'id',
67 | 'name',
68 | 'create_time',
69 | 'update_time',
70 | ];
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/Functional/Form/LoginFormTest.php:
--------------------------------------------------------------------------------
1 | [
22 | 'class' => UserFixture::className(),
23 | 'dataFile' => codecept_data_dir() . 'user.php'
24 | ]
25 | ];
26 | }
27 |
28 | public function testLoginNoUser()
29 | {
30 | $model = new LoginForm([
31 | 'username' => 'not_existing_username',
32 | 'password' => 'not_existing_password',
33 | ]);
34 |
35 | $model->handle();
36 | $this->assertTrue($model->hasErrors());
37 | $this->assertArrayHasKey('password', $model->getErrors());
38 | $this->assertTrue(Yii::$app->user->isGuest);
39 | }
40 |
41 | public function testLoginWrongPassword()
42 | {
43 | $model = new LoginForm([
44 | 'username' => 'bayer.hudson',
45 | 'password' => 'wrong_password',
46 | ]);
47 |
48 | $model->handle();
49 | $this->assertTrue($model->hasErrors());
50 | $this->assertArrayHasKey('password', $model->getErrors());
51 | $this->assertTrue(Yii::$app->user->isGuest);
52 | }
53 |
54 | public function testLoginCorrect()
55 | {
56 | $model = new LoginForm([
57 | 'username' => 'bayer.hudson',
58 | 'password' => 'password_0',
59 | ]);
60 |
61 | $model->handle();
62 | $this->assertFalse($model->hasErrors());
63 | $this->assertFalse(Yii::$app->user->isGuest);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/resources/migrations/m190731_024219_table_article.php:
--------------------------------------------------------------------------------
1 | createTable($this->tableName, [
14 | 'id' => $this->primaryKey(),
15 | 'category_id' => $this->integer()->notNull()->comment('Category ID'),
16 | 'creator' => $this->integer()->notNull()->comment('Creator ID'),
17 | 'title' => $this->string()->notNull()->comment('Title'),
18 | 'author' => $this->string()->notNull()->defaultValue('')->comment('Author'),
19 | 'summary' => $this->string()->notNull()->comment('Summary'),
20 | 'cover' => $this->string()->notNull()->comment('Cover'),
21 | 'status' => $this->status(),
22 | 'is_deleted' => $this->softDelete(),
23 | 'views' => $this->integer()->unsigned()->notNull()->defaultValue(0),
24 | 'version' => $this->optimisticLock(),
25 | 'release_time' => $this->timestamp('Release Time'),
26 | 'create_time' => $this->createTimestamp(),
27 | 'update_time' => $this->updateTimestamp(),
28 | ], $this->tableOptions());
29 |
30 | $this->createIndex('article_idx_creator', $this->tableName, ['creator']);
31 | $this->createIndex('article_idx_status', $this->tableName, ['status']);
32 | $this->createIndex('article_idx_is_deleted', $this->tableName, ['is_deleted']);
33 | $this->createIndex('article_idx_release_time', $this->tableName, ['release_time']);
34 | $this->createIndex('article_idx_category_id', $this->tableName, 'category_id');
35 | }
36 |
37 | public function safeDown()
38 | {
39 | $this->dropTable($this->tableName);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/Http/Form/ContactForm.php:
--------------------------------------------------------------------------------
1 | Yii::t('app', 'Name'),
40 | 'email' => Yii::t('app', 'Email'),
41 | 'subject' => Yii::t('app', 'Subject'),
42 | 'body' => Yii::t('app', 'Content'),
43 | 'verifyCode' => Yii::t('app', 'Verification Code'),
44 | ];
45 | }
46 |
47 | /**
48 | * Sends an email to the specified email address using the information collected by this model.
49 | *
50 | * @param string $email the target email address
51 | * @return bool whether the email was sent
52 | */
53 | public function sendEmail($email)
54 | {
55 | return Yii::$app->getMailer()
56 | ->compose()
57 | ->setTo($email)
58 | ->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
59 | ->setReplyTo([$this->email => $this->name])
60 | ->setSubject($this->subject)
61 | ->setTextBody($this->body)
62 | ->send();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/Http/Form/PasswordResetRequestForm.php:
--------------------------------------------------------------------------------
1 | User::class,
27 | 'filter' => ['status' => User::STATUS_ACTIVE],
28 | 'message' => 'There is no user with this email address.'
29 | ],
30 | ];
31 | }
32 |
33 | /**
34 | * Sends an email with a link, for resetting the password.
35 | *
36 | * @return bool whether the email was send
37 | */
38 | public function sendEmail()
39 | {
40 | /* @var $user User */
41 | $user = User::findOne([
42 | 'status' => User::STATUS_ACTIVE,
43 | 'email' => $this->email,
44 | ]);
45 |
46 | if (!$user) {
47 | return false;
48 | }
49 |
50 | if (!User::isPasswordResetTokenValid($user->password_reset_token)) {
51 | $user->generatePasswordResetToken();
52 | if (!$user->save()) {
53 | return false;
54 | }
55 | }
56 |
57 | return Yii::$app
58 | ->mailer
59 | ->compose(
60 | ['html' => 'passwordResetToken-html', 'text' => 'passwordResetToken-text'],
61 | ['user' => $user]
62 | )
63 | ->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name . ' robot'])
64 | ->setTo($this->email)
65 | ->setSubject('Password reset for ' . Yii::$app->name)
66 | ->send();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/Factory/SessionFactory.php:
--------------------------------------------------------------------------------
1 | getUserIP();
24 | $userAgent = mb_substr($request->getUserAgent(), 0, 255);
25 | $token = static::generateToken($userId);
26 | $refeshToken = static::generateRefreshToken($userId);
27 | $now = time();
28 | return new Session([
29 | 'id' => Uuid::uuid4()->toString(),
30 | 'token' => $token,
31 | 'refresh_token' => $refeshToken,
32 | 'user_id' => $userId,
33 | 'expire_time' => $now + $duration,
34 | 'refresh_token_expire_time' => $now + $refeshDuration,
35 | 'ip_address' => $ip,
36 | 'user_agent' => $userAgent,
37 | ]);
38 | }
39 |
40 | /**
41 | * Generates access token.
42 | *
43 | * @param int $userId user ID.
44 | *
45 | * @return string
46 | */
47 | public static function generateToken(int $userId): string
48 | {
49 | return ($userId % 10) . Yii::$app->getSecurity()->generateRandomString(31);
50 | }
51 |
52 | /**
53 | * Generates refresh token.
54 | *
55 | * @param int $userId user ID.
56 | *
57 | * @return string
58 | */
59 | public static function generateRefreshToken(int $userId): string
60 | {
61 | return ($userId % 10) . Yii::$app->getSecurity()->generateRandomString(63);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/resources/views/article/view.php:
--------------------------------------------------------------------------------
1 | title = Html::encode($model->title);
11 | $this->params['breadcrumbs'][] = ['label' => Yii::t('app', 'Article'), 'url' => Url::to('/articles')];
12 | $this->params['breadcrumbs'][] = $this->title;
13 |
14 | $this->registerJsFile(Yii::$app->getUrlManager()->createUrl('/js/article.js'), ['depends' => AppAsset::class]);
15 | ?>
16 |
17 |
18 | = Html::encode($model->title) ?>
19 | = Html::encode($model->category->name ?? '') ?>
20 |
21 |
22 |
23 |
24 |
25 | Created by = Html::encode($model->author) ?> and released at = Yii::$app->formatter->asDate($model->release_time) ?>
26 | = $model->views ?>
27 | getUser()->getIsGuest()): ?>
28 |
29 |
30 | = Html::icon('heart', [
31 | 'style' => $model->getHasLiked() ? '' : 'display: none;',
32 | 'id' => 'btn-dislike',
33 | 'data-id' => $model->id,
34 | ]) ?>
35 | = Html::icon('heart-empty', [
36 | 'style' => $model->getHasLiked() ? 'display: none;' : '',
37 | 'id' => 'btn-like',
38 | 'data-id' => $model->id,
39 | ]) ?>
40 |
41 | = $model->getLikesCount() ?>
42 |
43 |
44 | = $content ?>
45 |
46 |
47 |
48 | = Html::beginForm(['/article/comments']) ?>
49 | = Html::textarea('content', '', ['class' => 'form-control']) ?>
50 | = Html::button('Submit', ['class' => 'btn btn-primary']) ?>
51 | = Html::endForm() ?>
52 |
53 |
--------------------------------------------------------------------------------
/public/js/article.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | var likeLock = false;
3 | var likesCount = $('#likes-count');
4 | var btnLike = $('#btn-like');
5 | var btnDislike = $('#btn-dislike');
6 |
7 | $('#comment-items').load('/articles/1/comments');
8 |
9 | function updateLikesCounter(count) {
10 | likesCount.text(parseInt(likesCount.text()) + count);
11 | }
12 |
13 | function acuquireLikeLock() {
14 | if (likeLock) {
15 | return false;
16 | }
17 |
18 | likeLock = true;
19 | return true;
20 | }
21 |
22 | function releaseLikeLock() {
23 | likeLock = false;
24 | }
25 |
26 | btnLike.click(function() {
27 | var id = $(this).attr('data-id');
28 | $.ajax('/api/frontend/v1/articles/' + id + '/likes', {
29 | method: 'PUT',
30 | dataType: 'json',
31 | beforeSend: function() {
32 | if (!acuquireLikeLock()) {
33 | return false;
34 | }
35 | },
36 | success: function(response) {
37 | updateLikesCounter(1);
38 | btnDislike.show();
39 | btnLike.hide();
40 | },
41 | complete: function() {
42 | releaseLikeLock();
43 | }
44 | });
45 | });
46 |
47 | btnDislike.click(function() {
48 | var id = $(this).attr('data-id');
49 | $.ajax('/api/frontend/v1/articles/' + id + '/likes', {
50 | method: 'DELETE',
51 | dataType: 'json',
52 | beforeSend: function() {
53 | if (!acuquireLikeLock()) {
54 | return false;
55 | }
56 | },
57 | success: function(response) {
58 | updateLikesCounter(-1);
59 | btnLike.show();
60 | btnDislike.hide();
61 | },
62 | complete: function() {
63 | releaseLikeLock();
64 | }
65 | });
66 | });
67 | });
--------------------------------------------------------------------------------
/app/Http/Form/ResendVerificationEmailForm.php:
--------------------------------------------------------------------------------
1 | getUser();
32 | if (!$user) {
33 | $this->addError($attribute, 'There is no user with this email address.');
34 | return;
35 | }
36 | if ($user->isActive()) {
37 | $this->addError($attribute, 'The email was verified.');
38 | return;
39 | }
40 | }
41 |
42 | /**
43 | * Sends confirmation email to user
44 | *
45 | * @return bool whether the email was sent
46 | */
47 | public function sendEmail()
48 | {
49 | $user = User::findOne([
50 | 'email' => $this->email,
51 | ]);
52 |
53 | if ($user === null) {
54 | return false;
55 | }
56 |
57 | return Yii::$app
58 | ->mailer
59 | ->compose(
60 | ['html' => 'emailVerify-html', 'text' => 'emailVerify-text'],
61 | ['user' => $user]
62 | )
63 | ->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name . ' robot'])
64 | ->setTo($this->email)
65 | ->setSubject('Account registration at ' . Yii::$app->name)
66 | ->send();
67 | }
68 |
69 | private $user;
70 |
71 | public function getUser(): ?User
72 | {
73 | if ($this->user === null) {
74 | $this->user = User::findOne(['email' => $this->email]);
75 | }
76 |
77 | return $this->user;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/Http/Api/Frontend/Controller/ArticleController.php:
--------------------------------------------------------------------------------
1 | alias('a')
27 | ->orderBy([
28 | 'a.release_time' => SORT_DESC
29 | ])
30 | ->andWhere([
31 | 'a.is_deleted' => 0,
32 | 'a.status' => StatusInterface::STATUS_ACTIVE,
33 | ])
34 | ->andWhere(['<=', 'a.release_time', time()]);
35 | }
36 |
37 | public function searchModel()
38 | {
39 | return (new DynamicModel(['title' => '', 'author' => '', 'summary' => '']))
40 | ->addRule(['title', 'author', 'summary'], 'string');
41 | }
42 |
43 | protected function applyFilter($query, $model, $filter)
44 | {
45 | foreach (['title', 'author', 'summary'] as $name) {
46 | if (!empty($model->$name)) {
47 | $query->andWhere(['LIKE', 'a.' . $name, $model->$name]);
48 | }
49 | }
50 | }
51 |
52 | /**
53 | * Likes an article.
54 | */
55 | public function actionLike($id)
56 | {
57 | $model = new ArticleLikeForm();
58 | $model->load(['id' => $id], '');
59 | return $model->handle();
60 | }
61 |
62 | /**
63 | * Dislikes an article.
64 | */
65 | public function actionDislike($id)
66 | {
67 | $model = new ArticleDislikeForm();
68 | $model->load(['id' => $id], '');
69 | return $model->handle();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/docs/en/INSTALLATION.md:
--------------------------------------------------------------------------------
1 | INSTALLATION
2 | ------------
3 |
4 | ```
5 | $ composer create-project --prefer-dist razonyang/yii2-app-template app
6 | ```
7 |
8 | or
9 |
10 | ```
11 | $ git clone https://github.com/razonyang/yii2-app-template app
12 | $ cd app
13 | $ composer install
14 | ```
15 |
16 | INITIALIZE
17 | ----------
18 |
19 | ```shell
20 | $ cd app
21 |
22 | $ ./bin/init
23 |
24 | $ ./bin/yii migrate
25 | ```
26 |
27 | - `./bin/init` for initializing
28 | - `./bin/yii migrate` for database migrations
29 |
30 | > You should modify configurations located in `config` before `migrate`.
31 |
32 | WEB
33 | ---
34 |
35 | ```shell
36 | $ ./bin/yii serve
37 |
38 | $ curl -XPOST http://localhost:8080/api/backend/v1/login -H "Content-Type: application/json"
39 | {"status":"fail","data":{"username":"Username cannot be blank.","password":"Password cannot be blank."}}
40 |
41 | $ curl -XPOST http://localhost:8080/api/backend/v1/login -H "Content-Type: application/json" -d '{"username": "Admin", "password": "123456"}'
42 | {"status":"success","data":{"id":1,"username":"Admin","email":"admin@example.com","token":"1YG-bENSZ7uQWWUVcytBZ-Vr7uHikT5I","expires_in":7200,"refresh_token":"1O3LL5FlVUBtdsKh2Dgj_r83jgmEbOCVtC1oAObpIhKs8QIpqdPV66t1SCywfL8k","refresh_token_expire_in":403200}}
43 | ```
44 |
45 | Open [http://localhost:8080](http://localhost:8080) to take a look of the front end.
46 |
47 | CONSOLE
48 | -------
49 |
50 | ```
51 | $ ./bin/yii hello
52 | Hello World
53 | ```
54 |
55 | QUEUE
56 | -----
57 |
58 | ```shell
59 | $ ./bin/yii hello/job
60 | App\Console\Job\HelloJob#1
61 |
62 | $ ./bin/yii queue/listen -v
63 | 2019-08-06 12:30:28 [pid: 26514] - Worker is started
64 | 2019-08-06 12:30:51 [1] App\Console\Job\HelloJob (attempt: 1, pid: 26514) - Started
65 | Hello World
66 | 2019-08-06 12:30:51 [1] App\Console\Job\HelloJob (attempt: 1, pid: 26514) - Done (0.081 s)
67 | ```
68 |
69 | TESTS
70 | -----
71 |
72 | You should create test database(default to `yiitest`) and apply migrations into it.
73 |
74 | ```shell
75 | $ ./bin/yii_test migrate
76 | ```
77 |
78 | ```shell
79 | $ ./vendor/bin/codecept run --coverage --coverage-xml
80 | ```
81 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "razonyang/yii2-app-template",
3 | "description": "Yii2 App Project Template",
4 | "keywords": ["yii2", "framework", "advanced", "project template"],
5 | "homepage": "https://github.com/razonyang/yii2-app-template/issues",
6 | "type": "project",
7 | "license": "BSD-3-Clause",
8 | "support": {
9 | "issues": "https://github.com/razonyang/yii2-app-template/issues?state=open",
10 | "forum": "http://www.yiiframework.com/forum/",
11 | "wiki": "http://www.yiiframework.com/wiki/",
12 | "irc": "irc://irc.freenode.net/yii",
13 | "source": "https://github.com/yiisoft/yii2"
14 | },
15 | "minimum-stability": "dev",
16 | "prefer-stable": true,
17 | "require": {
18 | "php": ">=7.1",
19 | "ext-mbstring": "*",
20 | "ramsey/uuid": "^3.8",
21 | "razonyang/yii2-jsend": "^1.0.1",
22 | "razonyang/yii2-log-target-db": "^1.0",
23 | "razonyang/yii2-rate-limiter": "^1.0",
24 | "razonyang/yii2-setting": "^1.0",
25 | "razonyang/yii2-uploader": "^1.0",
26 | "yiisoft/yii2": "~2.0.23",
27 | "yiisoft/yii2-bootstrap": "~2.0.0",
28 | "yiisoft/yii2-queue": "^2.2",
29 | "yiisoft/yii2-redis": "^2.0",
30 | "yiisoft/yii2-swiftmailer": "~2.1.0"
31 | },
32 | "require-dev": {
33 | "codeception/codeception": "^3.0",
34 | "codeception/verify": "^1.1",
35 | "phpunit/phpunit": "^7",
36 | "facebook/webdriver": "^1.7",
37 | "symfony/browser-kit": ">=2.7 <=4.2.4",
38 | "yiisoft/yii2-debug": "~2.1.0",
39 | "yiisoft/yii2-gii": "~2.1.0"
40 | },
41 | "config": {
42 | "process-timeout": 1800,
43 | "fxp-asset": {
44 | "enabled": false
45 | }
46 | },
47 | "repositories": [
48 | {
49 | "type": "composer",
50 | "url": "https://asset-packagist.org"
51 | }
52 | ],
53 | "autoload": {
54 | "psr-4": {
55 | "App\\": "app/"
56 | }
57 | },
58 | "autoload-dev": {
59 | "psr-4": {
60 | "App\\Tests\\": "tests"
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Model/User.php:
--------------------------------------------------------------------------------
1 | ['validateRole']],
17 | ]);
18 | }
19 |
20 | public function validateRole($attribute)
21 | {
22 | $roleName = $this->$attribute;
23 | if (!Yii::$app->getAuthManager()->getRole($roleName)) {
24 | $this->addError($attribute, Yii::t('yii', '{attribute} is invalid.', [
25 | 'attribute' => sprintf("%s '%s'", $this->getAttributeLabel($attribute), $roleName)
26 | ]));
27 | }
28 | }
29 |
30 | public function afterSave($insert, $changedAttributes)
31 | {
32 | parent::afterSave($insert, $changedAttributes);
33 |
34 | $this->saveRoles();
35 | }
36 |
37 | private function saveRoles()
38 | {
39 | if (empty($this->role_names)) {
40 | return;
41 | }
42 |
43 | $auth = Yii::$app->getAuthManager();
44 | // revoke
45 | $auth->revokeAll($this->id);
46 | // assign
47 | foreach ($this->role_names as $name) {
48 | $auth->assign($auth->getRole($name), $this->id);
49 | }
50 | }
51 |
52 | public function extraFields()
53 | {
54 | return [
55 | 'roles',
56 | 'role_names' => 'roleNames',
57 | ];
58 | }
59 |
60 | public function getRoles()
61 | {
62 | $roles = Yii::$app->getAuthManager()->getRolesByUser($this->id);
63 | return ArrayHelper::getColumn($roles, function ($element) {
64 | return [
65 | 'name' => $element->name,
66 | 'description' => $element->description,
67 | ];
68 | }, false);
69 | }
70 |
71 | public function getRoleNames()
72 | {
73 | $roles = Yii::$app->getAuthManager()->getRolesByUser($this->id);
74 | return array_column($roles, 'name');
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/UserController.php:
--------------------------------------------------------------------------------
1 | load(Yii::$app->getRequest()->post(), '');
30 | return $form->handle();
31 | }
32 |
33 | public function actionRefreshAccessToken()
34 | {
35 | $form = new SessionRefreshForm();
36 | $form->load(Yii::$app->getRequest()->post(), '');
37 | return $form->handle();
38 | }
39 |
40 | public function actionLogout()
41 | {
42 | $form = new LogoutForm();
43 | return $form->handle();
44 | }
45 |
46 | public function searchModel()
47 | {
48 | return (new \yii\base\DynamicModel(['id', 'username' => null, 'email' => null, 'status' => null]))
49 | ->addRule(['id', 'status'], 'integer')
50 | ->addRule(['username', 'email'], 'trim')
51 | ->addRule(['username', 'email'], 'string');
52 | }
53 |
54 | protected function getQuery($action)
55 | {
56 | return parent::getQuery($action)
57 | ->alias('u')
58 | ->andWhere(['is_deleted' => 0])
59 | ->orderBy(['id' => SORT_DESC]);
60 | }
61 |
62 | protected function applyFilter($query, $model, $filter)
63 | {
64 | foreach (['id', 'username', 'email'] as $name) {
65 | if (!empty($model->$name)) {
66 | $query->andFilterWhere(['LIKE', 'u.' . $name, $model->$name]);
67 | }
68 | }
69 |
70 | if (is_numeric($model->status)) {
71 | $query->andWhere(['u.status' => intval($model->status)]);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/Model/ArticleLike.php:
--------------------------------------------------------------------------------
1 | TimestampBehavior::class,
34 | 'updatedAtAttribute' => null,
35 | ],
36 | [
37 | 'class' => CreatorBehavior::class,
38 | 'creatorAttribute' => 'user_id',
39 | ],
40 | ];
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function rules()
47 | {
48 | return [
49 | [['article_id'], 'required'],
50 | [['article_id', 'create_time'], 'integer'],
51 | // [['article_id', 'user_id'], 'unique', 'targetAttribute' => ['article_id', 'user_id']],
52 | ['article_id', 'exist', 'targetClass' => Article::class, 'targetAttribute' => 'id'],
53 | ];
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | public function attributeLabels()
60 | {
61 | return [
62 | 'article_id' => Yii::t('app', 'Article ID'),
63 | 'user_id' => Yii::t('app', 'User ID'),
64 | 'create_time' => Yii::t('app', 'Create Time'),
65 | ];
66 | }
67 |
68 | public function getArticle()
69 | {
70 | return $this->hasOne(Article::class, ['id' => 'article_id']);
71 | }
72 |
73 | public function getUser()
74 | {
75 | return $this->hasOne(User::class, ['id' => 'user_id']);
76 | }
77 |
78 | public static function findByArticleIdAndUserId(int $articleId, int $userId): ?self
79 | {
80 | return static::findOne([
81 | 'article_id' => $articleId,
82 | 'user_id' => $userId,
83 | ]);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/docs/en/DOCKER.md:
--------------------------------------------------------------------------------
1 | INSTALLATION
2 | ------------
3 |
4 | ```
5 | $ git clone https://github.com/razonyang/yii2-app-template app
6 | $ cd app
7 | ```
8 |
9 | INITIALIZE
10 | ----------
11 |
12 | ```
13 | $ cd app
14 |
15 | $ docker-compose start php-fpm
16 |
17 | $ docker-compose exec php-fpm php -r "file_put_contents('/app/composer.phar', file_get_contents('https://getcomposer.org/composer.phar'));"
18 |
19 | $ docker-compose exec php-fpm php /app/bin/init --env=Docker
20 |
21 | $ docker-compose exec php-fpm php /app/composer.phar --working-dir=/app install
22 |
23 | $ docker-compose up --scale queue=5
24 | ```
25 |
26 | - `./bin/init --env=Docker` for initializing docker configurations
27 | - `docker-compose up` for starting docker services, add `-d` to run as daemon, `--scale queue=5` starts `5` queue workers.
28 |
29 |
30 | DATABASE MIGRATION
31 | ------------------
32 |
33 | ```shell
34 | $ docker-compose exec php-fpm /app/bin/yii migrate
35 | ```
36 |
37 | WEB
38 | ---
39 |
40 | ```shell
41 | $ curl -XPOST http://localhost:8080/api/backend/v1/login -H "Content-Type: application/json"
42 | {"status":"fail","data":{"username":"Username cannot be blank.","password":"Password cannot be blank."}}
43 |
44 | $ curl -XPOST http://localhost:8080/api/backend/v1/login -H "Content-Type: application/json" -d '{"username": "Admin", "password": "123456"}'
45 | {"status":"success","data":{"id":1,"username":"Admin","email":"admin@example.com","token":"1YG-bENSZ7uQWWUVcytBZ-Vr7uHikT5I","expires_in":7200,"refresh_token":"1O3LL5FlVUBtdsKh2Dgj_r83jgmEbOCVtC1oAObpIhKs8QIpqdPV66t1SCywfL8k","refresh_token_expire_in":403200}}
46 | ```
47 |
48 | Open [http://localhost:8080](http://localhost:8080) to take a look of the front end.
49 |
50 | CONSOLE
51 | -------
52 |
53 | ```shell
54 | $ docker-compose exec php-fpm /app/bin/yii hello
55 | Hello World
56 | ```
57 |
58 | QUEUE
59 | -----
60 |
61 | ```shell
62 | $ docker-compose exec php-fpm /app/bin/yii hello/job
63 | App\Console\Job\HelloJob#1
64 | ```
65 |
66 | CRON
67 | ----
68 |
69 | You should add your cron jobs into `resources/docker/cron/crontab`, and restart cron service `docker-compose restart cron`.
70 |
71 | TESTS
72 | -----
73 |
74 | You should create test database(default to `yiitest`) and apply migrations into it.
75 |
76 | ```shell
77 | $ docker-compose exec db mysql -uroot -p
78 | $ docker-compose exec php-fpm /app/bin/yii_test migrate
79 | ```
80 |
81 | ```shell
82 | $ docker-compose exec php-fpm bash -c "cd /app/ && ./vendor/bin/codecept run --coverage --coverage-xml"
83 | ```
84 |
--------------------------------------------------------------------------------
/app/Http/Filter/ContentNegotiator.php:
--------------------------------------------------------------------------------
1 | languageParam) || ($language = $request->get($this->languageParam)) === null) {
25 | $language = null;
26 | $this->user = Instance::ensure($this->user, User::class);
27 | if (!$this->user->getIsGuest() && ($identity = $this->user->getIdentity()) instanceof LanguagePickerInterface) {
28 | $language = $identity->getLanguage();
29 |
30 | $picked = $this->pick($language);
31 | if ($picked !== null) {
32 | return $picked;
33 | }
34 | }
35 |
36 | $language = $request->getCookies()->getValue($this->languageParam, null);
37 | $picked = $this->pick($language);
38 | if ($picked !== null) {
39 | return $picked;
40 | }
41 | }
42 |
43 | return parent::negotiateLanguage($request);
44 | }
45 |
46 | /**
47 | * Pick language.
48 | *
49 | * @param string $language
50 | *
51 | * @return string|null returns a supportted language, returns null if not supported.
52 | */
53 | protected function pick($language)
54 | {
55 | if ($language === null) {
56 | return null;
57 | }
58 |
59 | if (isset($this->languages[$language])) {
60 | return $this->languages[$language];
61 | }
62 | foreach ($this->languages as $key => $supported) {
63 | if (is_int($key) && $this->isLanguageSupported($language, $supported)) {
64 | return $supported;
65 | }
66 | }
67 |
68 | return null;
69 | }
70 |
71 | /**
72 | * Store language to cookie.
73 | *
74 | * @param string $lang
75 | */
76 | public function storeLanguageToCookie($lang)
77 | {
78 | $cookie = new Cookie(['name' => $this->languageParam, 'value' => $lang]);
79 | Yii::$app->getResponse()->getCookies()->add($cookie);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/config/routes.php:
--------------------------------------------------------------------------------
1 | 'site/index',
5 | 'GET /captcha' => 'site/captcha',
6 | 'GET,POST /contact' => 'site/contact',
7 | 'GET,POST /login' => 'site/login',
8 | 'POST /logout' => 'site/logout',
9 | 'GET,POST /signup' => 'site/signup',
10 | 'GET,POST /reset-password' => 'site/reset-password',
11 | 'GET,POST /request-password-reset' => 'site/request-password-reset',
12 | 'GET /verify-email' => 'site/verify-email',
13 | 'GET,POST /resend-verification-email' => 'site/resend-verification-email',
14 | 'GET /about' => 'site/about',
15 |
16 | 'GET /articles' => 'article/index',
17 | 'GET /articles/' => 'article/view',
18 | 'GET /articles//comments' => 'article/comments',
19 |
20 | // API
21 | [
22 | 'class' => \yii\web\GroupUrlRule::class,
23 | 'prefix' => 'api/backend/v1',
24 | 'rules' => [
25 | 'POST upload/' => 'upload/',
26 | 'POST ' => 'user/',
27 | 'PUT access-token' => 'user/refresh-access-token',
28 | 'OPTIONS ' => 'user/options',
29 |
30 | 'GET my/info' => 'my/info',
31 | 'PUT my/password' => 'my/password',
32 |
33 | 'POST upload/' => 'upload/',
34 | 'OPTIONS upload/' => 'upload/options',
35 | ],
36 | ],
37 | [
38 | 'class' => \yii\rest\UrlRule::class,
39 | 'controller' => [
40 | // backend
41 | 'api/backend/v1/user',
42 | 'api/backend/v1/article',
43 | 'api/backend/v1/article-category',
44 |
45 | // frontend
46 | 'api/frontend/v1/article-comment',
47 | ],
48 | ],
49 |
50 | [
51 | 'class' => \yii\rest\UrlRule::class,
52 | 'controller' => [
53 | 'api/backend/v1/permission',
54 | 'api/backend/v1/role',
55 | 'api/backend/v1/my/sessions' => 'api/backend/v1/my-session',
56 | 'api/backend/v1/setting',
57 | ],
58 | 'tokens' => [
59 | '{id}' => '',
60 | ],
61 | ],
62 |
63 | [
64 | 'class' => \yii\rest\UrlRule::class,
65 | 'controller' => [
66 | 'api/frontend/v1/article',
67 | ],
68 | 'extraPatterns' => [
69 | 'PUT {id}/likes' => 'like',
70 | 'DELETE {id}/likes' => 'dislike',
71 | ],
72 | ],
73 | ];
74 |
--------------------------------------------------------------------------------
/public/css/app.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | }
5 |
6 | .wrap {
7 | min-height: 100%;
8 | height: auto;
9 | margin: 0 auto -60px;
10 | padding: 0 0 60px;
11 | }
12 |
13 | .wrap > .container {
14 | padding: 70px 15px 20px;
15 | }
16 |
17 | .footer {
18 | height: 60px;
19 | background-color: #f5f5f5;
20 | border-top: 1px solid #ddd;
21 | padding-top: 20px;
22 | }
23 |
24 | .jumbotron {
25 | text-align: center;
26 | background-color: transparent;
27 | }
28 |
29 | .jumbotron .btn {
30 | font-size: 21px;
31 | padding: 14px 24px;
32 | }
33 |
34 | .not-set {
35 | color: #c55;
36 | font-style: italic;
37 | }
38 |
39 | /* add sorting icons to gridview sort links */
40 | a.asc:after, a.desc:after {
41 | position: relative;
42 | top: 1px;
43 | display: inline-block;
44 | font-family: 'Glyphicons Halflings';
45 | font-style: normal;
46 | font-weight: normal;
47 | line-height: 1;
48 | padding-left: 5px;
49 | }
50 |
51 | a.asc:after {
52 | content: "\e151";
53 | }
54 |
55 | a.desc:after {
56 | content: "\e152";
57 | }
58 |
59 | .sort-numerical a.asc:after {
60 | content: "\e153";
61 | }
62 |
63 | .sort-numerical a.desc:after {
64 | content: "\e154";
65 | }
66 |
67 | .sort-ordinal a.asc:after {
68 | content: "\e155";
69 | }
70 |
71 | .sort-ordinal a.desc:after {
72 | content: "\e156";
73 | }
74 |
75 | .grid-view td {
76 | white-space: nowrap;
77 | }
78 |
79 | .grid-view .filters input,
80 | .grid-view .filters select {
81 | min-width: 50px;
82 | }
83 |
84 | .hint-block {
85 | display: block;
86 | margin-top: 5px;
87 | color: #999;
88 | }
89 |
90 | .error-summary {
91 | color: #a94442;
92 | background: #fdf7f7;
93 | border-left: 3px solid #eed3d7;
94 | padding: 10px 20px;
95 | margin: 0 0 15px 0;
96 | }
97 |
98 | /* align the logout "link" (button in form) of the navbar */
99 | .nav li > form > button.logout {
100 | padding: 15px;
101 | border: none;
102 | }
103 |
104 | @media(max-width:767px) {
105 | .nav li > form > button.logout {
106 | display:block;
107 | text-align: left;
108 | width: 100%;
109 | padding: 10px 15px;
110 | }
111 | }
112 |
113 | .nav > li > form > button.logout:focus,
114 | .nav > li > form > button.logout:hover {
115 | text-decoration: none;
116 | }
117 |
118 | .nav > li > form > button.logout:focus {
119 | outline: none;
120 | }
--------------------------------------------------------------------------------
/app/Http/Api/Backend/Controller/ControllerTrait.php:
--------------------------------------------------------------------------------
1 | [
18 | 'class' => Cors::class,
19 | 'cors' => [
20 | 'Origin' => Yii::$app->params['api.cors.origin'],
21 | 'Access-Control-Request-Method' => Yii::$app->params['api.cors.methods'],
22 | 'Access-Control-Request-Headers' => Yii::$app->params['api.cors.headers'],
23 | 'Access-Control-Allow-Credentials' => Yii::$app->params['api.cors.allowCredentials'],
24 | 'Access-Control-Max-Age' => Yii::$app->params['api.cors.maxAge'],
25 | 'Access-Control-Expose-Headers' => Yii::$app->params['api.cors.exposeHeaders'],
26 | ],
27 | ],
28 | 'authenticator' => [
29 | 'class' => Authenticator::class,
30 | 'optional' => [
31 | 'options',
32 | ],
33 | 'authMethods' => [
34 | 'query' => [
35 | 'class' => QueryParamAuth::class,
36 | 'tokenParam' => 'access_token',
37 | ],
38 | 'bearer' => [
39 | 'class' => HttpBearerAuth::class,
40 | ],
41 | ],
42 | ],
43 | 'verb' => [
44 | 'class' => VerbFilter::class,
45 | 'actions' => $this->verbs(),
46 | ],
47 | 'rateLimiter' => [
48 | 'class' => RateLimiter::class,
49 | 'redis' => 'redis',
50 | 'capacity' => Yii::$app->params['api.rateLimiter.capacity'],
51 | 'rate' => Yii::$app->params['api.rateLimiter.rate'],
52 | 'limitPeriod' => Yii::$app->params['api.rateLimiter.limitPeriod'],
53 | 'prefix' => Yii::$app->params['api.rateLimiter.prefix'],
54 | 'ttl' => Yii::$app->params['api.rateLimiter.ttl'],
55 | ],
56 | ];
57 | }
58 |
59 | /**
60 | * @return array
61 | */
62 | protected function verbs()
63 | {
64 | return [];
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/Http/Api/Frontend/Controller/ControllerTrait.php:
--------------------------------------------------------------------------------
1 | [
18 | 'class' => Cors::class,
19 | 'cors' => [
20 | 'Origin' => Yii::$app->params['api.cors.origin'],
21 | 'Access-Control-Request-Method' => Yii::$app->params['api.cors.methods'],
22 | 'Access-Control-Request-Headers' => Yii::$app->params['api.cors.headers'],
23 | 'Access-Control-Allow-Credentials' => Yii::$app->params['api.cors.allowCredentials'],
24 | 'Access-Control-Max-Age' => Yii::$app->params['api.cors.maxAge'],
25 | 'Access-Control-Expose-Headers' => Yii::$app->params['api.cors.exposeHeaders'],
26 | ],
27 | ],
28 | 'authenticator' => [
29 | 'class' => Authenticator::class,
30 | 'optional' => [
31 | 'options',
32 | ],
33 | 'authMethods' => [
34 | 'query' => [
35 | 'class' => QueryParamAuth::class,
36 | 'tokenParam' => 'access_token',
37 | ],
38 | 'bearer' => [
39 | 'class' => HttpBearerAuth::class,
40 | ],
41 | ],
42 | ],
43 | 'verb' => [
44 | 'class' => VerbFilter::class,
45 | 'actions' => $this->verbs(),
46 | ],
47 | 'rateLimiter' => [
48 | 'class' => RateLimiter::class,
49 | 'redis' => 'redis',
50 | 'capacity' => Yii::$app->params['api.rateLimiter.capacity'],
51 | 'rate' => Yii::$app->params['api.rateLimiter.rate'],
52 | 'limitPeriod' => Yii::$app->params['api.rateLimiter.limitPeriod'],
53 | 'prefix' => Yii::$app->params['api.rateLimiter.prefix'],
54 | 'ttl' => Yii::$app->params['api.rateLimiter.ttl'],
55 | ],
56 | ];
57 | }
58 |
59 | /**
60 | * @return array
61 | */
62 | protected function verbs()
63 | {
64 | return [];
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/Http/Widget/Alert.php:
--------------------------------------------------------------------------------
1 | session->setFlash('error', 'This is the message');
12 | * Yii::$app->session->setFlash('success', 'This is the message');
13 | * Yii::$app->session->setFlash('info', 'This is the message');
14 | * ```
15 | *
16 | * Multiple messages could be set as follows:
17 | *
18 | * ```php
19 | * Yii::$app->session->setFlash('error', ['Error 1', 'Error 2']);
20 | * ```
21 | *
22 | * @author Kartik Visweswaran
23 | * @author Alexander Makarov
24 | */
25 | class Alert extends \yii\bootstrap\Widget
26 | {
27 | /**
28 | * @var array the alert types configuration for the flash messages.
29 | * This array is setup as $key => $value, where:
30 | * - key: the name of the session flash variable
31 | * - value: the bootstrap alert type (i.e. danger, success, info, warning)
32 | */
33 | public $alertTypes = [
34 | 'error' => 'alert-danger',
35 | 'danger' => 'alert-danger',
36 | 'success' => 'alert-success',
37 | 'info' => 'alert-info',
38 | 'warning' => 'alert-warning'
39 | ];
40 | /**
41 | * @var array the options for rendering the close button tag.
42 | * Array will be passed to [[\yii\bootstrap\Alert::closeButton]].
43 | */
44 | public $closeButton = [];
45 |
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | public function run()
51 | {
52 | $session = Yii::$app->session;
53 | $flashes = $session->getAllFlashes();
54 | $appendClass = isset($this->options['class']) ? ' ' . $this->options['class'] : '';
55 |
56 | foreach ($flashes as $type => $flash) {
57 | if (!isset($this->alertTypes[$type])) {
58 | continue;
59 | }
60 |
61 | foreach ((array) $flash as $i => $message) {
62 | echo \yii\bootstrap\Alert::widget([
63 | 'body' => $message,
64 | 'closeButton' => $this->closeButton,
65 | 'options' => array_merge($this->options, [
66 | 'id' => $this->getId() . '-' . $type . '-' . $i,
67 | 'class' => $this->alertTypes[$type] . $appendClass,
68 | ]),
69 | ]);
70 | }
71 |
72 | $session->removeFlash($type);
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------