Browse Source

first commit

master
Jiangmenger 8 months ago
commit
2ea7e9a718
  1. 3
      .bowerrc
  2. 36
      .gitignore
  3. 29
      LICENSE.md
  4. 233
      README.md
  5. 92
      Vagrantfile
  6. 31
      assets/AppAsset.php
  7. 27
      codeception.yml
  8. 34
      commands/HelloController.php
  9. 90
      commands/PhonebookController.php
  10. 32
      commands/PostController.php
  11. 75
      composer.json
  12. 33
      config/__autocomplete.php
  13. 56
      config/console.php
  14. 14
      config/db.php
  15. 7
      config/params.php
  16. 46
      config/test.php
  17. 6
      config/test_db.php
  18. 74
      config/web.php
  19. 128
      controllers/SiteController.php
  20. 42
      data/contract.json
  21. 9
      docker-compose.yml
  22. 22
      mail/layouts/html.php
  23. 13
      mail/layouts/text.php
  24. 65
      models/ContactForm.php
  25. 81
      models/LoginForm.php
  26. 104
      models/User.php
  27. 162
      requirements.php
  28. 2
      runtime/.gitignore
  29. 6
      tests/_bootstrap.php
  30. 1
      tests/_data/.gitkeep
  31. 2
      tests/_output/.gitignore
  32. 26
      tests/_support/AcceptanceTester.php
  33. 23
      tests/_support/FunctionalTester.php
  34. 26
      tests/_support/UnitTester.php
  35. 10
      tests/acceptance.suite.yml.example
  36. 12
      tests/acceptance/AboutCest.php
  37. 34
      tests/acceptance/ContactCest.php
  38. 18
      tests/acceptance/HomeCest.php
  39. 21
      tests/acceptance/LoginCest.php
  40. 1
      tests/acceptance/_bootstrap.php
  41. 29
      tests/bin/yii
  42. 20
      tests/bin/yii.bat
  43. 14
      tests/functional.suite.yml
  44. 57
      tests/functional/ContactFormCest.php
  45. 59
      tests/functional/LoginFormCest.php
  46. 1
      tests/functional/_bootstrap.php
  47. 11
      tests/unit.suite.yml
  48. 3
      tests/unit/_bootstrap.php
  49. 41
      tests/unit/models/ContactFormTest.php
  50. 51
      tests/unit/models/LoginFormTest.php
  51. 44
      tests/unit/models/UserTest.php
  52. 261
      tests/unit/widgets/AlertTest.php
  53. 2
      vagrant/config/.gitignore
  54. 22
      vagrant/config/vagrant-local.example.yml
  55. 38
      vagrant/nginx/app.conf
  56. 3
      vagrant/nginx/log/.gitignore
  57. 18
      vagrant/provision/always-as-root.sh
  58. 79
      vagrant/provision/once-as-root.sh
  59. 31
      vagrant/provision/once-as-vagrant.sh
  60. 50
      vagrant/provision/provision.awk
  61. 83
      views/layouts/main.php
  62. 18
      views/site/about.php
  63. 68
      views/site/contact.php
  64. 27
      views/site/error.php
  65. 53
      views/site/index.php
  66. 55
      views/site/login.php
  67. 4
      web/.htaccess
  68. 2
      web/assets/.gitignore
  69. 88
      web/css/site.css
  70. BIN
      web/favicon.ico
  71. 16
      web/index-test.php
  72. 12
      web/index.php
  73. 2
      web/robots.txt
  74. 73
      widgets/Alert.php
  75. 21
      yii
  76. 20
      yii.bat

3
.bowerrc

@ -0,0 +1,3 @@
{
"directory" : "vendor/bower-asset"
}

36
.gitignore

@ -0,0 +1,36 @@
# phpstorm project files
.idea
# netbeans project files
nbproject
# zend studio for eclipse project files
.buildpath
.project
.settings
# windows thumbnail cache
Thumbs.db
# composer vendor dir
/vendor
# composer itself is not needed
composer.phar
# composer lock
composer.lock
# Mac DS_Store Files
.DS_Store
# phpunit itself is not needed
phpunit.phar
# local phpunit config
/phpunit.xml
tests/_output/*
tests/_support/_generated
#vagrant folder
/.vagrant

29
LICENSE.md

@ -0,0 +1,29 @@
Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Yii Software LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

233
README.md

@ -0,0 +1,233 @@
<p align="center">
<a href="https://github.com/yiisoft" target="_blank">
<img src="https://avatars0.githubusercontent.com/u/993323" height="100px">
</a>
<h1 align="center">Yii 2 Basic Project Template</h1>
<br>
</p>
Yii 2 Basic Project Template is a skeleton [Yii 2](https://www.yiiframework.com/) application best for
rapidly creating small projects.
The template contains the basic features including user login/logout and a contact page.
It includes all commonly used configurations that would allow you to focus on adding new
features to your application.
[![Latest Stable Version](https://img.shields.io/packagist/v/yiisoft/yii2-app-basic.svg)](https://packagist.org/packages/yiisoft/yii2-app-basic)
[![Total Downloads](https://img.shields.io/packagist/dt/yiisoft/yii2-app-basic.svg)](https://packagist.org/packages/yiisoft/yii2-app-basic)
[![build](https://github.com/yiisoft/yii2-app-basic/workflows/build/badge.svg)](https://github.com/yiisoft/yii2-app-basic/actions?query=workflow%3Abuild)
DIRECTORY STRUCTURE
-------------------
assets/ contains assets definition
commands/ contains console commands (controllers)
config/ contains application configurations
controllers/ contains Web controller classes
mail/ contains view files for e-mails
models/ contains model classes
runtime/ contains files generated during runtime
tests/ contains various tests for the basic application
vendor/ contains dependent 3rd-party packages
views/ contains view files for the Web application
web/ contains the entry script and Web resources
REQUIREMENTS
------------
The minimum requirement by this project template that your Web server supports PHP 7.4.
INSTALLATION
------------
### Install via Composer
If you do not have [Composer](https://getcomposer.org/), you may install it by following the instructions
at [getcomposer.org](https://getcomposer.org/doc/00-intro.md#installation-nix).
You can then install this project template using the following command:
~~~
composer create-project --prefer-dist yiisoft/yii2-app-basic basic
~~~
Now you should be able to access the application through the following URL, assuming `basic` is the directory
directly under the Web root.
~~~
http://localhost/basic/web/
~~~
### Install from an Archive File
Extract the archive file downloaded from [yiiframework.com](https://www.yiiframework.com/download/) to
a directory named `basic` that is directly under the Web root.
Set cookie validation key in `config/web.php` file to some random secret string:
```php
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => '<secret random string goes here>',
],
```
You can then access the application through the following URL:
~~~
http://localhost/basic/web/
~~~
### Install with Docker
Update your vendor packages
docker-compose run --rm php composer update --prefer-dist
Run the installation triggers (creating cookie validation code)
docker-compose run --rm php composer install
Start the container
docker-compose up -d
You can then access the application through the following URL:
http://127.0.0.1:8000
**NOTES:**
- Minimum required Docker engine version `17.04` for development (see [Performance tuning for volume mounts](https://docs.docker.com/docker-for-mac/osxfs-caching/))
- The default configuration uses a host-volume in your home directory `.docker-composer` for composer caches
CONFIGURATION
-------------
### Database
Edit the file `config/db.php` with real data, for example:
```php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2basic',
'username' => 'root',
'password' => '1234',
'charset' => 'utf8',
];
```
**NOTES:**
- Yii won't create the database for you, this has to be done manually before you can access it.
- Check and edit the other files in the `config/` directory to customize your application as required.
- Refer to the README in the `tests` directory for information specific to basic application tests.
TESTING
-------
Tests are located in `tests` directory. They are developed with [Codeception PHP Testing Framework](https://codeception.com/).
By default, there are 3 test suites:
- `unit`
- `functional`
- `acceptance`
Tests can be executed by running
```
vendor/bin/codecept run
```
The command above will execute unit and functional tests. Unit tests are testing the system components, while functional
tests are for testing user interaction. Acceptance tests are disabled by default as they require additional setup since
they perform testing in real browser.
### Running acceptance tests
To execute acceptance tests do the following:
1. Rename `tests/acceptance.suite.yml.example` to `tests/acceptance.suite.yml` to enable suite configuration
2. Replace `codeception/base` package in `composer.json` with `codeception/codeception` to install full-featured
version of Codeception
3. Update dependencies with Composer
```
composer update
```
4. Download [Selenium Server](https://www.seleniumhq.org/download/) and launch it:
```
java -jar ~/selenium-server-standalone-x.xx.x.jar
```
In case of using Selenium Server 3.0 with Firefox browser since v48 or Google Chrome since v53 you must download [GeckoDriver](https://github.com/mozilla/geckodriver/releases) or [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) and launch Selenium with it:
```
# for Firefox
java -jar -Dwebdriver.gecko.driver=~/geckodriver ~/selenium-server-standalone-3.xx.x.jar
# for Google Chrome
java -jar -Dwebdriver.chrome.driver=~/chromedriver ~/selenium-server-standalone-3.xx.x.jar
```
As an alternative way you can use already configured Docker container with older versions of Selenium and Firefox:
```
docker run --net=host selenium/standalone-firefox:2.53.0
```
5. (Optional) Create `yii2basic_test` database and update it by applying migrations if you have them.
```
tests/bin/yii migrate
```
The database configuration can be found at `config/test_db.php`.
6. Start web server:
```
tests/bin/yii serve
```
7. Now you can run all available tests
```
# run all available tests
vendor/bin/codecept run
# run acceptance tests
vendor/bin/codecept run acceptance
# run only unit and functional tests
vendor/bin/codecept run unit,functional
```
### Code coverage support
By default, code coverage is disabled in `codeception.yml` configuration file, you should uncomment needed rows to be able
to collect code coverage. You can run your tests and collect coverage with the following command:
```
#collect coverage for all tests
vendor/bin/codecept run --coverage --coverage-html --coverage-xml
#collect coverage only for unit tests
vendor/bin/codecept run unit --coverage --coverage-html --coverage-xml
#collect coverage for unit and functional tests
vendor/bin/codecept run functional,unit --coverage --coverage-html --coverage-xml
```
You can see code coverage output under the `tests/_output` directory.

92
Vagrantfile

@ -0,0 +1,92 @@
require 'yaml'
require 'fileutils'
required_plugins_installed = nil
required_plugins = %w( vagrant-hostmanager vagrant-vbguest )
required_plugins.each do |plugin|
unless Vagrant.has_plugin? plugin
system "vagrant plugin install #{plugin}"
required_plugins_installed = true
end
end
# IF plugin[s] was just installed - restart required
if required_plugins_installed
# Get CLI command[s] and call again
system 'vagrant' + ARGV.to_s.gsub(/\[\"|\", \"|\"\]/, ' ')
exit
end
domains = {
app: 'yii2basic.test'
}
vagrantfile_dir_path = File.dirname(__FILE__)
config = {
local: vagrantfile_dir_path + '/vagrant/config/vagrant-local.yml',
example: vagrantfile_dir_path + '/vagrant/config/vagrant-local.example.yml'
}
# copy config from example if local config not exists
FileUtils.cp config[:example], config[:local] unless File.exist?(config[:local])
# read config
options = YAML.load_file config[:local]
# check github token
if options['github_token'].nil? || options['github_token'].to_s.length != 40
puts "You must place REAL GitHub token into configuration:\n/yii2-app-basic/vagrant/config/vagrant-local.yml"
exit
end
# vagrant configurate
Vagrant.configure(2) do |config|
# select the box
config.vm.box = 'bento/ubuntu-18.04'
# should we ask about box updates?
config.vm.box_check_update = options['box_check_update']
config.vm.provider 'virtualbox' do |vb|
# machine cpus count
vb.cpus = options['cpus']
# machine memory size
vb.memory = options['memory']
# machine name (for VirtualBox UI)
vb.name = options['machine_name']
end
# machine name (for vagrant console)
config.vm.define options['machine_name']
# machine name (for guest machine console)
config.vm.hostname = options['machine_name']
# network settings
config.vm.network 'private_network', ip: options['ip']
# sync: folder 'yii2-app-advanced' (host machine) -> folder '/app' (guest machine)
config.vm.synced_folder './', '/app', owner: 'vagrant', group: 'vagrant'
# disable folder '/vagrant' (guest machine)
config.vm.synced_folder '.', '/vagrant', disabled: true
# hosts settings (host machine)
config.vm.provision :hostmanager
config.hostmanager.enabled = true
config.hostmanager.manage_host = true
config.hostmanager.ignore_private_ip = false
config.hostmanager.include_offline = true
config.hostmanager.aliases = domains.values
# quick fix for failed guest additions installations
# config.vbguest.auto_update = false
# provisioners
config.vm.provision 'shell', path: './vagrant/provision/once-as-root.sh', args: [options['timezone'], options['ip']]
config.vm.provision 'shell', path: './vagrant/provision/once-as-vagrant.sh', args: [options['github_token']], privileged: false
config.vm.provision 'shell', path: './vagrant/provision/always-as-root.sh', run: 'always'
# post-install message (vagrant console)
config.vm.post_up_message = "App URL: http://#{domains[:app]}"
end

31
assets/AppAsset.php

@ -0,0 +1,31 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace app\assets;
use yii\web\AssetBundle;
/**
* Main application asset bundle.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/site.css',
];
public $js = [
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap5\BootstrapAsset'
];
}

27
codeception.yml

@ -0,0 +1,27 @@
actor: Tester
bootstrap: _bootstrap.php
paths:
tests: tests
output: tests/_output
data: tests/_data
helpers: tests/_support
settings:
memory_limit: 1024M
colors: true
modules:
config:
Yii2:
configFile: 'config/test.php'
# To enable code coverage:
#coverage:
# #c3_url: http://localhost:8080/index-test.php/
# enabled: true
# #remote: true
# #remote_config: '../codeception.yml'
# whitelist:
# include:
# - models/*
# - controllers/*
# - commands/*
# - mail/*

34
commands/HelloController.php

@ -0,0 +1,34 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace app\commands;
use yii\console\Controller;
use yii\console\ExitCode;
/**
* This command echoes the first argument that you have entered.
*
* This command is provided as an example for you to learn how to create console commands.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class HelloController extends Controller
{
/**
* This command echoes what you have entered as the message.
* @param string $message the message to be echoed.
* @return int Exit code
*/
public function actionIndex($message = 'hello world')
{
echo $message . "\n";
return ExitCode::OK;
}
}

