diff --git a/.bowerrc b/.bowerrc
index 1669168f2..a39b5b0fb 100644
--- a/.bowerrc
+++ b/.bowerrc
@@ -1,3 +1,3 @@
 {
-    "directory" : "vendor/bower"
+    "directory" : "vendor/bower-asset"
 }
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 3d8d32c25..ea75bda5f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -3,5 +3,5 @@ Contributing to Yii2
 
 - [Report an issue](https://github.com/yiisoft/yii2/blob/master/docs/internals/report-an-issue.md)
 - [Translate documentation or messages](https://github.com/yiisoft/yii2/blob/master/docs/internals/translation-workflow.md)
-- [Give us feedback or start a design discussion](http://www.yiiframework.com/forum/index.php/forum/42-general-discussions-for-yii-20/)
+- [Give us feedback or start a design discussion](https://www.yiiframework.com/forum/index.php/forum/42-general-discussions-for-yii-20/)
 - [Contribute to the core code or fix bugs](https://github.com/yiisoft/yii2/blob/master/docs/internals/git-workflow.md)
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..692239301
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+open_collective: yiisoft
+tidelift: "packagist/yiisoft/yii2-app-basic"
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 52ec8692f..a2ee92439 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -9,6 +9,6 @@
 
 | Q                | A
 | ---------------- | ---
-| Yii vesion       |
+| Yii version       |
 | PHP version      |
 | Operating system |
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 000000000..405acca4e
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,6 @@
+# Security Policy
+
+Please use the [security issue form](https://www.yiiframework.com/security) to report to us any security issue you find in Yii.
+DO NOT use the issue tracker or discuss it in the public forum as it will cause more damage than help.
+
+Please note that as a non-commercial OpenSource project we are not able to pay bounties at the moment.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..a5beac653
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,83 @@
+on:
+  - pull_request
+  - push
+
+name: build
+
+jobs:
+  tests:
+    name: PHP ${{ matrix.php }} - ${{ matrix.os }}
+
+    env:
+      extensions: dom, json, gd, imagick
+      key: cache-v4
+
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      matrix:
+        os:
+          - ubuntu-latest
+          - windows-latest
+
+        php:
+          - "7.4"
+          - "8.0"
+          - "8.1"
+          - "8.2"
+          - "8.3"
+          - "8.4"
+          
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v5
+
+      - name: Setup cache environment
+        id: cache-env
+        uses: shivammathur/cache-extensions@v1
+        with:
+          php-version: ${{ matrix.php }}
+          extensions: ${{ env.extensions }}
+          key: ${{ env.key }}
+
+      - name: Cache extensions
+        uses: actions/cache@v4
+        with:
+          path: ${{ steps.cache-env.outputs.dir }}
+          key: ${{ steps.cache-env.outputs.key }}
+          restore-keys: ${{ steps.cache-env.outputs.key }}
+
+      - name: Install PHP with extensions
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php }}
+          extensions: ${{ env.extensions }}
+          ini-values: date.timezone='UTC'
+          
+      - name: Determine composer cache directory on Linux
+        if: matrix.os == 'ubuntu-latest'
+        run: |
+            echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV
+
+      - name: Determine composer cache directory on Windows
+        if: matrix.os == 'windows-latest'
+        run: |
+            echo "COMPOSER_CACHE_DIR=~\AppData\Local\Composer" >> $GITHUB_ENV
+
+      - name: Cache dependencies installed with composer
+        uses: actions/cache@v4
+        with:
+          path: ${{ steps.cache-env.outputs.dir }}
+          key: php${{ matrix.php }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}
+          restore-keys: |
+            php${{ matrix.php }}-composer-${{ matrix.dependencies }}-
+
+      - name: Install dependencies with composer php PHP [5.6 - 8.0]
+        run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi
+
+      - name: Run tests with codeception
+        run: |
+          sed -i "s/'cookieValidationKey' => ''/'cookieValidationKey' => 'testkey'/" config/web.php
+          php -S 127.0.0.1:8080 -t public > ./runtime/yii.log 2>&1 &
+          vendor/bin/codecept run
+        shell: bash
diff --git a/.gitignore b/.gitignore
index 45bf7bf41..5bd9be80b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,9 @@ composer.phar
 phpunit.phar
 # local phpunit config
 /phpunit.xml
+
+tests/_output/*
+tests/_support/_generated
+
+#vagrant folder
+/.vagrant
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 24b5d54ae..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,37 +0,0 @@
-language: php
-
-php:
-  - 5.4
-  - 5.5
-  - 5.6
-  - 7.0
-#  - hhvm
-
-# faster builds on new travis setup not using sudo
-sudo: false
-
-# cache vendor dirs
-cache:
-  directories:
-    - $HOME/.composer/cache
-
-install:
-  - travis_retry composer self-update && composer --version
-  - travis_retry composer global require "fxp/composer-asset-plugin:~1.1.1"
-  - export PATH="$HOME/.composer/vendor/bin:$PATH"
-  - travis_retry composer install --dev --prefer-dist --no-interaction
-# codeception
-  - travis_retry composer global require "codeception/codeception=2.0.*" "codeception/specify=*" "codeception/verify=*"
-# setup application:
-  - |
-    sed -i "s/'cookieValidationKey' => ''/'cookieValidationKey' => 'testkey'/" config/web.php
-    cd tests
-    codecept build
-    cd ..
-
-script:
-  - |
-    cd web
-    php -S localhost:8080 > /dev/null 2>&1 &
-    cd ../tests
-    codecept run
diff --git a/LICENSE.md b/LICENSE.md
index e98f03df8..ee872b9ab 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,3 @@
-The Yii framework is free software. It is released under the terms of
-the following BSD License.
-
 Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com)
 All rights reserved.
 
diff --git a/README.md b/README.md
index 598f9637b..981a7f9d9 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,21 @@
-Yii 2 Basic Project Template
-============================
-
-Yii 2 Basic Project Template is a skeleton [Yii 2](http://www.yiiframework.com/) application best for
+<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://poser.pugx.org/yiisoft/yii2-app-basic/v/stable.png)](https://packagist.org/packages/yiisoft/yii2-app-basic)
-[![Total Downloads](https://poser.pugx.org/yiisoft/yii2-app-basic/downloads.png)](https://packagist.org/packages/yiisoft/yii2-app-basic)
-[![Build Status](https://travis-ci.org/yiisoft/yii2-app-basic.svg?branch=master)](https://travis-ci.org/yiisoft/yii2-app-basic)
+[![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
 -------------------
@@ -32,15 +37,33 @@ DIRECTORY STRUCTURE
 REQUIREMENTS
 ------------
 
-The minimum requirement by this project template that your Web server supports PHP 5.4.0.
+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](http://www.yiiframework.com/download/) to
+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:
@@ -59,24 +82,27 @@ http://localhost/basic/web/
 ~~~
 
 
-### Install via Composer
+### Install with Docker
 
-If you do not have [Composer](http://getcomposer.org/), you may install it by following the instructions
-at [getcomposer.org](http://getcomposer.org/doc/00-intro.md#installation-nix).
+Update your vendor packages
 
-You can then install this project template using the following command:
+    docker-compose run --rm php composer update --prefer-dist
+    
+Run the installation triggers (creating cookie validation code)
 
-~~~
-php composer.phar global require "fxp/composer-asset-plugin:~1.1.1"
-php composer.phar create-project --prefer-dist --stability=dev yiisoft/yii2-app-basic basic
-~~~
+    docker-compose run --rm php composer install    
+    
+Start the container
 
-Now you should be able to access the application through the following URL, assuming `basic` is the directory
-directly under the Web root.
+    docker-compose up -d
+    
+You can then access the application through the following URL:
 
-~~~
-http://localhost/basic/web/
-~~~
+    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
@@ -100,3 +126,108 @@ return [
 - 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.
diff --git a/Vagrantfile b/Vagrantfile
new file mode 100644
index 000000000..258aea601
--- /dev/null
+++ b/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
diff --git a/assets/AppAsset.php b/assets/AppAsset.php
index 0e495a8f8..3d40487ae 100644
--- a/assets/AppAsset.php
+++ b/assets/AppAsset.php
@@ -1,8 +1,8 @@
 <?php
 /**
- * @link http://www.yiiframework.com/
+ * @link https://www.yiiframework.com/
  * @copyright Copyright (c) 2008 Yii Software LLC
- * @license http://www.yiiframework.com/license/
+ * @license https://www.yiiframework.com/license/
  */
 
 namespace app\assets;
@@ -10,6 +10,8 @@
 use yii\web\AssetBundle;
 
 /**
+ * Main application asset bundle.
+ *
  * @author Qiang Xue <qiang.xue@gmail.com>
  * @since 2.0
  */
@@ -24,6 +26,6 @@ class AppAsset extends AssetBundle
     ];
     public $depends = [
         'yii\web\YiiAsset',
-        'yii\bootstrap\BootstrapAsset',
+        'yii\bootstrap5\BootstrapAsset'
     ];
 }
diff --git a/codeception.yml b/codeception.yml
new file mode 100644
index 000000000..dd8febcff
--- /dev/null
+++ b/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/*
diff --git a/commands/HelloController.php b/commands/HelloController.php
index 86ab8b853..9ec914d3c 100644
--- a/commands/HelloController.php
+++ b/commands/HelloController.php
@@ -1,13 +1,14 @@
 <?php
 /**
- * @link http://www.yiiframework.com/
+ * @link https://www.yiiframework.com/
  * @copyright Copyright (c) 2008 Yii Software LLC
- * @license http://www.yiiframework.com/license/
+ * @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.
@@ -22,9 +23,12 @@ 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;
     }
 }
diff --git a/composer.json b/composer.json
index 4ad378784..4b6cab88c 100644
--- a/composer.json
+++ b/composer.json
@@ -2,35 +2,50 @@
     "name": "yiisoft/yii2-app-basic",
     "description": "Yii 2 Basic Project Template",
     "keywords": ["yii2", "framework", "basic", "project template"],
-    "homepage": "http://www.yiiframework.com/",
+    "homepage": "https://www.yiiframework.com/",
     "type": "project",
     "license": "BSD-3-Clause",
     "support": {
         "issues": "https://github.com/yiisoft/yii2/issues?state=open",
-        "forum": "http://www.yiiframework.com/forum/",
-        "wiki": "http://www.yiiframework.com/wiki/",
-        "irc": "irc://irc.freenode.net/yii",
+        "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": "dev",
     "require": {
-        "php": ">=5.4.0",
-        "yiisoft/yii2": ">=2.0.5",
-        "yiisoft/yii2-bootstrap": "*",
-        "yiisoft/yii2-swiftmailer": "*"
+        "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-codeception": "*",
-        "yiisoft/yii2-debug": "*",
-        "yiisoft/yii2-gii": "*",
-        "yiisoft/yii2-faker": "*"
+        "yiisoft/yii2-debug": "~2.1.0",
+        "yiisoft/yii2-gii": "~2.2.0",
+        "yiisoft/yii2-faker": "~2.0.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"
     },
     "config": {
-        "process-timeout": 1800
+        "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::postCreateProject",
+            "yii\\composer\\Installer::postInstall"
         ]
     },
     "extra": {
@@ -41,14 +56,18 @@
                     "web/assets": "0777",
                     "yii": "0755"
                 }
-            ],
+            ]
+        },
+        "yii\\composer\\Installer::postInstall": {
             "generateCookieValidationKey": [
                 "config/web.php"
             ]
-        },
-        "asset-installer-paths": {
-            "npm-asset-library": "vendor/npm",
-            "bower-asset-library": "vendor/bower"
         }
-    }
+    },
+    "repositories": [
+        {
+            "type": "composer",
+            "url": "https://asset-packagist.org"
+        }
+    ]
 }
diff --git a/config/__autocomplete.php b/config/__autocomplete.php
new file mode 100644
index 000000000..d99dea50e
--- /dev/null
+++ b/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 {
+}
diff --git a/config/console.php b/config/console.php
index 7306e2764..ca429affa 100644
--- a/config/console.php
+++ b/config/console.php
@@ -1,15 +1,18 @@
 <?php
 
-Yii::setAlias('@tests', dirname(__DIR__) . '/tests/codeception');
-
-$params = require(__DIR__ . '/params.php');
-$db = require(__DIR__ . '/db.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',
@@ -40,6 +43,14 @@
     $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;
diff --git a/config/db.php b/config/db.php
index c4c12529c..bc75e616f 100644
--- a/config/db.php
+++ b/config/db.php
@@ -6,4 +6,9 @@
     'username' => 'root',
     'password' => '',
     'charset' => 'utf8',
+
+    // Schema cache options (for production environment)
+    //'enableSchemaCache' => true,
+    //'schemaCacheDuration' => 60,
+    //'schemaCache' => 'cache',
 ];
diff --git a/config/params.php b/config/params.php
index 6ebf2792b..981c621ac 100644
--- a/config/params.php
+++ b/config/params.php
@@ -2,4 +2,6 @@
 
 return [
     'adminEmail' => 'admin@example.com',
+    'senderEmail' => 'noreply@example.com',
+    'senderName' => 'Example.com mailer',
 ];
diff --git a/config/test.php b/config/test.php
new file mode 100644
index 000000000..317bc1cb6
--- /dev/null
+++ b/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,
+];
diff --git a/config/test_db.php b/config/test_db.php
new file mode 100644
index 000000000..f4290e0b0
--- /dev/null
+++ b/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;
diff --git a/config/web.php b/config/web.php
index 90a82876b..8b9dd3f94 100644
--- a/config/web.php
+++ b/config/web.php
@@ -1,11 +1,16 @@
 <?php
 
-$params = require(__DIR__ . '/params.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
@@ -22,10 +27,9 @@
             'errorAction' => 'site/error',
         ],
         'mailer' => [
-            'class' => 'yii\swiftmailer\Mailer',
-            // send all mails to a file by default. You have to set
-            // 'useFileTransport' to false and configure a transport
-            // for the mailer to send real emails.
+            'class' => \yii\symfonymailer\Mailer::class,
+            'viewPath' => '@app/mail',
+            // send all mails to a file by default.
             'useFileTransport' => true,
         ],
         'log' => [
@@ -37,7 +41,7 @@
                 ],
             ],
         ],
-        'db' => require(__DIR__ . '/db.php'),
+        'db' => $db,
         /*
         'urlManager' => [
             'enablePrettyUrl' => true,
@@ -55,11 +59,15 @@
     $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'],
     ];
 }
 
diff --git a/controllers/SiteController.php b/controllers/SiteController.php
index df1a0d376..67c3f50f8 100644
--- a/controllers/SiteController.php
+++ b/controllers/SiteController.php
@@ -5,17 +5,21 @@
 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::className(),
+                'class' => AccessControl::class,
                 'only' => ['logout'],
                 'rules' => [
                     [
@@ -26,7 +30,7 @@ public function behaviors()
                 ],
             ],
             'verbs' => [
-                'class' => VerbFilter::className(),
+                'class' => VerbFilter::class,
                 'actions' => [
                     'logout' => ['post'],
                 ],
@@ -34,6 +38,9 @@ public function behaviors()
         ];
     }
 
+    /**
+     * {@inheritdoc}
+     */
     public function actions()
     {
         return [
@@ -47,11 +54,21 @@ public function actions()
         ];
     }
 
+    /**
+     * Displays homepage.
+     *
+     * @return string
+     */
     public function actionIndex()
     {
         return $this->render('index');
     }
 
+    /**
+     * Login action.
+     *
+     * @return Response|string
+     */
     public function actionLogin()
     {
         if (!Yii::$app->user->isGuest) {
@@ -62,11 +79,18 @@ public function actionLogin()
         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();
@@ -74,6 +98,11 @@ public function actionLogout()
         return $this->goHome();
     }
 
+    /**
+     * Displays contact page.
+     *
+     * @return Response|string
+     */
     public function actionContact()
     {
         $model = new ContactForm();
@@ -87,6 +116,11 @@ public function actionContact()
         ]);
     }
 
+    /**
+     * Displays about page.
+     *
+     * @return string
+     */
     public function actionAbout()
     {
         return $this->render('about');
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..86be3bd0d
--- /dev/null
+++ b/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'
diff --git a/mail/layouts/html.php b/mail/layouts/html.php
index bddbc6129..95732cdfa 100644
--- a/mail/layouts/html.php
+++ b/mail/layouts/html.php
@@ -1,9 +1,9 @@
 <?php
 use yii\helpers\Html;
 
-/* @var $this \yii\web\View view component instance */
-/* @var $message \yii\mail\MessageInterface the message being composed */
-/* @var $content string main view render result */
+/** @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">
diff --git a/mail/layouts/text.php b/mail/layouts/text.php
new file mode 100644
index 000000000..c3768640b
--- /dev/null
+++ b/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();
diff --git a/models/ContactForm.php b/models/ContactForm.php
index 37f48b312..f001d2192 100644
--- a/models/ContactForm.php
+++ b/models/ContactForm.php
@@ -45,14 +45,15 @@ public function attributeLabels()
     /**
      * Sends an email to the specified email address using the information collected by this model.
      * @param string $email the target email address
-     * @return boolean whether the model passes validation
+     * @return bool whether the model passes validation
      */
     public function contact($email)
     {
         if ($this->validate()) {
             Yii::$app->mailer->compose()
                 ->setTo($email)
-                ->setFrom([$this->email => $this->name])
+                ->setFrom([Yii::$app->params['senderEmail'] => Yii::$app->params['senderName']])
+                ->setReplyTo([$this->email => $this->name])
                 ->setSubject($this->subject)
                 ->setTextBody($this->body)
                 ->send();
diff --git a/models/LoginForm.php b/models/LoginForm.php
index 7ef5c99b3..dce15ccf7 100644
--- a/models/LoginForm.php
+++ b/models/LoginForm.php
@@ -8,7 +8,7 @@
 /**
  * LoginForm is the model behind the login form.
  *
- * @property User|null $user This property is read-only.
+ * @property-read User|null $user
  *
  */
 class LoginForm extends Model
@@ -55,7 +55,7 @@ public function validatePassword($attribute, $params)
 
     /**
      * Logs in a user using the provided username and password.
-     * @return boolean whether the user is logged in successfully
+     * @return bool whether the user is logged in successfully
      */
     public function login()
     {
diff --git a/models/User.php b/models/User.php
index 163a64fcd..2e3fb25ed 100644
--- a/models/User.php
+++ b/models/User.php
@@ -2,7 +2,7 @@
 
 namespace app\models;
 
-class User extends \yii\base\Object implements \yii\web\IdentityInterface
+class User extends \yii\base\BaseObject implements \yii\web\IdentityInterface
 {
     public $id;
     public $username;
@@ -29,7 +29,7 @@ class User extends \yii\base\Object implements \yii\web\IdentityInterface
 
 
     /**
-     * @inheritdoc
+     * {@inheritdoc}
      */
     public static function findIdentity($id)
     {
@@ -37,7 +37,7 @@ public static function findIdentity($id)
     }
 
     /**
-     * @inheritdoc
+     * {@inheritdoc}
      */
     public static function findIdentityByAccessToken($token, $type = null)
     {
@@ -68,7 +68,7 @@ public static function findByUsername($username)
     }
 
     /**
-     * @inheritdoc
+     * {@inheritdoc}
      */
     public function getId()
     {
@@ -76,7 +76,7 @@ public function getId()
     }
 
     /**
-     * @inheritdoc
+     * {@inheritdoc}
      */
     public function getAuthKey()
     {
@@ -84,7 +84,7 @@ public function getAuthKey()
     }
 
     /**
-     * @inheritdoc
+     * {@inheritdoc}
      */
     public function validateAuthKey($authKey)
     {
@@ -95,7 +95,7 @@ public function validateAuthKey($authKey)
      * Validates password
      *
      * @param string $password password to validate
-     * @return boolean if password provided is valid for current user
+     * @return bool if password provided is valid for current user
      */
     public function validatePassword($password)
     {
diff --git a/requirements.php b/requirements.php
index ee3184f72..c1a6bf680 100644
--- a/requirements.php
+++ b/requirements.php
@@ -7,17 +7,40 @@
  *
  * 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
+ * ln ../requirements.php requirements.php
  */
 
 // you may need to adjust this path to the correct Yii framework path
-$frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2';
+// uncomment and adjust the following line if Yii is not located at the default path
+//$frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2';
 
-if (!is_dir($frameworkPath)) {
-    echo '<h1>Error</h1>';
-    echo '<p><strong>The path to yii framework seems to be incorrect.</strong></p>';
-    echo '<p>You need to install Yii framework via composer or adjust the framework path in file <abbr title="' . __FILE__ . '">' . basename(__FILE__) . '</abbr>.</p>';
-    echo '<p>Please refer to the <abbr title="' . dirname(__FILE__) . '/README.md">README</abbr> on how to install Yii.</p>';
+
+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');
@@ -82,22 +105,22 @@
         'name' => 'Memcache extension',
         'mandatory' => false,
         'condition' => extension_loaded('memcache') || extension_loaded('memcached'),
-        'by' => '<a href="http://www.yiiframework.com/doc-2.0/yii-caching-memcache.html">MemCache</a>',
-        'memo' => extension_loaded('memcached') ? 'To use memcached set <a href="http://www.yiiframework.com/doc-2.0/yii-caching-memcache.html#$useMemcached-detail">MemCache::useMemcached</a> to <code>true</code>.' : ''
+        '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="http://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
+        '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="http://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
+        'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
         'memo' => $imagickMemo,
     ),
     // PHP ini :
@@ -130,8 +153,10 @@
         'name' => 'APC extension',
         'mandatory' => false,
         'condition' => extension_loaded('apc'),
-        'by' => '<a href="http://www.yiiframework.com/doc-2.0/yii-caching-apccache.html">ApcCache</a>',
+        'by' => '<a href="https://www.yiiframework.com/doc-2.0/yii-caching-apccache.html">ApcCache</a>',
     );
 }
 
-$requirementsChecker->checkYii()->check($requirements)->render();
+$result = $requirementsChecker->checkYii()->check($requirements)->getResult();
+$requirementsChecker->render();
+exit($result['summary']['errors'] === 0 ? 0 : 1);
diff --git a/tests/README.md b/tests/README.md
deleted file mode 100644
index fd80fe55c..000000000
--- a/tests/README.md
+++ /dev/null
@@ -1,127 +0,0 @@
-This directory contains various tests for the basic application.
-
-Tests in `codeception` directory are developed with [Codeception PHP Testing Framework](http://codeception.com/).
-
-After creating the basic application, follow these steps to prepare for the tests:
-
-1. Install Codeception if it's not yet installed:
-
-   ```
-   composer global require "codeception/codeception=2.0.*"
-   composer global require "codeception/specify=*"
-   composer global require "codeception/verify=*"
-   ```
-
-   If you've never used Composer for global packages run `composer global status`. It should output:
-
-   ```
-   Changed current directory to <directory>
-   ```
-
-  Then add `<directory>/vendor/bin` to you `PATH` environment variable. Now we're able to use `codecept` from command
-  line globally.
-
-2. Install faker extension by running the following from template root directory where `composer.json` is:
-
-   ```
-   composer require --dev "yiisoft/yii2-faker:*"
-   ```
-
-3. Create `yii2_basic_tests` database and update it by applying migrations (you may skip this step if you do not have created any migrations yet):
-
-   ```
-   codeception/bin/yii migrate
-   ```
-
-   The command needs to be run in the `tests` directory.
-   The database configuration can be found at `tests/codeception/config/config.php`.
-
-4. Build the test suites:
-
-   ```
-   codecept build
-   ```
-
-5. In order to be able to run acceptance tests you need to start a webserver. The simplest way is to use PHP built in
-webserver. In the `web` directory execute the following:
-
-   ```
-   php -S localhost:8080
-   ```
-
-6. Now you can run the tests with the following commands:
-
-   ```
-   # run all available tests
-   codecept run
-   # run acceptance tests
-   codecept run acceptance
-   # run functional tests
-   codecept run functional
-   # run unit tests
-   codecept run unit
-   ```
-
-Fixtures Default Configuration
-------------------------------
-The `fixture` commands refer to the following `ActiveFixture` configuration by default:
-
-- Fixtures path: `@tests/unit/fixtures`
-- Fixtures data path: `@tests/unit/fixtures/data`
-- Template files path: `@tests/unit/templates/fixtures`
-- Namespace: `tests\unit\fixtures`
-
-Where `@tests` refers to `@app/tests/codeception`.
-
-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
-codecept run --coverage-html --coverage-xml
-
-#collect coverage only for unit tests
-codecept run unit --coverage-html --coverage-xml
-
-#collect coverage for unit and functional tests
-codecept run functional,unit --coverage-html --coverage-xml
-```
-
-You can see code coverage output under the `tests/_output` directory.
-
-###Remote code coverage
-
-When you run your tests not in the same process where code coverage is collected, then you should uncomment `remote` option and its
-related options, to be able to collect code coverage correctly. To setup remote code coverage you should follow [instructions](http://codeception.com/docs/11-Codecoverage)
-from codeception site.
-
-1. install `Codeception c3` remote support `composer require "codeception/c3:*"`;
-
-2. copy `c3.php` file under your `web` directory;
-
-3. include `c3.php` file in your `index-test.php` file before application run, so it can catch needed requests.
-
-4. edit `c3.php` to update config file path (~ line 55) with `$config_file = realpath(__DIR__ . '/../tests/codeception.yml');`
-
-Configuration options that are used by remote code coverage:
-
-- c3_url: url pointing to entry script that includes `c3.php` file, so `Codeception` will be able to produce code coverage;
-- remote: whether to enable remote code coverage or not;
-- remote_config: path to the `codeception.yml` configuration file, from the directory where `c3.php` file is located. This is needed
-  so that `Codeception` can create itself instance and collect code coverage correctly.
-
-By default `c3_url` and `remote_config` setup correctly, you only need to copy and include `c3.php` file in your `index-test.php`
-
-After that you should be able to collect code coverage from tests that run through `PhpBrowser` or `WebDriver` with same command
-as for other tests:
-
-```
-#collect coverage from remote
-codecept run acceptance --coverage-html --coverage-xml
-```
-
-Please refer to [Codeception tutorial](http://codeception.com/docs/01-Introduction) for
-more details about writing and running acceptance, functional and unit tests.
diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php
new file mode 100644
index 000000000..131da42af
--- /dev/null
+++ b/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';
\ No newline at end of file
diff --git a/tests/_data/.gitkeep b/tests/_data/.gitkeep
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/tests/_data/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/tests/codeception/_output/.gitignore b/tests/_output/.gitignore
similarity index 100%
rename from tests/codeception/_output/.gitignore
rename to tests/_output/.gitignore
diff --git a/tests/_support/AcceptanceTester.php b/tests/_support/AcceptanceTester.php
new file mode 100644
index 000000000..4c7dcbb6d
--- /dev/null
+++ b/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
+    */
+}
diff --git a/tests/_support/FunctionalTester.php b/tests/_support/FunctionalTester.php
new file mode 100644
index 000000000..db2ce28be
--- /dev/null
+++ b/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;
+
+}
diff --git a/tests/_support/UnitTester.php b/tests/_support/UnitTester.php
new file mode 100644
index 000000000..28353572c
--- /dev/null
+++ b/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
+    */
+}
diff --git a/tests/acceptance.suite.yml.example b/tests/acceptance.suite.yml.example
new file mode 100644
index 000000000..3821d4fe4
--- /dev/null
+++ b/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
diff --git a/tests/acceptance/AboutCest.php b/tests/acceptance/AboutCest.php
new file mode 100644
index 000000000..8a7be5af1
--- /dev/null
+++ b/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');
+    }
+}
diff --git a/tests/acceptance/ContactCest.php b/tests/acceptance/ContactCest.php
new file mode 100644
index 000000000..90f98482e
--- /dev/null
+++ b/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.');
+    }
+}
diff --git a/tests/acceptance/HomeCest.php b/tests/acceptance/HomeCest.php
new file mode 100644
index 000000000..e65df16ae
--- /dev/null
+++ b/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.');
+    }
+}
diff --git a/tests/acceptance/LoginCest.php b/tests/acceptance/LoginCest.php
new file mode 100644
index 000000000..6f5cb2f38
--- /dev/null
+++ b/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');
+    }
+}
diff --git a/tests/acceptance/_bootstrap.php b/tests/acceptance/_bootstrap.php
new file mode 100644
index 000000000..b3d9bbc7f
--- /dev/null
+++ b/tests/acceptance/_bootstrap.php
@@ -0,0 +1 @@
+<?php
diff --git a/tests/bin/yii b/tests/bin/yii
new file mode 100755
index 000000000..39df4013d
--- /dev/null
+++ b/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);
diff --git a/tests/codeception/bin/yii.bat b/tests/bin/yii.bat
similarity index 82%
rename from tests/codeception/bin/yii.bat
rename to tests/bin/yii.bat
index d516b3a19..ce14c92bc 100644
--- a/tests/codeception/bin/yii.bat
+++ b/tests/bin/yii.bat
@@ -4,9 +4,9 @@ rem -------------------------------------------------------------
 rem  Yii command line bootstrap script for Windows.
 rem
 rem  @author Qiang Xue <qiang.xue@gmail.com>
-rem  @link http://www.yiiframework.com/
+rem  @link https://www.yiiframework.com/
 rem  @copyright Copyright (c) 2008 Yii Software LLC
-rem  @license http://www.yiiframework.com/license/
+rem  @license https://www.yiiframework.com/license/
 rem -------------------------------------------------------------
 
 @setlocal
diff --git a/tests/codeception.yml b/tests/codeception.yml
deleted file mode 100644
index b71ba31c1..000000000
--- a/tests/codeception.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-actor: Tester
-#coverage:
-#    #c3_url: http://localhost:8080/index-test.php/
-#    enabled: true
-#    #remote: true
-#    #remote_config: '../tests/codeception.yml'
-#    white_list:
-#        include:
-#            - ../models/*
-#            - ../controllers/*
-#            - ../commands/*
-#            - ../mail/*
-#    blacklist:
-#        include:
-#            - ../assets/*
-#            - ../config/*
-#            - ../runtime/*
-#            - ../vendor/*
-#            - ../views/*
-#            - ../web/*
-#            - ../tests/*
-paths:
-    tests: codeception
-    log: codeception/_output
-    data: codeception/_data
-    helpers: codeception/_support
-settings:
-    bootstrap: _bootstrap.php
-    suite_class: \PHPUnit_Framework_TestSuite
-    memory_limit: 1024M
-    log: true
-    colors: true
-config:
-    # the entry script URL (with host info) for functional and acceptance tests
-    # PLEASE ADJUST IT TO THE ACTUAL ENTRY SCRIPT URL
-    test_entry_url: http://localhost:8080/index-test.php
\ No newline at end of file
diff --git a/tests/codeception/.gitignore b/tests/codeception/.gitignore
deleted file mode 100644
index 985dbb423..000000000
--- a/tests/codeception/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-# these files are auto generated by codeception build
-/unit/UnitTester.php
-/functional/FunctionalTester.php
-/acceptance/AcceptanceTester.php
diff --git a/tests/codeception/_bootstrap.php b/tests/codeception/_bootstrap.php
deleted file mode 100644
index 755029ef3..000000000
--- a/tests/codeception/_bootstrap.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-defined('YII_DEBUG') or define('YII_DEBUG', true);
-defined('YII_ENV') or define('YII_ENV', 'test');
-
-defined('YII_TEST_ENTRY_URL') or define('YII_TEST_ENTRY_URL', parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PATH));
-defined('YII_TEST_ENTRY_FILE') or define('YII_TEST_ENTRY_FILE', dirname(dirname(__DIR__)) . '/web/index-test.php');
-
-require_once(__DIR__ . '/../../vendor/autoload.php');
-require_once(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
-
-$_SERVER['SCRIPT_FILENAME'] = YII_TEST_ENTRY_FILE;
-$_SERVER['SCRIPT_NAME'] = YII_TEST_ENTRY_URL;
-$_SERVER['SERVER_NAME'] = parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_HOST);
-$_SERVER['SERVER_PORT'] =  parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PORT) ?: '80';
-
-Yii::setAlias('@tests', dirname(__DIR__));
diff --git a/tests/codeception/_pages/AboutPage.php b/tests/codeception/_pages/AboutPage.php
deleted file mode 100644
index b56320d09..000000000
--- a/tests/codeception/_pages/AboutPage.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace tests\codeception\_pages;
-
-use yii\codeception\BasePage;
-
-/**
- * Represents about page
- * @property \AcceptanceTester|\FunctionalTester $actor
- */
-class AboutPage extends BasePage
-{
-    public $route = 'site/about';
-}
diff --git a/tests/codeception/_pages/ContactPage.php b/tests/codeception/_pages/ContactPage.php
deleted file mode 100644
index 5701e2cb4..000000000
--- a/tests/codeception/_pages/ContactPage.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-namespace tests\codeception\_pages;
-
-use yii\codeception\BasePage;
-
-/**
- * Represents contact page
- * @property \AcceptanceTester|\FunctionalTester $actor
- */
-class ContactPage extends BasePage
-{
-    public $route = 'site/contact';
-
-    /**
-     * @param array $contactData
-     */
-    public function submit(array $contactData)
-    {
-        foreach ($contactData as $field => $value) {
-            $inputType = $field === 'body' ? 'textarea' : 'input';
-            $this->actor->fillField($inputType . '[name="ContactForm[' . $field . ']"]', $value);
-        }
-        $this->actor->click('contact-button');
-    }
-}
diff --git a/tests/codeception/_pages/LoginPage.php b/tests/codeception/_pages/LoginPage.php
deleted file mode 100644
index c3a2ef2dd..000000000
--- a/tests/codeception/_pages/LoginPage.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-namespace tests\codeception\_pages;
-
-use yii\codeception\BasePage;
-
-/**
- * Represents login page
- * @property \AcceptanceTester|\FunctionalTester $actor
- */
-class LoginPage extends BasePage
-{
-    public $route = 'site/login';
-
-    /**
-     * @param string $username
-     * @param string $password
-     */
-    public function login($username, $password)
-    {
-        $this->actor->fillField('input[name="LoginForm[username]"]', $username);
-        $this->actor->fillField('input[name="LoginForm[password]"]', $password);
-        $this->actor->click('login-button');
-    }
-}
diff --git a/tests/codeception/acceptance.suite.yml b/tests/codeception/acceptance.suite.yml
deleted file mode 100644
index 1781b0077..000000000
--- a/tests/codeception/acceptance.suite.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-# Codeception Test Suite Configuration
-
-# suite for acceptance tests.
-# perform tests in browser using the Selenium-like tools.
-# powered by Mink (http://mink.behat.org).
-# (tip: that's what your customer will see).
-# (tip: test your ajax and javascript by one of Mink drivers).
-
-# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
-
-class_name: AcceptanceTester
-modules:
-    enabled:
-        - PhpBrowser
-# you can use WebDriver instead of PhpBrowser to test javascript and ajax.
-# This will require you to install selenium. See http://codeception.com/docs/04-AcceptanceTests#Selenium
-# "restart" option is used by the WebDriver to start each time per test-file new session and cookies,
-# it is useful if you want to login in your app in each test.
-#        - WebDriver
-    config:
-        PhpBrowser:
-# PLEASE ADJUST IT TO THE ACTUAL ENTRY POINT WITHOUT PATH INFO
-            url: http://localhost:8080
-#        WebDriver:
-#            url: http://localhost:8080
-#            browser: firefox
-#            restart: true
diff --git a/tests/codeception/acceptance/AboutCept.php b/tests/codeception/acceptance/AboutCept.php
deleted file mode 100644
index 88a550698..000000000
--- a/tests/codeception/acceptance/AboutCept.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-use tests\codeception\_pages\AboutPage;
-
-/* @var $scenario Codeception\Scenario */
-
-$I = new AcceptanceTester($scenario);
-$I->wantTo('ensure that about works');
-AboutPage::openBy($I);
-$I->see('About', 'h1');
diff --git a/tests/codeception/acceptance/ContactCept.php b/tests/codeception/acceptance/ContactCept.php
deleted file mode 100644
index f848443da..000000000
--- a/tests/codeception/acceptance/ContactCept.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-use tests\codeception\_pages\ContactPage;
-
-/* @var $scenario Codeception\Scenario */
-
-$I = new AcceptanceTester($scenario);
-$I->wantTo('ensure that contact works');
-
-$contactPage = ContactPage::openBy($I);
-
-$I->see('Contact', 'h1');
-
-$I->amGoingTo('submit contact form with no data');
-$contactPage->submit([]);
-if (method_exists($I, 'wait')) {
-    $I->wait(3); // only for selenium
-}
-$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');
-
-$I->amGoingTo('submit contact form with not correct email');
-$contactPage->submit([
-    'name' => 'tester',
-    'email' => 'tester.email',
-    'subject' => 'test subject',
-    'body' => 'test content',
-    'verifyCode' => 'testme',
-]);
-if (method_exists($I, 'wait')) {
-    $I->wait(3); // only for selenium
-}
-$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');
-
-$I->amGoingTo('submit contact form with correct data');
-$contactPage->submit([
-    'name' => 'tester',
-    'email' => 'tester@example.com',
-    'subject' => 'test subject',
-    'body' => 'test content',
-    'verifyCode' => 'testme',
-]);
-if (method_exists($I, 'wait')) {
-    $I->wait(3); // only for selenium
-}
-$I->dontSeeElement('#contact-form');
-$I->see('Thank you for contacting us. We will respond to you as soon as possible.');
diff --git a/tests/codeception/acceptance/HomeCept.php b/tests/codeception/acceptance/HomeCept.php
deleted file mode 100644
index 1f9353595..000000000
--- a/tests/codeception/acceptance/HomeCept.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-/* @var $scenario Codeception\Scenario */
-
-$I = new AcceptanceTester($scenario);
-$I->wantTo('ensure that home page works');
-$I->amOnPage(Yii::$app->homeUrl);
-$I->see('My Company');
-$I->seeLink('About');
-$I->click('About');
-$I->see('This is the About page.');
diff --git a/tests/codeception/acceptance/LoginCept.php b/tests/codeception/acceptance/LoginCept.php
deleted file mode 100644
index 90d063583..000000000
--- a/tests/codeception/acceptance/LoginCept.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-use tests\codeception\_pages\LoginPage;
-
-/* @var $scenario Codeception\Scenario */
-
-$I = new AcceptanceTester($scenario);
-$I->wantTo('ensure that login works');
-
-$loginPage = LoginPage::openBy($I);
-
-$I->see('Login', 'h1');
-
-$I->amGoingTo('try to login with empty credentials');
-$loginPage->login('', '');
-if (method_exists($I, 'wait')) {
-    $I->wait(3); // only for selenium
-}
-$I->expectTo('see validations errors');
-$I->see('Username cannot be blank.');
-$I->see('Password cannot be blank.');
-
-$I->amGoingTo('try to login with wrong credentials');
-$loginPage->login('admin', 'wrong');
-if (method_exists($I, 'wait')) {
-    $I->wait(3); // only for selenium
-}
-$I->expectTo('see validations errors');
-$I->see('Incorrect username or password.');
-
-$I->amGoingTo('try to login with correct credentials');
-$loginPage->login('admin', 'admin');
-if (method_exists($I, 'wait')) {
-    $I->wait(3); // only for selenium
-}
-$I->expectTo('see user info');
-$I->see('Logout (admin)');
diff --git a/tests/codeception/acceptance/_bootstrap.php b/tests/codeception/acceptance/_bootstrap.php
deleted file mode 100644
index 36f9f1d11..000000000
--- a/tests/codeception/acceptance/_bootstrap.php
+++ /dev/null
@@ -1,2 +0,0 @@
-<?php
-new yii\web\Application(require(dirname(__DIR__) . '/config/acceptance.php'));
diff --git a/tests/codeception/bin/_bootstrap.php b/tests/codeception/bin/_bootstrap.php
deleted file mode 100644
index bbadc36ef..000000000
--- a/tests/codeception/bin/_bootstrap.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-defined('YII_DEBUG') or define('YII_DEBUG', true);
-defined('YII_ENV') or define('YII_ENV', 'test');
-
-defined('YII_APP_BASE_PATH') or define('YII_APP_BASE_PATH', dirname(dirname(dirname(__DIR__))));
-
-require(YII_APP_BASE_PATH . '/vendor/autoload.php');
-require(YII_APP_BASE_PATH . '/vendor/yiisoft/yii2/Yii.php');
-
-Yii::setAlias('@tests', dirname(dirname(__DIR__)));
diff --git a/tests/codeception/bin/yii b/tests/codeception/bin/yii
deleted file mode 100644
index c6df160d8..000000000
--- a/tests/codeception/bin/yii
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env php
-<?php
-/**
- * Yii console bootstrap file.
- *
- * @link http://www.yiiframework.com/
- * @copyright Copyright (c) 2008 Yii Software LLC
- * @license http://www.yiiframework.com/license/
- */
-
-require_once __DIR__ . '/_bootstrap.php';
-
-$config = yii\helpers\ArrayHelper::merge(
-    require(YII_APP_BASE_PATH . '/config/console.php'),
-    require(__DIR__ . '/../config/config.php')
-);
-
-$application = new yii\console\Application($config);
-$exitCode = $application->run();
-exit($exitCode);
diff --git a/tests/codeception/config/acceptance.php b/tests/codeception/config/acceptance.php
deleted file mode 100644
index c688575b6..000000000
--- a/tests/codeception/config/acceptance.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-/**
- * Application configuration for acceptance tests
- */
-return yii\helpers\ArrayHelper::merge(
-    require(__DIR__ . '/../../../config/web.php'),
-    require(__DIR__ . '/config.php'),
-    [
-
-    ]
-);
diff --git a/tests/codeception/config/config.php b/tests/codeception/config/config.php
deleted file mode 100644
index d4e1f77ce..000000000
--- a/tests/codeception/config/config.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-/**
- * Application configuration shared by all test types
- */
-return [
-    'language' => 'en-US',
-    'controllerMap' => [
-        'fixture' => [
-            'class' => 'yii\faker\FixtureController',
-            'fixtureDataPath' => '@tests/codeception/fixtures',
-            'templatePath' => '@tests/codeception/templates',
-            'namespace' => 'tests\codeception\fixtures',
-        ],
-    ],
-    'components' => [
-        'db' => [
-            'dsn' => 'mysql:host=localhost;dbname=yii2_basic_tests',
-        ],
-        'mailer' => [
-            'useFileTransport' => true,
-        ],
-        'urlManager' => [
-            'showScriptName' => true,
-        ],
-    ],
-];
diff --git a/tests/codeception/config/functional.php b/tests/codeception/config/functional.php
deleted file mode 100644
index 6d22bd97b..000000000
--- a/tests/codeception/config/functional.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-$_SERVER['SCRIPT_FILENAME'] = YII_TEST_ENTRY_FILE;
-$_SERVER['SCRIPT_NAME'] = YII_TEST_ENTRY_URL;
-
-/**
- * Application configuration for functional tests
- */
-return yii\helpers\ArrayHelper::merge(
-    require(__DIR__ . '/../../../config/web.php'),
-    require(__DIR__ . '/config.php'),
-    [
-        'components' => [
-            'request' => [
-                // it's not recommended to run functional tests with CSRF validation enabled
-                'enableCsrfValidation' => false,
-                // but if you absolutely need it set cookie domain to localhost
-                /*
-                'csrfCookie' => [
-                    'domain' => 'localhost',
-                ],
-                */
-            ],
-        ],
-    ]
-);
diff --git a/tests/codeception/config/unit.php b/tests/codeception/config/unit.php
deleted file mode 100644
index 5bab5eaec..000000000
--- a/tests/codeception/config/unit.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-/**
- * Application configuration for unit tests
- */
-return yii\helpers\ArrayHelper::merge(
-    require(__DIR__ . '/../../../config/web.php'),
-    require(__DIR__ . '/config.php'),
-    [
-
-    ]
-);
diff --git a/tests/codeception/fixtures/.gitignore b/tests/codeception/fixtures/.gitignore
deleted file mode 100644
index c96a04f00..000000000
--- a/tests/codeception/fixtures/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
\ No newline at end of file
diff --git a/tests/codeception/functional/AboutCept.php b/tests/codeception/functional/AboutCept.php
deleted file mode 100644
index 09260612f..000000000
--- a/tests/codeception/functional/AboutCept.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-use tests\codeception\_pages\AboutPage;
-
-/* @var $scenario Codeception\Scenario */
-
-$I = new FunctionalTester($scenario);
-$I->wantTo('ensure that about works');
-AboutPage::openBy($I);
-$I->see('About', 'h1');
diff --git a/tests/codeception/functional/ContactCept.php b/tests/codeception/functional/ContactCept.php
deleted file mode 100644
index b17a7576d..000000000
--- a/tests/codeception/functional/ContactCept.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-use tests\codeception\_pages\ContactPage;
-
-/* @var $scenario Codeception\Scenario */
-
-$I = new FunctionalTester($scenario);
-$I->wantTo('ensure that contact works');
-
-$contactPage = ContactPage::openBy($I);
-
-$I->see('Contact', 'h1');
-
-$I->amGoingTo('submit contact form with no data');
-$contactPage->submit([]);
-$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');
-
-$I->amGoingTo('submit contact form with not correct email');
-$contactPage->submit([
-    'name'			=>	'tester',
-    'email'			=>	'tester.email',
-    'subject'		=>	'test subject',
-    'body'			=>	'test content',
-    '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');
-
-$I->amGoingTo('submit contact form with correct data');
-$contactPage->submit([
-    'name'			=>	'tester',
-    'email'			=>	'tester@example.com',
-    'subject'		=>	'test subject',
-    'body'			=>	'test content',
-    'verifyCode'	=>	'testme',
-]);
-$I->dontSeeElement('#contact-form');
-$I->see('Thank you for contacting us. We will respond to you as soon as possible.');
diff --git a/tests/codeception/functional/HomeCept.php b/tests/codeception/functional/HomeCept.php
deleted file mode 100644
index 94efb09cd..000000000
--- a/tests/codeception/functional/HomeCept.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-/* @var $scenario Codeception\Scenario */
-
-$I = new FunctionalTester($scenario);
-$I->wantTo('ensure that home page works');
-$I->amOnPage(Yii::$app->homeUrl);
-$I->see('My Company');
-$I->seeLink('About');
-$I->click('About');
-$I->see('This is the About page.');
diff --git a/tests/codeception/functional/LoginCept.php b/tests/codeception/functional/LoginCept.php
deleted file mode 100644
index 66e760989..000000000
--- a/tests/codeception/functional/LoginCept.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-use tests\codeception\_pages\LoginPage;
-
-/* @var $scenario Codeception\Scenario */
-
-$I = new FunctionalTester($scenario);
-$I->wantTo('ensure that login works');
-
-$loginPage = LoginPage::openBy($I);
-
-$I->see('Login', 'h1');
-
-$I->amGoingTo('try to login with empty credentials');
-$loginPage->login('', '');
-$I->expectTo('see validations errors');
-$I->see('Username cannot be blank.');
-$I->see('Password cannot be blank.');
-
-$I->amGoingTo('try to login with wrong credentials');
-$loginPage->login('admin', 'wrong');
-$I->expectTo('see validations errors');
-$I->see('Incorrect username or password.');
-
-$I->amGoingTo('try to login with correct credentials');
-$loginPage->login('admin', 'admin');
-$I->expectTo('see user info');
-$I->see('Logout (admin)');
diff --git a/tests/codeception/functional/_bootstrap.php b/tests/codeception/functional/_bootstrap.php
deleted file mode 100644
index 8aac0915b..000000000
--- a/tests/codeception/functional/_bootstrap.php
+++ /dev/null
@@ -1,2 +0,0 @@
-<?php
-new yii\web\Application(require(dirname(__DIR__) . '/config/functional.php'));
diff --git a/tests/codeception/templates/.gitignore b/tests/codeception/templates/.gitignore
deleted file mode 100644
index c96a04f00..000000000
--- a/tests/codeception/templates/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
\ No newline at end of file
diff --git a/tests/codeception/unit/fixtures/.gitkeep b/tests/codeception/unit/fixtures/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/codeception/unit/fixtures/data/.gitkeep b/tests/codeception/unit/fixtures/data/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/codeception/unit/models/ContactFormTest.php b/tests/codeception/unit/models/ContactFormTest.php
deleted file mode 100644
index 323482943..000000000
--- a/tests/codeception/unit/models/ContactFormTest.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-namespace tests\codeception\unit\models;
-
-use Yii;
-use yii\codeception\TestCase;
-use Codeception\Specify;
-
-class ContactFormTest extends TestCase
-{
-    use Specify;
-
-    protected function setUp()
-    {
-        parent::setUp();
-        Yii::$app->mailer->fileTransportCallback = function ($mailer, $message) {
-            return 'testing_message.eml';
-        };
-    }
-
-    protected function tearDown()
-    {
-        unlink($this->getMessageFile());
-        parent::tearDown();
-    }
-
-    public function testContact()
-    {
-        $model = $this->getMock('app\models\ContactForm', ['validate']);
-        $model->expects($this->once())->method('validate')->will($this->returnValue(true));
-
-        $model->attributes = [
-            'name' => 'Tester',
-            'email' => 'tester@example.com',
-            'subject' => 'very important letter subject',
-            'body' => 'body of current message',
-        ];
-
-        $model->contact('admin@example.com');
-
-        $this->specify('email should be send', function () {
-            expect('email file should exist', file_exists($this->getMessageFile()))->true();
-        });
-
-        $this->specify('message should contain correct data', function () use ($model) {
-            $emailMessage = file_get_contents($this->getMessageFile());
-
-            expect('email should contain user name', $emailMessage)->contains($model->name);
-            expect('email should contain sender email', $emailMessage)->contains($model->email);
-            expect('email should contain subject', $emailMessage)->contains($model->subject);
-            expect('email should contain body', $emailMessage)->contains($model->body);
-        });
-    }
-
-    private function getMessageFile()
-    {
-        return Yii::getAlias(Yii::$app->mailer->fileTransportPath) . '/testing_message.eml';
-    }
-
-}
diff --git a/tests/codeception/unit/models/LoginFormTest.php b/tests/codeception/unit/models/LoginFormTest.php
deleted file mode 100644
index c7f971a06..000000000
--- a/tests/codeception/unit/models/LoginFormTest.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-namespace tests\codeception\unit\models;
-
-use Yii;
-use yii\codeception\TestCase;
-use app\models\LoginForm;
-use Codeception\Specify;
-
-class LoginFormTest extends TestCase
-{
-    use Specify;
-
-    protected function tearDown()
-    {
-        Yii::$app->user->logout();
-        parent::tearDown();
-    }
-
-    public function testLoginNoUser()
-    {
-        $model = new LoginForm([
-            'username' => 'not_existing_username',
-            'password' => 'not_existing_password',
-        ]);
-
-        $this->specify('user should not be able to login, when there is no identity', function () use ($model) {
-            expect('model should not login user', $model->login())->false();
-            expect('user should not be logged in', Yii::$app->user->isGuest)->true();
-        });
-    }
-
-    public function testLoginWrongPassword()
-    {
-        $model = new LoginForm([
-            'username' => 'demo',
-            'password' => 'wrong_password',
-        ]);
-
-        $this->specify('user should not be able to login with wrong password', function () use ($model) {
-            expect('model should not login user', $model->login())->false();
-            expect('error message should be set', $model->errors)->hasKey('password');
-            expect('user should not be logged in', Yii::$app->user->isGuest)->true();
-        });
-    }
-
-    public function testLoginCorrect()
-    {
-        $model = new LoginForm([
-            'username' => 'demo',
-            'password' => 'demo',
-        ]);
-
-        $this->specify('user should be able to login with correct credentials', function () use ($model) {
-            expect('model should login user', $model->login())->true();
-            expect('error message should not be set', $model->errors)->hasntKey('password');
-            expect('user should be logged in', Yii::$app->user->isGuest)->false();
-        });
-    }
-
-}
diff --git a/tests/codeception/unit/models/UserTest.php b/tests/codeception/unit/models/UserTest.php
deleted file mode 100644
index f4f4f4bb9..000000000
--- a/tests/codeception/unit/models/UserTest.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace tests\codeception\unit\models;
-
-use yii\codeception\TestCase;
-
-class UserTest extends TestCase
-{
-    protected function setUp()
-    {
-        parent::setUp();
-        // uncomment the following to load fixtures for user table
-        //$this->loadFixtures(['user']);
-    }
-
-    // TODO add test methods here
-}
diff --git a/tests/codeception/unit/templates/fixtures/.gitkeep b/tests/codeception/unit/templates/fixtures/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/tests/codeception/functional.suite.yml b/tests/functional.suite.yml
similarity index 73%
rename from tests/codeception/functional.suite.yml
rename to tests/functional.suite.yml
index ad7164cb6..9d8cf149c 100644
--- a/tests/codeception/functional.suite.yml
+++ b/tests/functional.suite.yml
@@ -6,11 +6,9 @@
 
 # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
 #basic/web/index.php
-class_name: FunctionalTester
+actor: FunctionalTester
 modules:
     enabled:
       - Filesystem
       - Yii2
-    config:
-        Yii2:
-            configFile: 'codeception/config/functional.php'
+      - Asserts
diff --git a/tests/functional/ContactFormCest.php b/tests/functional/ContactFormCest.php
new file mode 100644
index 000000000..d17ef52fd
--- /dev/null
+++ b/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.');        
+    }
+}
diff --git a/tests/functional/LoginFormCest.php b/tests/functional/LoginFormCest.php
new file mode 100644
index 000000000..7a83a27d6
--- /dev/null
+++ b/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');              
+    }
+}
\ No newline at end of file
diff --git a/tests/functional/_bootstrap.php b/tests/functional/_bootstrap.php
new file mode 100644
index 000000000..b3d9bbc7f
--- /dev/null
+++ b/tests/functional/_bootstrap.php
@@ -0,0 +1 @@
+<?php
diff --git a/tests/codeception/unit.suite.yml b/tests/unit.suite.yml
similarity index 53%
rename from tests/codeception/unit.suite.yml
rename to tests/unit.suite.yml
index a0582a536..c14e49c37 100644
--- a/tests/codeception/unit.suite.yml
+++ b/tests/unit.suite.yml
@@ -3,4 +3,9 @@
 # suite for unit (internal) tests.
 # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
 
-class_name: UnitTester
+actor: UnitTester
+modules:
+    enabled:
+      - Asserts
+      - Yii2:
+            part: [orm, email, fixtures]
diff --git a/tests/codeception/unit/_bootstrap.php b/tests/unit/_bootstrap.php
similarity index 100%
rename from tests/codeception/unit/_bootstrap.php
rename to tests/unit/_bootstrap.php
diff --git a/tests/unit/models/ContactFormTest.php b/tests/unit/models/ContactFormTest.php
new file mode 100644
index 000000000..1bb1bb2d0
--- /dev/null
+++ b/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');
+    }
+}
diff --git a/tests/unit/models/LoginFormTest.php b/tests/unit/models/LoginFormTest.php
new file mode 100644
index 000000000..3c1dcddaa
--- /dev/null
+++ b/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');
+    }
+
+}
diff --git a/tests/unit/models/UserTest.php b/tests/unit/models/UserTest.php
new file mode 100644
index 000000000..28986cbbd
--- /dev/null
+++ b/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();        
+    }
+
+}
diff --git a/tests/unit/widgets/AlertTest.php b/tests/unit/widgets/AlertTest.php
new file mode 100644
index 000000000..e9857a161
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/vagrant/config/.gitignore b/vagrant/config/.gitignore
new file mode 100644
index 000000000..0685a56ca
--- /dev/null
+++ b/vagrant/config/.gitignore
@@ -0,0 +1,2 @@
+# local configuration
+vagrant-local.yml
\ No newline at end of file
diff --git a/vagrant/config/vagrant-local.example.yml b/vagrant/config/vagrant-local.example.yml
new file mode 100644
index 000000000..919aec0e8
--- /dev/null
+++ b/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
diff --git a/vagrant/nginx/app.conf b/vagrant/nginx/app.conf
new file mode 100644
index 000000000..1bfc0d583
--- /dev/null
+++ b/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;
+   }
+}
diff --git a/vagrant/nginx/log/.gitignore b/vagrant/nginx/log/.gitignore
new file mode 100644
index 000000000..c0fbfdd8d
--- /dev/null
+++ b/vagrant/nginx/log/.gitignore
@@ -0,0 +1,3 @@
+#nginx logs
+yii2basic.access.log
+yii2basic.error.log
\ No newline at end of file
diff --git a/vagrant/provision/always-as-root.sh b/vagrant/provision/always-as-root.sh
new file mode 100644
index 000000000..17fb58421
--- /dev/null
+++ b/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
diff --git a/vagrant/provision/once-as-root.sh b/vagrant/provision/once-as-root.sh
new file mode 100644
index 000000000..1d2917016
--- /dev/null
+++ b/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
diff --git a/vagrant/provision/once-as-vagrant.sh b/vagrant/provision/once-as-vagrant.sh
new file mode 100644
index 000000000..8da47217c
--- /dev/null
+++ b/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
diff --git a/vagrant/provision/provision.awk b/vagrant/provision/provision.awk
new file mode 100644
index 000000000..bcf44d3be
--- /dev/null
+++ b/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."
+}
diff --git a/views/layouts/main.php b/views/layouts/main.php
index 2974d6d89..c204435c0 100644
--- a/views/layouts/main.php
+++ b/views/layouts/main.php
@@ -1,74 +1,79 @@
 <?php
 
-/* @var $this \yii\web\View */
-/* @var $content string */
+/** @var yii\web\View $this */
+/** @var string $content */
 
-use yii\helpers\Html;
-use yii\bootstrap\Nav;
-use yii\bootstrap\NavBar;
-use yii\widgets\Breadcrumbs;
 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 ?>">
+<html lang="<?= Yii::$app->language ?>" class="h-100">
 <head>
-    <meta charset="<?= Yii::$app->charset ?>">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <?= Html::csrfMetaTags() ?>
     <title><?= Html::encode($this->title) ?></title>
     <?php $this->head() ?>
 </head>
-<body>
+<body class="d-flex flex-column h-100">
 <?php $this->beginBody() ?>
 
-<div class="wrap">
+<header id="header">
     <?php
     NavBar::begin([
-        'brandLabel' => 'My Company',
+        'brandLabel' => Yii::$app->name,
         'brandUrl' => Yii::$app->homeUrl,
-        'options' => [
-            'class' => 'navbar-inverse navbar-fixed-top',
-        ],
+        'options' => ['class' => 'navbar-expand-md navbar-dark bg-dark fixed-top']
     ]);
     echo Nav::widget([
-        'options' => ['class' => 'navbar-nav navbar-right'],
+        '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>'
-                . Html::beginForm(['/site/logout'], 'post', ['class' => 'navbar-form'])
-                . Html::submitButton(
-                    'Logout (' . Yii::$app->user->identity->username . ')',
-                    ['class' => 'btn btn-link']
-                )
-                . Html::endForm()
-                . '</li>'
-            )
-        ],
+            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">
-        <?= Breadcrumbs::widget([
-            'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
-        ]) ?>
+        <?php if (!empty($this->params['breadcrumbs'])): ?>
+            <?= Breadcrumbs::widget(['links' => $this->params['breadcrumbs']]) ?>
+        <?php endif ?>
+        <?= Alert::widget() ?>
         <?= $content ?>
     </div>
-</div>
+</main>
 
-<footer class="footer">
+<footer id="footer" class="mt-auto py-3 bg-light">
     <div class="container">
-        <p class="pull-left">&copy; My Company <?= date('Y') ?></p>
-
-        <p class="pull-right"><?= Yii::powered() ?></p>
+        <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>
 
diff --git a/views/site/about.php b/views/site/about.php
index 68d5cf3bd..ea006ecda 100644
--- a/views/site/about.php
+++ b/views/site/about.php
@@ -1,6 +1,6 @@
 <?php
 
-/* @var $this yii\web\View */
+/** @var yii\web\View $this */
 
 use yii\helpers\Html;
 
diff --git a/views/site/contact.php b/views/site/contact.php
index b98840918..597fabcb2 100644
--- a/views/site/contact.php
+++ b/views/site/contact.php
@@ -1,11 +1,11 @@
 <?php
 
-/* @var $this yii\web\View */
-/* @var $form yii\bootstrap\ActiveForm */
-/* @var $model app\models\ContactForm */
+/** @var yii\web\View $this */
+/** @var yii\bootstrap5\ActiveForm $form */
+/** @var app\models\ContactForm $model */
 
-use yii\helpers\Html;
-use yii\bootstrap\ActiveForm;
+use yii\bootstrap5\ActiveForm;
+use yii\bootstrap5\Html;
 use yii\captcha\Captcha;
 
 $this->title = 'Contact';
@@ -49,9 +49,9 @@
 
                     <?= $form->field($model, 'subject') ?>
 
-                    <?= $form->field($model, 'body')->textArea(['rows' => 6]) ?>
+                    <?= $form->field($model, 'body')->textarea(['rows' => 6]) ?>
 
-                    <?= $form->field($model, 'verifyCode')->widget(Captcha::className(), [
+                    <?= $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>',
                     ]) ?>
 
diff --git a/views/site/error.php b/views/site/error.php
index 0ba257465..176874796 100644
--- a/views/site/error.php
+++ b/views/site/error.php
@@ -1,9 +1,9 @@
 <?php
 
-/* @var $this yii\web\View */
-/* @var $name string */
-/* @var $message string */
-/* @var $exception Exception */
+/** @var yii\web\View $this */
+/** @var string $name */
+/** @var string $message */
+/** @var Exception $exception */
 
 use yii\helpers\Html;
 
diff --git a/views/site/index.php b/views/site/index.php
index f78061088..df5df7c3d 100644
--- a/views/site/index.php
+++ b/views/site/index.php
@@ -1,23 +1,23 @@
 <?php
 
-/* @var $this yii\web\View */
+/** @var yii\web\View $this */
 
 $this->title = 'My Yii Application';
 ?>
 <div class="site-index">
 
-    <div class="jumbotron">
-        <h1>Congratulations!</h1>
+    <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="http://www.yiiframework.com">Get started with Yii</a></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">
+            <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
@@ -25,9 +25,9 @@
                     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-default" href="http://www.yiiframework.com/doc/">Yii Documentation &raquo;</a></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">
+            <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
@@ -35,7 +35,7 @@
                     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-default" href="http://www.yiiframework.com/forum/">Yii Forum &raquo;</a></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>
@@ -45,7 +45,7 @@
                     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-default" href="http://www.yiiframework.com/extensions/">Yii Extensions &raquo;</a></p>
+                <p><a class="btn btn-outline-secondary" href="https://www.yiiframework.com/extensions/">Yii Extensions &raquo;</a></p>
             </div>
         </div>
 
diff --git a/views/site/login.php b/views/site/login.php
index 498f46fce..db003c01c 100644
--- a/views/site/login.php
+++ b/views/site/login.php
@@ -1,11 +1,12 @@
 <?php
 
-/* @var $this yii\web\View */
-/* @var $form yii\bootstrap\ActiveForm */
-/* @var $model app\models\LoginForm */
+/** @var yii\web\View $this */
+/** @var yii\bootstrap5\ActiveForm $form */
 
-use yii\helpers\Html;
-use yii\bootstrap\ActiveForm;
+/** @var app\models\LoginForm $model */
+
+use yii\bootstrap5\ActiveForm;
+use yii\bootstrap5\Html;
 
 $this->title = 'Login';
 $this->params['breadcrumbs'][] = $this->title;
@@ -15,33 +16,40 @@
 
     <p>Please fill out the following fields to login:</p>
 
-    <?php $form = ActiveForm::begin([
-        'id' => 'login-form',
-        'options' => ['class' => 'form-horizontal'],
-        'fieldConfig' => [
-            'template' => "{label}\n<div class=\"col-lg-3\">{input}</div>\n<div class=\"col-lg-8\">{error}</div>",
-            'labelOptions' => ['class' => 'col-lg-1 control-label'],
-        ],
-    ]); ?>
+    <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, 'username')->textInput(['autofocus' => true]) ?>
 
-        <?= $form->field($model, 'password')->passwordInput() ?>
+            <?= $form->field($model, 'password')->passwordInput() ?>
 
-        <?= $form->field($model, 'rememberMe')->checkbox([
-            'template' => "<div class=\"col-lg-offset-1 col-lg-3\">{input} {label}</div>\n<div class=\"col-lg-8\">{error}</div>",
-        ]) ?>
+            <?= $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 class="col-lg-offset-1 col-lg-11">
-                <?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
+            <div class="form-group">
+                <div>
+                    <?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
+                </div>
             </div>
-        </div>
 
-    <?php ActiveForm::end(); ?>
+            <?php ActiveForm::end(); ?>
 
-    <div class="col-lg-offset-1" 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 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>
diff --git a/web/.htaccess b/web/.htaccess
new file mode 100644
index 000000000..197199e8d
--- /dev/null
+++ b/web/.htaccess
@@ -0,0 +1,4 @@
+RewriteEngine on
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule . index.php [L]
diff --git a/web/css/site.css b/web/css/site.css
index 698be709c..fb300b39e 100644
--- a/web/css/site.css
+++ b/web/css/site.css
@@ -1,34 +1,16 @@
-html,
-body {
-    height: 100%;
-}
-
-.wrap {
-    min-height: 100%;
-    height: auto;
-    margin: 0 auto -60px;
-    padding: 0 0 60px;
-}
-
-.wrap > .container {
+main > .container {
     padding: 70px 15px 20px;
 }
 
 .footer {
-    height: 60px;
     background-color: #f5f5f5;
-    border-top: 1px solid #ddd;
-    padding-top: 20px;
-}
-
-.jumbotron {
-    text-align: center;
-    background-color: transparent;
+    font-size: .9em;
+    height: 60px;
 }
 
-.jumbotron .btn {
-    font-size: 21px;
-    padding: 14px 24px;
+.footer > .container {
+    padding-right: 15px;
+    padding-left: 15px;
 }
 
 .not-set {
@@ -38,38 +20,24 @@ body {
 
 /* add sorting icons to gridview sort links */
 a.asc:after, a.desc:after {
-    position: relative;
-    top: 1px;
+    content: '';
+    left: 3px;
     display: inline-block;
-    font-family: 'Glyphicons Halflings';
-    font-style: normal;
-    font-weight: normal;
-    line-height: 1;
-    padding-left: 5px;
+    width: 0;
+    height: 0;
+    border: solid 5px transparent;
+    margin: 4px 4px 2px 4px;
+    background: transparent;
 }
 
 a.asc:after {
-    content: /*"\e113"*/ "\e151";
+    border-bottom: solid 7px #212529;
+    border-top-width: 0;
 }
 
 a.desc:after {
-    content: /*"\e114"*/ "\e152";
-}
-
-.sort-numerical a.asc:after {
-    content: "\e153";
-}
-
-.sort-numerical a.desc:after {
-    content: "\e154";
-}
-
-.sort-ordinal a.asc:after {
-    content: "\e155";
-}
-
-.sort-ordinal a.desc:after {
-    content: "\e156";
+    border-top: solid 7px #212529;
+    border-bottom-width: 0;
 }
 
 .grid-view th {
@@ -89,3 +57,32 @@ a.desc:after {
     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;
+}
\ No newline at end of file
diff --git a/web/index-test.php b/web/index-test.php
index 32b4ce3e1..1ec192f22 100644
--- a/web/index-test.php
+++ b/web/index-test.php
@@ -8,9 +8,9 @@
 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');
+require __DIR__ . '/../vendor/autoload.php';
+require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
 
-$config = require(__DIR__ . '/../tests/codeception/config/acceptance.php');
+$config = require __DIR__ . '/../config/test.php';
 
 (new yii\web\Application($config))->run();
diff --git a/web/index.php b/web/index.php
index d1e070a39..02d5e1da9 100644
--- a/web/index.php
+++ b/web/index.php
@@ -4,9 +4,9 @@
 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');
+require __DIR__ . '/../vendor/autoload.php';
+require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
 
-$config = require(__DIR__ . '/../config/web.php');
+$config = require __DIR__ . '/../config/web.php';
 
 (new yii\web\Application($config))->run();
diff --git a/widgets/Alert.php b/widgets/Alert.php
new file mode 100644
index 000000000..1302c1f29
--- /dev/null
+++ b/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);
+        }
+    }
+}
diff --git a/yii b/yii
index fc7090f77..1d0008f5d 100755
--- a/yii
+++ b/yii
@@ -3,18 +3,18 @@
 /**
  * Yii console bootstrap file.
  *
- * @link http://www.yiiframework.com/
+ * @link https://www.yiiframework.com/
  * @copyright Copyright (c) 2008 Yii Software LLC
- * @license http://www.yiiframework.com/license/
+ * @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');
+require __DIR__ . '/vendor/autoload.php';
+require __DIR__ . '/vendor/yiisoft/yii2/Yii.php';
 
-$config = require(__DIR__ . '/config/console.php');
+$config = require __DIR__ . '/config/console.php';
 
 $application = new yii\console\Application($config);
 $exitCode = $application->run();
diff --git a/yii.bat b/yii.bat
index d516b3a19..ce14c92bc 100644
--- a/yii.bat
+++ b/yii.bat
@@ -4,9 +4,9 @@ rem -------------------------------------------------------------
 rem  Yii command line bootstrap script for Windows.
 rem
 rem  @author Qiang Xue <qiang.xue@gmail.com>
-rem  @link http://www.yiiframework.com/
+rem  @link https://www.yiiframework.com/
 rem  @copyright Copyright (c) 2008 Yii Software LLC
-rem  @license http://www.yiiframework.com/license/
+rem  @license https://www.yiiframework.com/license/
 rem -------------------------------------------------------------
 
 @setlocal