90
commands/PhonebookController.php

@ -0,0 +1,90 @@
<?php
namespace app\commands;
use yii\console\Controller;
class PhonebookController extends Controller
{
// 读取和保存文件
private function loadContacts()
{
$file = '/opt/phonebook/data/contract.json';
if (!file_exists($file)) {
return [];
}
return json_decode(file_get_contents($file), true);
}
private function saveContacts($contacts)
{
file_put_contents('/opt/phonebook/data/contract.json', json_encode($contacts, JSON_PRETTY_PRINT));
}
//增添联系人 (actionAdd)
public function actionAdd($name, $phone)
{
$contacts = $this->loadContacts();
$contacts[] = ['id' => count($contacts) + 1, 'name' => $name, 'phone' => $phone];
$this->saveContacts($contacts);
echo "Contact added successfully.\n";
}
//搜索联系人 (actionSearch)
public function actionSearch($name)
{
$contacts = $this->loadContacts();
$found = array_filter($contacts, function ($contact) use ($name) {
return strpos(strtolower($contact['name']), strtolower($name)) !== false;
});
if (empty($found)) {
echo "No contacts found.\n";
return;
}
foreach ($found as $contact) {
echo "ID: {$contact['id']}, Name: {$contact['name']}, Phone: {$contact['phone']}\n";
}
}
//删除联系人 (actionDelete)
public function actionDelete($id)
{
$contacts = $this->loadContacts();
foreach ($contacts as $i => $contact) {
if ($contact['id'] == $id) {
array_splice($contacts, $i, 1);
$this->saveContacts($contacts);
echo "Contact deleted successfully.\n";
return;
}
}
echo "Contact not found.\n";
}
//修改联系人 (actionUpdate)
public function actionUpdate($id, $name, $phone)
{
$contacts = $this->loadContacts();
foreach ($contacts as &$contact) {
if ($contact['id'] == $id) {
$contact['name'] = $name;
$contact['phone'] = $phone;
$this->saveContacts($contacts);
echo "Contact updated successfully.\n";
return;
}
}
echo "Contact not found.\n";
}
}
?>

32
commands/PostController.php

@ -0,0 +1,32 @@
<?php
namespace app\controllers;
use Yii;
use app\models\Post;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
class PostController extends Controller{
public function actionView($id){
$model = Post::findOne($id);
if( $model === null){
throw new NotFoundHttpException;
}
return $this->render('view', [
'model' => $model,
]);
}
public function actionCreate(){
$model = new Post;
if($model->load(Yii::$app->request->post())&& $model->save()){
}
}
}
?>

75
composer.json

@ -0,0 +1,75 @@
{
"name": "yiisoft/yii2-app-basic",
"description": "Yii 2 Basic Project Template",
"keywords": ["yii2", "framework", "basic", "project template"],
"homepage": "https://www.yiiframework.com/",
"type": "project",
"license": "BSD-3-Clause",
"support": {
"issues": "https://github.com/yiisoft/yii2/issues?state=open",
"forum": "https://www.yiiframework.com/forum/",
"wiki": "https://www.yiiframework.com/wiki/",
"irc": "ircs://irc.libera.chat:6697/yii",
"source": "https://github.com/yiisoft/yii2"
},
"minimum-stability": "stable",
"require": {
"php": ">=7.4.0",
"yiisoft/yii2": "~2.0.45",
"yiisoft/yii2-bootstrap5": "~2.0.2",
"yiisoft/yii2-symfonymailer": "~2.0.3"
},
"require-dev": {
"yiisoft/yii2-debug": "~2.1.0",
"yiisoft/yii2-gii": "~2.2.0",
"yiisoft/yii2-faker": "~2.0.0",
"phpunit/phpunit": "~9.5.0",
"codeception/codeception": "^5.0.0 || ^4.0",
"codeception/lib-innerbrowser": "^4.0 || ^3.0 || ^1.1",
"codeception/module-asserts": "^3.0 || ^1.1",
"codeception/module-yii2": "^1.1",
"codeception/module-filesystem": "^3.0 || ^2.0 || ^1.1",
"codeception/verify": "^3.0 || ^2.2",
"symfony/browser-kit": "^6.0 || >=2.7 <=4.2.4"
},
"config": {
"allow-plugins": {
"yiisoft/yii2-composer" : true
},
"process-timeout": 1800,
"fxp-asset": {
"enabled": false
}
},
"scripts": {
"post-install-cmd": [
"yii\\composer\\Installer::postInstall"
],
"post-create-project-cmd": [
"yii\\composer\\Installer::postCreateProject",
"yii\\composer\\Installer::postInstall"
]
},
"extra": {
"yii\\composer\\Installer::postCreateProject": {
"setPermission": [
{
"runtime": "0777",
"web/assets": "0777",
"yii": "0755"
}
]
},
"yii\\composer\\Installer::postInstall": {
"generateCookieValidationKey": [
"config/web.php"
]
}
},
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
}
]
}

33
config/__autocomplete.php

@ -0,0 +1,33 @@
<?php
/**
* This class only exists here for IDE (PHPStorm/Netbeans/...) autocompletion.
* This file is never included anywhere.
* Adjust this file to match classes configured in your application config, to enable IDE autocompletion for custom components.
* Example: A property phpdoc can be added in `__Application` class as `@property \vendor\package\Rollbar|__Rollbar $rollbar` and adding a class in this file
* ```php
* // @property of \vendor\package\Rollbar goes here
* class __Rollbar {
* }
* ```
*/
class Yii {
/**
* @var \yii\web\Application|\yii\console\Application|__Application
*/
public static $app;
}
/**
* @property yii\rbac\DbManager $authManager
* @property \yii\web\User|__WebUser $user
*
*/
class __Application {
}
/**
* @property app\models\User $identity
*/
class __WebUser {
}

56
config/console.php

@ -0,0 +1,56 @@
<?php
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/db.php';
$config = [
'id' => 'basic-console',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'controllerNamespace' => 'app\commands',
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
'@tests' => '@app/tests',
],
'components' => [
'cache' => [
'class' => 'yii\caching\FileCache',
],
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => $db,
],
'params' => $params,
/*
'controllerMap' => [
'fixture' => [ // Fixture generation command line.
'class' => 'yii\faker\FixtureController',
],
],
*/
];
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
$config['bootstrap'][] = 'gii';
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
];
// configuration adjustments for 'dev' environment
// requires version `2.1.21` of yii2-debug module
$config['bootstrap'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yii\debug\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
}
return $config;

14
config/db.php

@ -0,0 +1,14 @@
<?php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2basic',
'username' => 'root',
'password' => 'deep123',
'charset' => 'utf8',
// Schema cache options (for production environment)
//'enableSchemaCache' => true,
//'schemaCacheDuration' => 60,
//'schemaCache' => 'cache',
];

7
config/params.php

@ -0,0 +1,7 @@
<?php
return [
'adminEmail' => 'admin@example.com',
'senderEmail' => 'noreply@example.com',
'senderName' => 'Example.com mailer',
];

46
config/test.php

@ -0,0 +1,46 @@
<?php
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/test_db.php';
/**
* Application configuration shared by all test types
*/
return [
'id' => 'basic-tests',
'basePath' => dirname(__DIR__),
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
],
'language' => 'en-US',
'components' => [
'db' => $db,
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
'viewPath' => '@app/mail',
// send all mails to a file by default.
'useFileTransport' => true,
'messageClass' => 'yii\symfonymailer\Message'
],
'assetManager' => [
'basePath' => __DIR__ . '/../web/assets',
],
'urlManager' => [
'showScriptName' => true,
],
'user' => [
'identityClass' => 'app\models\User',
],
'request' => [
'cookieValidationKey' => 'test',
'enableCsrfValidation' => false,
// but if you absolutely need it set cookie domain to localhost
/*
'csrfCookie' => [
'domain' => 'localhost',
],
*/
],
],
'params' => $params,
];

6
config/test_db.php

@ -0,0 +1,6 @@
<?php
$db = require __DIR__ . '/db.php';
// test database! Important not to run tests on production or development databases
$db['dsn'] = 'mysql:host=localhost;dbname=yii2basic_test';
return $db;

74
config/web.php

@ -0,0 +1,74 @@
<?php
$params = require __DIR__ . '/params.php';
$db = require __DIR__ . '/db.php';
$config = [
'id' => 'basic',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
],
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => 'hovIi5cjCDE92evC_zPUv3332pNJ7sIC',
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
'user' => [
'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
],
'errorHandler' => [
'errorAction' => 'site/error',
],
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
'viewPath' => '@app/mail',
// send all mails to a file by default.
'useFileTransport' => true,
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => $db,
/*
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
],
],
*/
],
'params' => $params,
];
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
$config['bootstrap'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yii\debug\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
$config['bootstrap'][] = 'gii';
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
// uncomment the following to add your IP if you are not connecting from localhost.
//'allowedIPs' => ['127.0.0.1', '::1'],
];
}
return $config;

128
controllers/SiteController.php

@ -0,0 +1,128 @@
<?php
namespace app\controllers;
use Yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\Response;
use yii\filters\VerbFilter;
use app\models\LoginForm;
use app\models\ContactForm;
class SiteController extends Controller
{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::class,
'only' => ['logout'],
'rules' => [
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['@'],
],
],
],
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'logout' => ['post'],
],
],
];
}
/**
* {@inheritdoc}
*/
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
/**
* Displays homepage.
*
* @return string
*/
public function actionIndex()
{
return $this->render('index');
}
/**
* Login action.
*
* @return Response|string
*/
public function actionLogin()
{
if (!Yii::$app->user->isGuest) {
return $this->goHome();
}
$model = new LoginForm();
if ($model->load(Yii::$app->request->post()) && $model->login()) {
return $this->goBack();
}
$model->password = '';
return $this->render('login', [
'model' => $model,
]);
}
/**
* Logout action.
*
* @return Response
*/
public function actionLogout()
{
Yii::$app->user->logout();
return $this->goHome();
}
/**
* Displays contact page.
*
* @return Response|string
*/
public function actionContact()
{
$model = new ContactForm();
if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
Yii::$app->session->setFlash('contactFormSubmitted');
return $this->refresh();
}
return $this->render('contact', [
'model' => $model,
]);
}
/**
* Displays about page.
*
* @return string
*/
public function actionAbout()
{
return $this->render('about');
}
}

42
data/contract.json

@ -0,0 +1,42 @@
[
{
"id": 1,
"name": "jiang",
"phone": "19866827739"
},
{
"id": 2,
"name": "zhang",
"phone": "001"
},
{
"id": 3,
"name": "chen",
"phone": "002"
},
{
"id": 4,
"name": "chen",
"phone": "002"
},
{
"id": 5,
"name": "li",
"phone": "003"
},
{
"id": 6,
"name": "guan",
"phone": "004"
},
{
"id": 7,
"name": "huang",
"phone": "005"
},
{
"id": 8,
"name": "zeng",
"phone": "006"
}
]

9
docker-compose.yml

@ -0,0 +1,9 @@
version: '2'
services:
php:
image: yiisoftware/yii2-php:7.4-apache
volumes:
- ~/.composer-docker/cache:/root/.composer/cache:delegated
- ./:/app:delegated
ports:
- '8000:80'

22
mail/layouts/html.php

@ -0,0 +1,22 @@
<?php
use yii\helpers\Html;
/** @var \yii\web\View $this view component instance */
/** @var \yii\mail\MessageInterface $message the message being composed */
/** @var string $content main view render result */
?>
<?php $this->beginPage() ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=<?= Yii::$app->charset ?>" />
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<?= $content ?>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

13
mail/layouts/text.php

@ -0,0 +1,13 @@
<?php
/**
* @var yii\web\View $this view component instance
* @var yii\mail\BaseMessage $message the message being composed
* @var string $content main view render result
*/
$this->beginPage();
$this->beginBody();
echo $content;
$this->endBody();
$this->endPage();

65
models/ContactForm.php

@ -0,0 +1,65 @@
<?php
namespace app\models;
use Yii;
use yii\base\Model;
/**
* ContactForm is the model behind the contact form.
*/
class ContactForm extends Model
{
public $name;
public $email;
public $subject;
public $body;
public $verifyCode;
/**
* @return array the validation rules.
*/
public function rules()
{
return [
// name, email, subject and body are required
[['name', 'email', 'subject', 'body'], 'required'],
// email has to be a valid email address
['email', 'email'],
// verifyCode needs to be entered correctly
['verifyCode', 'captcha'],
];
}
/**
* @return array customized attribute labels
*/
public function attributeLabels()
{
return [
'verifyCode' => 'Verification Code',
];
}
/**
* Sends an email to the specified email address using the information collected by this model.
* @param string $email the target email address
* @return bool whether the model passes validation
*/
public function contact($email)
{
if ($this->validate()) {
Yii::$app->mailer->compose()
->setTo($email)
->setFrom([Yii::$app->params['senderEmail'] => Yii::$app->params['senderName']])
->setReplyTo([$this->email => $this->name])
->setSubject($this->subject)
->setTextBody($this->body)
->send();
return true;
}
return false;
}
}

81
models/LoginForm.php

@ -0,0 +1,81 @@
<?php
namespace app\models;
use Yii;
use yii\base\Model;
/**
* LoginForm is the model behind the login form.
*
* @property-read User|null $user
*
*/
class LoginForm extends Model
{
public $username;
public $password;
public $rememberMe = true;
private $_user = false;
/**
* @return array the validation rules.
*/
public function rules()
{
return [
// username and password are both required
[['username', 'password'], 'required'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}
/**
* Validates the password.
* This method serves as the inline validation for password.
*
* @param string $attribute the attribute currently being validated
* @param array $params the additional name-value pairs given in the rule
*/
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
$user = $this->getUser();
if (!$user || !$user->validatePassword($this->password)) {
$this->addError($attribute, 'Incorrect username or password.');
}
}
}
/**
* Logs in a user using the provided username and password.
* @return bool whether the user is logged in successfully
*/
public function login()
{
if ($this->validate()) {
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0);
}
return false;
}
/**
* Finds user by [[username]]
*
* @return User|null
*/
public function getUser()
{
if ($this->_user === false) {
$this->_user = User::findByUsername($this->username);
}
return $this->_user;
}
}

104
models/User.php

@ -0,0 +1,104 @@
<?php
namespace app\models;
class User extends \yii\base\BaseObject implements \yii\web\IdentityInterface
{
public $id;
public $username;
public $password;
public $authKey;
public $accessToken;
private static $users = [
'100' => [
'id' => '100',
'username' => 'admin',
'password' => 'admin',
'authKey' => 'test100key',
'accessToken' => '100-token',
],
'101' => [
'id' => '101',
'username' => 'demo',
'password' => 'demo',
'authKey' => 'test101key',
'accessToken' => '101-token',
],
];
/**
* {@inheritdoc}
*/
public static function findIdentity($id)
{
return isset(self::$users[$id]) ? new static(self::$users[$id]) : null;
}
/**
* {@inheritdoc}
*/
public static function findIdentityByAccessToken($token, $type = null)
{
foreach (self::$users as $user) {
if ($user['accessToken'] === $token) {
return new static($user);
}
}
return null;
}
/**
* Finds user by username
*
* @param string $username
* @return static|null
*/
public static function findByUsername($username)
{
foreach (self::$users as $user) {
if (strcasecmp($user['username'], $username) === 0) {
return new static($user);
}
}
return null;
}
/**
* {@inheritdoc}
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getAuthKey()
{
return $this->authKey;
}
/**
* {@inheritdoc}
*/
public function validateAuthKey($authKey)
{
return $this->authKey === $authKey;
}
/**
* Validates password
*
* @param string $password password to validate
* @return bool if password provided is valid for current user
*/
public function validatePassword($password)
{
return $this->password === $password;
}
}

162
requirements.php

@ -0,0 +1,162 @@
<?php
/**
* Application requirement checker script.
*
* In order to run this script use the following console command:
* php requirements.php
*
* In order to run this script from the web, you should copy it to the web root.
* If you are using Linux you can create a hard link instead, using the following command:
* ln ../requirements.php requirements.php
*/
// you may need to adjust this path to the correct Yii framework path
// uncomment and adjust the following line if Yii is not located at the default path
//$frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2';
if (!isset($frameworkPath)) {
$searchPaths = array(
dirname(__FILE__) . '/vendor/yiisoft/yii2',
dirname(__FILE__) . '/../vendor/yiisoft/yii2',
);
foreach ($searchPaths as $path) {
if (is_dir($path)) {
$frameworkPath = $path;
break;
}
}
}
if (!isset($frameworkPath) || !is_dir($frameworkPath)) {
$message = "<h1>Error</h1>\n\n"
. "<p><strong>The path to yii framework seems to be incorrect.</strong></p>\n"
. '<p>You need to install Yii framework via composer or adjust the framework path in file <abbr title="' . __FILE__ . '">' . basename(__FILE__) . "</abbr>.</p>\n"
. '<p>Please refer to the <abbr title="' . dirname(__FILE__) . "/README.md\">README</abbr> on how to install Yii.</p>\n";
if (!empty($_SERVER['argv'])) {
// do not print HTML when used in console mode
echo strip_tags($message);
} else {
echo $message;
}
exit(1);
}
require_once($frameworkPath . '/requirements/YiiRequirementChecker.php');
$requirementsChecker = new YiiRequirementChecker();
$gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.';
$gdOK = $imagickOK = false;
if (extension_loaded('imagick')) {
$imagick = new Imagick();
$imagickFormats = $imagick->queryFormats('PNG');
if (in_array('PNG', $imagickFormats)) {
$imagickOK = true;
} else {
$imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.';
}
}
if (extension_loaded('gd')) {
$gdInfo = gd_info();
if (!empty($gdInfo['FreeType Support'])) {
$gdOK = true;
} else {
$gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.';
}
}
/**
* Adjust requirements according to your application specifics.
*/
$requirements = array(
// Database :
array(
'name' => 'PDO extension',
'mandatory' => true,
'condition' => extension_loaded('pdo'),
'by' => 'All DB-related classes',
),
array(
'name' => 'PDO SQLite extension',
'mandatory' => false,
'condition' => extension_loaded('pdo_sqlite'),
'by' => 'All DB-related classes',
'memo' => 'Required for SQLite database.',
),
array(
'name' => 'PDO MySQL extension',
'mandatory' => false,
'condition' => extension_loaded('pdo_mysql'),
'by' => 'All DB-related classes',
'memo' => 'Required for MySQL database.',
),
array(
'name' => 'PDO PostgreSQL extension',
'mandatory' => false,
'condition' => extension_loaded('pdo_pgsql'),
'by' => 'All DB-related classes',
'memo' => 'Required for PostgreSQL database.',
),
// Cache :
array(
'name' => 'Memcache extension',
'mandatory' => false,
'condition' => extension_loaded('memcache') || extension_loaded('memcached'),
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-caching-memcache.html">MemCache</a>',
'memo' => extension_loaded('memcached') ? 'To use memcached set <a href="https://www.yiiframework.com/doc-2.0/yii-caching-memcache.html#$useMemcached-detail">MemCache::useMemcached</a> to <code>true</code>.' : ''
),
// CAPTCHA:
array(
'name' => 'GD PHP extension with FreeType support',
'mandatory' => false,
'condition' => $gdOK,
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
'memo' => $gdMemo,
),
array(
'name' => 'ImageMagick PHP extension with PNG support',
'mandatory' => false,
'condition' => $imagickOK,
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
'memo' => $imagickMemo,
),
// PHP ini :
'phpExposePhp' => array(
'name' => 'Expose PHP',
'mandatory' => false,
'condition' => $requirementsChecker->checkPhpIniOff("expose_php"),
'by' => 'Security reasons',
'memo' => '"expose_php" should be disabled at php.ini',
),
'phpAllowUrlInclude' => array(
'name' => 'PHP allow url include',
'mandatory' => false,
'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"),
'by' => 'Security reasons',
'memo' => '"allow_url_include" should be disabled at php.ini',
),
'phpSmtp' => array(
'name' => 'PHP mail SMTP',
'mandatory' => false,
'condition' => strlen(ini_get('SMTP')) > 0,
'by' => 'Email sending',
'memo' => 'PHP mail SMTP server required',
),
);
// OPcache check
if (!version_compare(phpversion(), '5.5', '>=')) {
$requirements[] = array(
'name' => 'APC extension',
'mandatory' => false,
'condition' => extension_loaded('apc'),
'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-caching-apccache.html">ApcCache</a>',
);
}
$result = $requirementsChecker->checkYii()->check($requirements)->getResult();
$requirementsChecker->render();
exit($result['summary']['errors'] === 0 ? 0 : 1);

2
runtime/.gitignore

@ -0,0 +1,2 @@
*
!.gitignore

6
tests/_bootstrap.php

@ -0,0 +1,6 @@
<?php
define('YII_ENV', 'test');
defined('YII_DEBUG') or define('YII_DEBUG', true);
require_once __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
require __DIR__ .'/../vendor/autoload.php';

1
tests/_data/.gitkeep

@ -0,0 +1 @@

2
tests/_output/.gitignore

@ -0,0 +1,2 @@
*
!.gitignore

26
tests/_support/AcceptanceTester.php

@ -0,0 +1,26 @@
<?php
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*/
class AcceptanceTester extends \Codeception\Actor
{
use _generated\AcceptanceTesterActions;
/**
* Define custom actions here
*/
}

23
tests/_support/FunctionalTester.php

@ -0,0 +1,23 @@
<?php
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*/
class FunctionalTester extends \Codeception\Actor
{
use _generated\FunctionalTesterActions;
}

26
tests/_support/UnitTester.php

@ -0,0 +1,26 @@
<?php
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*/
class UnitTester extends \Codeception\Actor
{
use _generated\UnitTesterActions;
/**
* Define custom actions here
*/
}

10
tests/acceptance.suite.yml.example

@ -0,0 +1,10 @@
actor: AcceptanceTester
modules:
enabled:
- WebDriver:
url: http://127.0.0.1:8080/
browser: firefox
- Yii2:
part: orm
entryScript: index-test.php
cleanup: false

12
tests/acceptance/AboutCest.php

@ -0,0 +1,12 @@
<?php
use yii\helpers\Url;
class AboutCest
{
public function ensureThatAboutWorks(AcceptanceTester $I)
{
$I->amOnPage(Url::toRoute('/site/about'));
$I->see('About', 'h1');
}
}

34
tests/acceptance/ContactCest.php

@ -0,0 +1,34 @@
<?php
use yii\helpers\Url;
class ContactCest
{
public function _before(\AcceptanceTester $I)
{
$I->amOnPage(Url::toRoute('/site/contact'));
}
public function contactPageWorks(AcceptanceTester $I)
{
$I->wantTo('ensure that contact page works');
$I->see('Contact', 'h1');
}
public function contactFormCanBeSubmitted(AcceptanceTester $I)
{
$I->amGoingTo('submit contact form with correct data');
$I->fillField('#contactform-name', 'tester');
$I->fillField('#contactform-email', 'tester@example.com');
$I->fillField('#contactform-subject', 'test subject');
$I->fillField('#contactform-body', 'test content');
$I->fillField('#contactform-verifycode', 'testme');
$I->click('contact-button');
$I->wait(2); // wait for button to be clicked
$I->dontSeeElement('#contact-form');
$I->see('Thank you for contacting us. We will respond to you as soon as possible.');
}
}

18
tests/acceptance/HomeCest.php

@ -0,0 +1,18 @@
<?php
use yii\helpers\Url;
class HomeCest
{
public function ensureThatHomePageWorks(AcceptanceTester $I)
{
$I->amOnPage(Url::toRoute('/site/index'));
$I->see('My Company');
$I->seeLink('About');
$I->click('About');
$I->wait(2); // wait for page to be opened
$I->see('This is the About page.');
}
}

21
tests/acceptance/LoginCest.php

@ -0,0 +1,21 @@
<?php
use yii\helpers\Url;
class LoginCest
{
public function ensureThatLoginWorks(AcceptanceTester $I)
{
$I->amOnPage(Url::toRoute('/site/login'));
$I->see('Login', 'h1');
$I->amGoingTo('try to login with correct credentials');
$I->fillField('input[name="LoginForm[username]"]', 'admin');
$I->fillField('input[name="LoginForm[password]"]', 'admin');
$I->click('login-button');
$I->wait(2); // wait for button to be clicked
$I->expectTo('see user info');
$I->see('Logout');
}
}

1
tests/acceptance/_bootstrap.php

@ -0,0 +1 @@
<?php

29
tests/bin/yii

@ -0,0 +1,29 @@
#!/usr/bin/env php
<?php
/**
* Yii console bootstrap file.
*
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test');
require __DIR__ . '/../../vendor/autoload.php';
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
$config = yii\helpers\ArrayHelper::merge(
require __DIR__ . '/../../config/console.php',
[
'components' => [
'db' => require __DIR__ . '/../../config/test_db.php'
]
]
);
$application = new yii\console\Application($config);
$exitCode = $application->run();
exit($exitCode);

20
tests/bin/yii.bat

@ -0,0 +1,20 @@
@echo off
rem -------------------------------------------------------------
rem Yii command line bootstrap script for Windows.
rem
rem @author Qiang Xue <qiang.xue@gmail.com>
rem @link https://www.yiiframework.com/
rem @copyright Copyright (c) 2008 Yii Software LLC
rem @license https://www.yiiframework.com/license/
rem -------------------------------------------------------------
@setlocal
set YII_PATH=%~dp0
if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
"%PHP_COMMAND%" "%YII_PATH%yii" %*
@endlocal

14
tests/functional.suite.yml

@ -0,0 +1,14 @@
# Codeception Test Suite Configuration
# suite for functional (integration) tests.
# emulate web requests and make application process them.
# (tip: better to use with frameworks).
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
#basic/web/index.php
actor: FunctionalTester
modules:
enabled:
- Filesystem
- Yii2
- Asserts

57
tests/functional/ContactFormCest.php

@ -0,0 +1,57 @@
<?php
class ContactFormCest
{
public function _before(\FunctionalTester $I)
{
$I->amOnRoute('site/contact');
}
public function openContactPage(\FunctionalTester $I)
{
$I->see('Contact', 'h1');
}
public function submitEmptyForm(\FunctionalTester $I)
{
$I->submitForm('#contact-form', []);
$I->expectTo('see validations errors');
$I->see('Contact', 'h1');
$I->see('Name cannot be blank');
$I->see('Email cannot be blank');
$I->see('Subject cannot be blank');
$I->see('Body cannot be blank');
$I->see('The verification code is incorrect');
}
public function submitFormWithIncorrectEmail(\FunctionalTester $I)
{
$I->submitForm('#contact-form', [
'ContactForm[name]' => 'tester',
'ContactForm[email]' => 'tester.email',
'ContactForm[subject]' => 'test subject',
'ContactForm[body]' => 'test content',
'ContactForm[verifyCode]' => 'testme',
]);
$I->expectTo('see that email address is wrong');
$I->dontSee('Name cannot be blank', '.help-inline');
$I->see('Email is not a valid email address.');
$I->dontSee('Subject cannot be blank', '.help-inline');
$I->dontSee('Body cannot be blank', '.help-inline');
$I->dontSee('The verification code is incorrect', '.help-inline');
}
public function submitFormSuccessfully(\FunctionalTester $I)
{
$I->submitForm('#contact-form', [
'ContactForm[name]' => 'tester',
'ContactForm[email]' => 'tester@example.com',
'ContactForm[subject]' => 'test subject',
'ContactForm[body]' => 'test content',
'ContactForm[verifyCode]' => 'testme',
]);
$I->seeEmailIsSent();
$I->dontSeeElement('#contact-form');
$I->see('Thank you for contacting us. We will respond to you as soon as possible.');
}
}

59
tests/functional/LoginFormCest.php

@ -0,0 +1,59 @@
<?php
class LoginFormCest
{
public function _before(\FunctionalTester $I)
{
$I->amOnRoute('site/login');
}
public function openLoginPage(\FunctionalTester $I)
{
$I->see('Login', 'h1');
}
// demonstrates `amLoggedInAs` method
public function internalLoginById(\FunctionalTester $I)
{
$I->amLoggedInAs(100);
$I->amOnPage('/');
$I->see('Logout (admin)');
}
// demonstrates `amLoggedInAs` method
public function internalLoginByInstance(\FunctionalTester $I)
{
$I->amLoggedInAs(\app\models\User::findByUsername('admin'));
$I->amOnPage('/');
$I->see('Logout (admin)');
}
public function loginWithEmptyCredentials(\FunctionalTester $I)
{
$I->submitForm('#login-form', []);
$I->expectTo('see validations errors');
$I->see('Username cannot be blank.');
$I->see('Password cannot be blank.');
}
public function loginWithWrongCredentials(\FunctionalTester $I)
{
$I->submitForm('#login-form', [
'LoginForm[username]' => 'admin',
'LoginForm[password]' => 'wrong',
]);
$I->expectTo('see validations errors');
$I->see('Incorrect username or password.');
}
public function loginSuccessfully(\FunctionalTester $I)
{
$I->submitForm('#login-form', [
'LoginForm[username]' => 'admin',
'LoginForm[password]' => 'admin',
]);
$I->see('Logout (admin)');
$I->dontSeeElement('form#login-form');
}
}

1
tests/functional/_bootstrap.php

@ -0,0 +1 @@
<?php

11
tests/unit.suite.yml

@ -0,0 +1,11 @@
# Codeception Test Suite Configuration
# suite for unit (internal) tests.
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
actor: UnitTester
modules:
enabled:
- Asserts
- Yii2:
part: [orm, email, fixtures]

3
tests/unit/_bootstrap.php

@ -0,0 +1,3 @@
<?php
// add unit testing specific bootstrap code here

41
tests/unit/models/ContactFormTest.php

@ -0,0 +1,41 @@
<?php
namespace tests\unit\models;
use app\models\ContactForm;
use yii\mail\MessageInterface;
class ContactFormTest extends \Codeception\Test\Unit
{
/**
* @var \UnitTester
*/
public $tester;
public function testEmailIsSentOnContact()
{
$model = new ContactForm();
$model->attributes = [
'name' => 'Tester',
'email' => 'tester@example.com',
'subject' => 'very important letter subject',
'body' => 'body of current message',
'verifyCode' => 'testme',
];
verify($model->contact('admin@example.com'))->notEmpty();
// using Yii2 module actions to check email was sent
$this->tester->seeEmailIsSent();
/** @var MessageInterface $emailMessage */
$emailMessage = $this->tester->grabLastSentEmail();
verify($emailMessage)->instanceOf('yii\mail\MessageInterface');
verify($emailMessage->getTo())->arrayHasKey('admin@example.com');
verify($emailMessage->getFrom())->arrayHasKey('noreply@example.com');
verify($emailMessage->getReplyTo())->arrayHasKey('tester@example.com');
verify($emailMessage->getSubject())->equals('very important letter subject');
verify($emailMessage->toString())->stringContainsString('body of current message');
}
}

51
tests/unit/models/LoginFormTest.php

@ -0,0 +1,51 @@
<?php
namespace tests\unit\models;
use app\models\LoginForm;
class LoginFormTest extends \Codeception\Test\Unit
{
private $model;
protected function _after()
{
\Yii::$app->user->logout();
}
public function testLoginNoUser()
{
$this->model = new LoginForm([
'username' => 'not_existing_username',
'password' => 'not_existing_password',
]);
verify($this->model->login())->false();
verify(\Yii::$app->user->isGuest)->true();
}
public function testLoginWrongPassword()
{
$this->model = new LoginForm([
'username' => 'demo',
'password' => 'wrong_password',
]);
verify($this->model->login())->false();
verify(\Yii::$app->user->isGuest)->true();
verify($this->model->errors)->arrayHasKey('password');
}
public function testLoginCorrect()
{
$this->model = new LoginForm([
'username' => 'demo',
'password' => 'demo',
]);
verify($this->model->login())->true();
verify(\Yii::$app->user->isGuest)->false();
verify($this->model->errors)->arrayHasNotKey('password');
}
}

44
tests/unit/models/UserTest.php

@ -0,0 +1,44 @@
<?php
namespace tests\unit\models;
use app\models\User;
class UserTest extends \Codeception\Test\Unit
{
public function testFindUserById()
{
verify($user = User::findIdentity(100))->notEmpty();
verify($user->username)->equals('admin');
verify(User::findIdentity(999))->empty();
}
public function testFindUserByAccessToken()
{
verify($user = User::findIdentityByAccessToken('100-token'))->notEmpty();
verify($user->username)->equals('admin');
verify(User::findIdentityByAccessToken('non-existing'))->empty();
}
public function testFindUserByUsername()
{
verify($user = User::findByUsername('admin'))->notEmpty();
verify(User::findByUsername('not-admin'))->empty();
}
/**
* @depends testFindUserByUsername
*/
public function testValidateUser()
{
$user = User::findByUsername('admin');
verify($user->validateAuthKey('test100key'))->notEmpty();
verify($user->validateAuthKey('test102key'))->empty();
verify($user->validatePassword('admin'))->notEmpty();
verify($user->validatePassword('123456'))->empty();
}
}

261
tests/unit/widgets/AlertTest.php

@ -0,0 +1,261 @@
<?php
namespace tests\unit\widgets;
use app\widgets\Alert;
use Yii;
class AlertTest extends \Codeception\Test\Unit
{
public function testSingleErrorMessage()
{
$message = 'This is an error message';
Yii::$app->session->setFlash('error', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testMultipleErrorMessages()
{
$firstMessage = 'This is the first error message';
$secondMessage = 'This is the second error message';
Yii::$app->session->setFlash('error', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testSingleDangerMessage()
{
$message = 'This is a danger message';
Yii::$app->session->setFlash('danger', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testMultipleDangerMessages()
{
$firstMessage = 'This is the first danger message';
$secondMessage = 'This is the second danger message';
Yii::$app->session->setFlash('danger', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testSingleSuccessMessage()
{
$message = 'This is a success message';
Yii::$app->session->setFlash('success', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testMultipleSuccessMessages()
{
$firstMessage = 'This is the first danger message';
$secondMessage = 'This is the second danger message';
Yii::$app->session->setFlash('success', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testSingleInfoMessage()
{
$message = 'This is an info message';
Yii::$app->session->setFlash('info', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testMultipleInfoMessages()
{
$firstMessage = 'This is the first info message';
$secondMessage = 'This is the second info message';
Yii::$app->session->setFlash('info', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-info');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-warning');
}
public function testSingleWarningMessage()
{
$message = 'This is a warning message';
Yii::$app->session->setFlash('warning', $message);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($message);
verify($renderingResult)->stringContainsString('alert-warning');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
}
public function testMultipleWarningMessages()
{
$firstMessage = 'This is the first warning message';
$secondMessage = 'This is the second warning message';
Yii::$app->session->setFlash('warning', [$firstMessage, $secondMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstMessage);
verify($renderingResult)->stringContainsString($secondMessage);
verify($renderingResult)->stringContainsString('alert-warning');
verify($renderingResult)->stringNotContainsString('alert-danger');
verify($renderingResult)->stringNotContainsString('alert-success');
verify($renderingResult)->stringNotContainsString('alert-info');
}
public function testSingleMixedMessages() {
$errorMessage = 'This is an error message';
$dangerMessage = 'This is a danger message';
$successMessage = 'This is a success message';
$infoMessage = 'This is a info message';
$warningMessage = 'This is a warning message';
Yii::$app->session->setFlash('error', $errorMessage);
Yii::$app->session->setFlash('danger', $dangerMessage);
Yii::$app->session->setFlash('success', $successMessage);
Yii::$app->session->setFlash('info', $infoMessage);
Yii::$app->session->setFlash('warning', $warningMessage);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($errorMessage);
verify($renderingResult)->stringContainsString($dangerMessage);
verify($renderingResult)->stringContainsString($successMessage);
verify($renderingResult)->stringContainsString($infoMessage);
verify($renderingResult)->stringContainsString($warningMessage);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringContainsString('alert-success');
verify($renderingResult)->stringContainsString('alert-info');
verify($renderingResult)->stringContainsString('alert-warning');
}
public function testMultipleMixedMessages() {
$firstErrorMessage = 'This is the first error message';
$secondErrorMessage = 'This is the second error message';
$firstDangerMessage = 'This is the first danger message';
$secondDangerMessage = 'This is the second';
$firstSuccessMessage = 'This is the first success message';
$secondSuccessMessage = 'This is the second success message';
$firstInfoMessage = 'This is the first info message';
$secondInfoMessage = 'This is the second info message';
$firstWarningMessage = 'This is the first warning message';
$secondWarningMessage = 'This is the second warning message';
Yii::$app->session->setFlash('error', [$firstErrorMessage, $secondErrorMessage]);
Yii::$app->session->setFlash('danger', [$firstDangerMessage, $secondDangerMessage]);
Yii::$app->session->setFlash('success', [$firstSuccessMessage, $secondSuccessMessage]);
Yii::$app->session->setFlash('info', [$firstInfoMessage, $secondInfoMessage]);
Yii::$app->session->setFlash('warning', [$firstWarningMessage, $secondWarningMessage]);
$renderingResult = Alert::widget();
verify($renderingResult)->stringContainsString($firstErrorMessage);
verify($renderingResult)->stringContainsString($secondErrorMessage);
verify($renderingResult)->stringContainsString($firstDangerMessage);
verify($renderingResult)->stringContainsString($secondDangerMessage);
verify($renderingResult)->stringContainsString($firstSuccessMessage);
verify($renderingResult)->stringContainsString($secondSuccessMessage);
verify($renderingResult)->stringContainsString($firstInfoMessage);
verify($renderingResult)->stringContainsString($secondInfoMessage);
verify($renderingResult)->stringContainsString($firstWarningMessage);
verify($renderingResult)->stringContainsString($secondWarningMessage);
verify($renderingResult)->stringContainsString('alert-danger');
verify($renderingResult)->stringContainsString('alert-success');
verify($renderingResult)->stringContainsString('alert-info');
verify($renderingResult)->stringContainsString('alert-warning');
}
public function testFlashIntegrity()
{
$errorMessage = 'This is an error message';
$unrelatedMessage = 'This is a message that is not related to the alert widget';
Yii::$app->session->setFlash('error', $errorMessage);
Yii::$app->session->setFlash('unrelated', $unrelatedMessage);
Alert::widget();
// Simulate redirect
Yii::$app->session->close();
Yii::$app->session->open();
verify(Yii::$app->session->getFlash('error'))->empty();
verify(Yii::$app->session->getFlash('unrelated'))->equals($unrelatedMessage);
}
}

2
vagrant/config/.gitignore

@ -0,0 +1,2 @@
# local configuration
vagrant-local.yml

22
vagrant/config/vagrant-local.example.yml

@ -0,0 +1,22 @@
# Your personal GitHub token
github_token: <your-personal-github-token>
# Read more: https://github.com/blog/1509-personal-api-tokens
# You can generate it here: https://github.com/settings/tokens
# Guest OS timezone
timezone: Europe/London
# Are we need check box updates for every 'vagrant up'?
box_check_update: false
# Virtual machine name
machine_name: yii2basic
# Virtual machine IP
ip: 192.168.83.137
# Virtual machine CPU cores number
cpus: 1
# Virtual machine RAM
memory: 1024

38
vagrant/nginx/app.conf

@ -0,0 +1,38 @@
server {
charset utf-8;
client_max_body_size 128M;
sendfile off;
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name yii2basic.test;
root /app/web/;
index index.php;
access_log /app/vagrant/nginx/log/yii2basic.access.log;
error_log /app/vagrant/nginx/log/yii2basic.error.log;
location / {
# Redirect everything that isn't a real file to index.php
try_files $uri $uri/ /index.php$is_args$args;
}
# uncomment to avoid processing of calls to non-existing static files by Yii
#location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
# try_files $uri =404;
#}
#error_page 404 /404.html;
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
#fastcgi_pass 127.0.0.1:9000;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
try_files $uri =404;
}
location ~ /\.(ht|svn|git) {
deny all;
}
}

3
vagrant/nginx/log/.gitignore

@ -0,0 +1,3 @@
#nginx logs
yii2basic.access.log
yii2basic.error.log

18
vagrant/provision/always-as-root.sh

@ -0,0 +1,18 @@
#!/usr/bin/env bash
#== Bash helpers ==
function info {
echo " "
echo "--> $1"
echo " "
}
#== Provision script ==
info "Provision-script user: `whoami`"
info "Restart web-stack"
service php7.2-fpm restart
service nginx restart
service mysql restart

79
vagrant/provision/once-as-root.sh

@ -0,0 +1,79 @@
#!/usr/bin/env bash
#== Import script args ==
timezone=$(echo "$1")
readonly IP=$2
#== Bash helpers ==
function info {
echo " "
echo "--> $1"
echo " "
}
#== Provision script ==
info "Provision-script user: `whoami`"
export DEBIAN_FRONTEND=noninteractive
info "Configure timezone"
timedatectl set-timezone ${timezone} --no-ask-password
info "Add the VM IP to the list of allowed IPs"
awk -v ip=$IP -f /app/vagrant/provision/provision.awk /app/config/web.php
info "Prepare root password for MySQL"
debconf-set-selections <<< 'mariadb-server mysql-server/root_password password'
debconf-set-selections <<< 'mariadb-server mysql-server/root_password_again password'
echo "Done!"
info "Update OS software"
apt-get update
apt-get upgrade -y
info "Install additional software"
apt-get install -y php7.2-curl php7.2-cli php7.2-intl php7.2-mysqlnd php7.2-gd php7.2-fpm php7.2-mbstring php7.2-xml unzip nginx mariadb-server-10.1 php.xdebug
info "Configure MySQL"
sed -i 's/.*bind-address.*/bind-address = 0.0.0.0/' /etc/mysql/mariadb.conf.d/50-server.cnf
mysql <<< "CREATE USER 'root'@'%' IDENTIFIED BY ''"
mysql <<< "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'"
mysql <<< "DROP USER 'root'@'localhost'"
mysql <<< 'FLUSH PRIVILEGES'
echo "Done!"
info "Configure PHP-FPM"
sed -i 's/user = www-data/user = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
sed -i 's/group = www-data/group = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
sed -i 's/owner = www-data/owner = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
cat << EOF > /etc/php/7.2/mods-available/xdebug.ini
zend_extension=xdebug.so
xdebug.remote_enable=1
xdebug.remote_connect_back=1
xdebug.remote_port=9000
xdebug.remote_autostart=1
EOF
echo "Done!"
info "Configure NGINX"
sed -i 's/user www-data/user vagrant/g' /etc/nginx/nginx.conf
echo "Done!"
info "Enabling site configuration"
ln -s /app/vagrant/nginx/app.conf /etc/nginx/sites-enabled/app.conf
echo "Done!"
info "Removing default site configuration"
rm /etc/nginx/sites-enabled/default
echo "Done!"
info "Initialize databases for MySQL"
mysql <<< 'CREATE DATABASE yii2basic'
mysql <<< 'CREATE DATABASE yii2basic_test'
echo "Done!"
info "Install composer"
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

31
vagrant/provision/once-as-vagrant.sh

@ -0,0 +1,31 @@
#!/usr/bin/env bash
#== Import script args ==
github_token=$(echo "$1")
#== Bash helpers ==
function info {
echo " "
echo "--> $1"
echo " "
}
#== Provision script ==
info "Provision-script user: `whoami`"
info "Configure composer"
composer config --global github-oauth.github.com ${github_token}
echo "Done!"
info "Install project dependencies"
cd /app
composer --no-progress --prefer-dist install
info "Create bash-alias 'app' for vagrant user"
echo 'alias app="cd /app"' | tee /home/vagrant/.bash_aliases
info "Enabling colorized prompt for guest console"
sed -i "s/#force_color_prompt=yes/force_color_prompt=yes/" /home/vagrant/.bashrc

50
vagrant/provision/provision.awk

@ -0,0 +1,50 @@
###
# Modifying Yii2's files for Vagrant VM
#
# @author HA3IK <golubha3ik@gmail.com>
# @version 1.0.0
BEGIN {
print "AWK BEGINs its work:"
IGNORECASE = 1
# Correct IP - wildcard last octet
match(ip, /(([0-9]+\.)+)/, arr)
ip = arr[1] "*"
}
# BODY
{
# Check if it's the same file
if (FILENAME != isFile["same"]){
msg = "- Work with: " FILENAME
# Close a previous file
close(isFile["same"])
# Delete previous data
delete isFile
# Save current file
isFile["same"] = FILENAME
# Define array index for the file
switch (FILENAME){
case /config\/web\.php$/:
isFile["IsConfWeb"] = 1
msg = msg " - add allowed IP: " ip
break
}
# Print the concatenated message for the file
print msg
}
# IF config/web.php
if (isFile["IsConfWeb"]){
# IF line has "allowedIPs" and doesn't has our IP
if (match($0, "allowedIPs") && !match($0, ip)){
match($0, /([^\]]+)(.+)/, arr)
$0 = sprintf("%s, '%s'%s", arr[1], ip, arr[2])
}
# Rewrite the file
print $0 > FILENAME
}
}
END {
print "AWK ENDs its work."
}

83
views/layouts/main.php

@ -0,0 +1,83 @@
<?php
/** @var yii\web\View $this */
/** @var string $content */
use app\assets\AppAsset;
use app\widgets\Alert;
use yii\bootstrap5\Breadcrumbs;
use yii\bootstrap5\Html;
use yii\bootstrap5\Nav;
use yii\bootstrap5\NavBar;
AppAsset::register($this);
$this->registerCsrfMetaTags();
$this->registerMetaTag(['charset' => Yii::$app->charset], 'charset');
$this->registerMetaTag(['name' => 'viewport', 'content' => 'width=device-width, initial-scale=1, shrink-to-fit=no']);
$this->registerMetaTag(['name' => 'description', 'content' => $this->params['meta_description'] ?? '']);
$this->registerMetaTag(['name' => 'keywords', 'content' => $this->params['meta_keywords'] ?? '']);
$this->registerLinkTag(['rel' => 'icon', 'type' => 'image/x-icon', 'href' => Yii::getAlias('@web/favicon.ico')]);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>" class="h-100">
<head>
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>
<body class="d-flex flex-column h-100">
<?php $this->beginBody() ?>
<header id="header">
<?php
NavBar::begin([
'brandLabel' => Yii::$app->name,
'brandUrl' => Yii::$app->homeUrl,
'options' => ['class' => 'navbar-expand-md navbar-dark bg-dark fixed-top']
]);
echo Nav::widget([
'options' => ['class' => 'navbar-nav'],
'items' => [
['label' => 'Home', 'url' => ['/site/index']],
['label' => 'About', 'url' => ['/site/about']],
['label' => 'Contact', 'url' => ['/site/contact']],
Yii::$app->user->isGuest
? ['label' => 'Login', 'url' => ['/site/login']]
: '<li class="nav-item">'
. Html::beginForm(['/site/logout'])
. Html::submitButton(
'Logout (' . Yii::$app->user->identity->username . ')',
['class' => 'nav-link btn btn-link logout']
)
. Html::endForm()
. '</li>'
]
]);
NavBar::end();
?>
</header>
<main id="main" class="flex-shrink-0" role="main">
<div class="container">
<?php if (!empty($this->params['breadcrumbs'])): ?>
<?= Breadcrumbs::widget(['links' => $this->params['breadcrumbs']]) ?>
<?php endif ?>
<?= Alert::widget() ?>
<?= $content ?>
</div>
</main>
<footer id="footer" class="mt-auto py-3 bg-light">
<div class="container">
<div class="row text-muted">
<div class="col-md-6 text-center text-md-start">&copy; My Company <?= date('Y') ?></div>
<div class="col-md-6 text-center text-md-end"><?= Yii::powered() ?></div>
</div>
</div>
</footer>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

18
views/site/about.php

@ -0,0 +1,18 @@
<?php
/** @var yii\web\View $this */
use yii\helpers\Html;
$this->title = 'About';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-about">
<h1><?= Html::encode($this->title) ?></h1>
<p>
This is the About page. You may modify the following file to customize its content:
</p>
<code><?= __FILE__ ?></code>
</div>

68
views/site/contact.php

@ -0,0 +1,68 @@
<?php
/** @var yii\web\View $this */
/** @var yii\bootstrap5\ActiveForm $form */
/** @var app\models\ContactForm $model */
use yii\bootstrap5\ActiveForm;
use yii\bootstrap5\Html;
use yii\captcha\Captcha;
$this->title = 'Contact';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-contact">
<h1><?= Html::encode($this->title) ?></h1>
<?php if (Yii::$app->session->hasFlash('contactFormSubmitted')): ?>
<div class="alert alert-success">
Thank you for contacting us. We will respond to you as soon as possible.
</div>
<p>
Note that if you turn on the Yii debugger, you should be able
to view the mail message on the mail panel of the debugger.
<?php if (Yii::$app->mailer->useFileTransport): ?>
Because the application is in development mode, the email is not sent but saved as
a file under <code><?= Yii::getAlias(Yii::$app->mailer->fileTransportPath) ?></code>.
Please configure the <code>useFileTransport</code> property of the <code>mail</code>
application component to be false to enable email sending.
<?php endif; ?>
</p>
<?php else: ?>
<p>
If you have business inquiries or other questions, please fill out the following form to contact us.
Thank you.
</p>
<div class="row">
<div class="col-lg-5">
<?php $form = ActiveForm::begin(['id' => 'contact-form']); ?>
<?= $form->field($model, 'name')->textInput(['autofocus' => true]) ?>
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'subject') ?>
<?= $form->field($model, 'body')->textarea(['rows' => 6]) ?>
<?= $form->field($model, 'verifyCode')->widget(Captcha::class, [
'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>',
]) ?>
<div class="form-group">
<?= Html::submitButton('Submit', ['class' => 'btn btn-primary', 'name' => 'contact-button']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
</div>
<?php endif; ?>
</div>

27
views/site/error.php

@ -0,0 +1,27 @@
<?php
/** @var yii\web\View $this */
/** @var string $name */
/** @var string $message */
/** @var Exception$exception */
use yii\helpers\Html;
$this->title = $name;
?>
<div class="site-error">
<h1><?= Html::encode($this->title) ?></h1>
<div class="alert alert-danger">
<?= nl2br(Html::encode($message)) ?>
</div>
<p>
The above error occurred while the Web server was processing your request.
</p>
<p>
Please contact us if you think this is a server error. Thank you.
</p>
</div>

53
views/site/index.php

@ -0,0 +1,53 @@
<?php
/** @var yii\web\View $this */
$this->title = 'My Yii Application';
?>
<div class="site-index">
<div class="jumbotron text-center bg-transparent mt-5 mb-5">
<h1 class="display-4">Congratulations!</h1>
<p class="lead">You have successfully created your Yii-powered application.</p>
<p><a class="btn btn-lg btn-success" href="https://www.yiiframework.com">Get started with Yii</a></p>
</div>
<div class="body-content">
<div class="row">
<div class="col-lg-4 mb-3">
<h2>Heading</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.</p>
<p><a class="btn btn-outline-secondary" href="https://www.yiiframework.com/doc/">Yii Documentation &raquo;</a></p>
</div>
<div class="col-lg-4 mb-3">
<h2>Heading</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.</p>
<p><a class="btn btn-outline-secondary" href="https://www.yiiframework.com/forum/">Yii Forum &raquo;</a></p>
</div>
<div class="col-lg-4">
<h2>Heading</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.</p>
<p><a class="btn btn-outline-secondary" href="https://www.yiiframework.com/extensions/">Yii Extensions &raquo;</a></p>
</div>
</div>
</div>
</div>

55
views/site/login.php

@ -0,0 +1,55 @@
<?php
/** @var yii\web\View $this */
/** @var yii\bootstrap5\ActiveForm $form */
/** @var app\models\LoginForm $model */
use yii\bootstrap5\ActiveForm;
use yii\bootstrap5\Html;
$this->title = 'Login';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-login">
<h1><?= Html::encode($this->title) ?></h1>
<p>Please fill out the following fields to login:</p>
<div class="row">
<div class="col-lg-5">
<?php $form = ActiveForm::begin([
'id' => 'login-form',
'fieldConfig' => [
'template' => "{label}\n{input}\n{error}",
'labelOptions' => ['class' => 'col-lg-1 col-form-label mr-lg-3'],
'inputOptions' => ['class' => 'col-lg-3 form-control'],
'errorOptions' => ['class' => 'col-lg-7 invalid-feedback'],
],
]); ?>
<?= $form->field($model, 'username')->textInput(['autofocus' => true]) ?>
<?= $form->field($model, 'password')->passwordInput() ?>
<?= $form->field($model, 'rememberMe')->checkbox([
'template' => "<div class=\"custom-control custom-checkbox\">{input} {label}</div>\n<div class=\"col-lg-8\">{error}</div>",
]) ?>
<div class="form-group">
<div>
<?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
</div>
</div>
<?php ActiveForm::end(); ?>
<div style="color:#999;">
You may login with <strong>admin/admin</strong> or <strong>demo/demo</strong>.<br>
To modify the username/password, please check out the code <code>app\models\User::$users</code>.
</div>
</div>
</div>
</div>

4
web/.htaccess

@ -0,0 +1,4 @@
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]

2
web/assets/.gitignore

@ -0,0 +1,2 @@
*
!.gitignore

88
web/css/site.css

@ -0,0 +1,88 @@
main > .container {
padding: 70px 15px 20px;
}
.footer {
background-color: #f5f5f5;
font-size: .9em;
height: 60px;
}
.footer > .container {
padding-right: 15px;
padding-left: 15px;
}
.not-set {
color: #c55;
font-style: italic;
}
/* add sorting icons to gridview sort links */
a.asc:after, a.desc:after {
content: '';
left: 3px;
display: inline-block;
width: 0;
height: 0;
border: solid 5px transparent;
margin: 4px 4px 2px 4px;
background: transparent;
}
a.asc:after {
border-bottom: solid 7px #212529;
border-top-width: 0;
}
a.desc:after {
border-top: solid 7px #212529;
border-bottom-width: 0;
}
.grid-view th {
white-space: nowrap;
}
.hint-block {
display: block;
margin-top: 5px;
color: #999;
}
.error-summary {
color: #a94442;
background: #fdf7f7;
border-left: 3px solid #eed3d7;
padding: 10px 20px;
margin: 0 0 15px 0;
}
/* align the logout "link" (button in form) of the navbar */
.nav li > form > button.logout {
padding-top: 7px;
color: rgba(255, 255, 255, 0.5);
}
@media(max-width:767px) {
.nav li > form > button.logout {
display:block;
text-align: left;
width: 100%;
padding: 10px 0;
}
}
.nav > li > form > button.logout:focus,
.nav > li > form > button.logout:hover {
text-decoration: none;
color: rgba(255, 255, 255, 0.75);
}
.nav > li > form > button.logout:focus {
outline: none;
}
.form-group {
margin-bottom: 1rem;
}

BIN
web/favicon.ico

16
web/index-test.php

@ -0,0 +1,16 @@
<?php
// NOTE: Make sure this file is not accessible when deployed to production
if (!in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
die('You are not allowed to access this file.');
}
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test');
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
$config = require __DIR__ . '/../config/test.php';
(new yii\web\Application($config))->run();

12
web/index.php

@ -0,0 +1,12 @@
<?php
// comment out the following two lines when deployed to production
defined('YII_DEBUG') or define('YII_DEBUG', true);//定義兩個常量,查看你當前是運行環境還是開發環境
defined('YII_ENV') or define('YII_ENV', 'dev');
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
$config = require __DIR__ . '/../config/web.php';
(new yii\web\Application($config))->run();

2
web/robots.txt

@ -0,0 +1,2 @@
User-agent: *
Disallow:

73
widgets/Alert.php

@ -0,0 +1,73 @@
<?php
namespace app\widgets;
use Yii;
/**
* Alert widget renders a message from session flash. All flash messages are displayed
* in the sequence they were assigned using setFlash. You can set message as following:
*
* ```php
* Yii::$app->session->setFlash('error', 'This is the message');
* Yii::$app->session->setFlash('success', 'This is the message');
* Yii::$app->session->setFlash('info', 'This is the message');
* ```
*
* Multiple messages could be set as follows:
*
* ```php
* Yii::$app->session->setFlash('error', ['Error 1', 'Error 2']);
* ```
*
* @author Kartik Visweswaran <kartikv2@gmail.com>
* @author Alexander Makarov <sam@rmcreative.ru>
*/
class Alert extends \yii\bootstrap5\Widget
{
/**
* @var array the alert types configuration for the flash messages.
* This array is setup as $key => $value, where:
* - key: the name of the session flash variable
* - value: the bootstrap alert type (i.e. danger, success, info, warning)
*/
public $alertTypes = [
'error' => 'alert-danger',
'danger' => 'alert-danger',
'success' => 'alert-success',
'info' => 'alert-info',
'warning' => 'alert-warning'
];
/**
* @var array the options for rendering the close button tag.
* Array will be passed to [[\yii\bootstrap\Alert::closeButton]].
*/
public $closeButton = [];
/**
* {@inheritdoc}
*/
public function run()
{
$session = Yii::$app->session;
$appendClass = isset($this->options['class']) ? ' ' . $this->options['class'] : '';
foreach (array_keys($this->alertTypes) as $type) {
$flash = $session->getFlash($type);
foreach ((array) $flash as $i => $message) {
echo \yii\bootstrap5\Alert::widget([
'body' => $message,
'closeButton' => $this->closeButton,
'options' => array_merge($this->options, [
'id' => $this->getId() . '-' . $type . '-' . $i,
'class' => $this->alertTypes[$type] . $appendClass,
]),
]);
}
$session->removeFlash($type);
}
}
}

21
yii

@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
/**
* Yii console bootstrap file.
*
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/vendor/yiisoft/yii2/Yii.php';
$config = require __DIR__ . '/config/console.php';
$application = new yii\console\Application($config);
$exitCode = $application->run();
exit($exitCode);

20
yii.bat

@ -0,0 +1,20 @@
@echo off
rem -------------------------------------------------------------
rem Yii command line bootstrap script for Windows.
rem
rem @author Qiang Xue <qiang.xue@gmail.com>
rem @link https://www.yiiframework.com/
rem @copyright Copyright (c) 2008 Yii Software LLC
rem @license https://www.yiiframework.com/license/
rem -------------------------------------------------------------
@setlocal
set YII_PATH=%~dp0
if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
"%PHP_COMMAND%" "%YII_PATH%yii" %*
@endlocal
Loading…
Cancel
Save