Difference between #144 and #209 of
Yii v2 snippet guide

Changes

Title unchanged

Yii v2 snippet guide

Category unchanged

Tutorials

Yii version unchanged

2.0

Tags unchanged

tutorial,beginner,yii2

Content changed

**Intro** --
 
Hi all!
 
 
**Please note, that this article will be updated regularly as I have more and more snippets so come back in a few weeks**
 
 
This snippet guide works with the basic Yii demo application and enhances it. It continues in my series of simple Yii tutorials. Previous two contain basic info about MVC concept, exporting to Excel and other topics so read them as well, but they are meant for Yii v1. I started with them cca in year 2011:
 
 
- [https://www.yiiframework.com/wiki/250/yii-for-beginners](https://www.yiiframework.com/wiki/250/yii-for-beginners)
 
- [https://www.yiiframework.com/wiki/462/yii-for-beginners-2](https://www.yiiframework.com/wiki/462/yii-for-beginners-2)
 
 
... and today I am beginning with Yii 2 so I will also gather my snippets and publish them here so we all can quickly setup the yii-basic-demo just by copying and pasting. This is my goal - to show how-to without long descriptions.
 
 
I was suprised that the Yii 2 demo application does not contain some basic functionalities (like login via DB, translations etc) which must be implemented in the most of web projects so I will focus on them. Plus I will talk about GitLab. 
 
 
If you find any problems in my snippets, let me know, please.
 
 
**Prerequisities**
 
--
 
Skip this paragraph if you know how to run your Yii demo project...
 
 
I work with Win10 + [XAMPP Server](https://www.apachefriends.org/download.html) so I will expect this configuration. Do not forget to start the server and enable Apache + MySQL in the dialog. Then test that following 2 URLs work for you
 
 
- [http://localhost/](http://localhost/)
 
- [http://localhost/phpmyadmin/](http://localhost/phpmyadmin/)
 
 
You should also download the [Yii basic demo application](https://www.yiiframework.com/download) and place it into the **htdocs** folder. In my case it is here:
 
 
- C:\xampp\htdocs
 
 
And your index.php should be here:
 
 
- C:\xampp\htdocs\basic\web\index.php
 
 
If you set things correctly up, following URL will open your demo application. Now it will probably throw an exception:
 
 
- [http://localhost/basic/web/](http://localhost/basic/web/)
 
 
**The Exception is removed by entering any text into attribute 'cookieValidationKey' in file**:
 
 
- C:\xampp\htdocs\basic\config\web.php
 
 
Dont forget to connect Yii to the DB. It is done in file:
 
 
- C:\xampp\htdocs\basic\config\db.php
 
 
... but it should work out-of-the-box if you use DB name "yii2basic" which is also used in examples below ...
 
 
Yii demo app + GitLab
 
---
 
 
Once you download and run the basic app, I recommend to push it into [GitLab](https://gitlab.com/). You will probably need a SSH certificate which can be generated [like this](https://www.huber.xyz/?p=275) using [PuTTYgen](https://www.puttygen.com/). When I work with Git I use [TortoiseGIT](https://www.puttygen.com/) which integrates all git functionalities into the context menu in Windows File Explorer.
 
 
First go to GitLab web and [create a new project](https://gitlab.com/projects/new). Then you might need to fight a bit, because the process of connecting your PC to GIT seems to be quite complicated. At least for me.
 
 
Once things work, just create an empty folder, right click it and select Git Clone. Enter your git path, best is this format:
 
 
- git@gitlab.com:{username}/{projectName}.git
 
- or you can use also this URL:
 
- https://gitlab.com/{username}/{projectName}.git
 
- or you can use HTTP:
 
- http://gitlab.com/{username}/{projectName}.git
 
 
When cloned, copy the content of the "basic" folder into the new empty git-folder and push everything except for folder "vendor". (It contains 75MB and 7000 files so you dont want to have it in GIT)
 
 
Then you can start to modify you project, for example based on this "tutorial".
 
 
**Automatical copying from GitLab to FTP**
 
 
I found these two pages where things are explained: [link](https://www.savjee.be/2019/04/gitlab-ci-deploy-to-ftp-with-lftp/) [link](https://stackoverflow.com/questions/49632077/use-gitlab-pipeline-to-push-data-to-ftpserver) and I just copied something.
 
 
You only need to create 1 file in your GitLab repository. It is named .gitlab-ci.yml and it should contain following code:
 
 
```
 
variables:
 
  HOST: "ftp url"
 
  USERNAME: "user"
 
  PASSWORD: "password"
 
  FOLDER: "relative path if needed, or just ./"
 
 
deploy:
 
  script:
 
    - apt-get update -qq && apt-get install -y -qq lftp
 
    - lftp -c "set ftp:ssl-allow no; open -u $USERNAME,$PASSWORD $HOST; mirror -Rnev ./ $FOLDER --ignore-time --parallel=10 --exclude-glob .git* --exclude .git/ --exclude vendor --exclude web/assets --exclude web/index.php --exclude web/index-test.php" 
 
  only:
 
    - master
 
```
 
 
I just added some exclusions (listed below) and will probably add **--delete** in the future. Read linked webs.
 
- exclude vendor = huge folder with 3rd party SW which is not in GIT
 
- exclude web/assets = also some cache
 
- exclude web/index.php = in GIT is your devel index witn DEBU mode enabled. You dont wanna have this file in productive environment
 
- exclude web/index-test.php = tests are only on your computer and in GIT
 
 
**User management + DB creation + login via DB**
 
---
 
To create DB with users, use following command. I recommend charset **utf8_unicode_ci** (or utf8mb4_unicode_ci) as it allows you to use [more international characters](https://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci).
 
 
```sql
 
CREATE DATABASE IF NOT EXISTS `yii2basic` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
 
 
CREATE TABLE IF NOT EXISTS `user` (
 
  `id` INT NOT NULL AUTO_INCREMENT,
 
  `username` VARCHAR(45) NOT NULL,
 
  `password` VARCHAR(60) NOT NULL,
 
  `email`    VARCHAR(60) NOT NULL,
 
  `authKey`  VARCHAR(60),
 
  PRIMARY KEY (`id`))
 
ENGINE = InnoDB;
 
 
INSERT INTO `user` (`id`, `username`, `password`, `email`, `authKey`) VALUES (NULL, 'user01', '0497fe4d674fe37194a6fcb08913e596ef6a307f', 'user01@gmail.com', NULL);
 
```
 
 
If you must use MyISAM instead of InnoDB, just change the word InnoDB into MYISAM.
 
 
Then replace existing model User with following snippet
 
 
- The model was generated by Gii and originally had 3 methods: tableName(), rules(), attributeLabels()
 
- In order to use the DB for login, we needed to implement IdentityInterface which requires [5 new methods](https://www.yiiframework.com/doc/api/2.0/yii-web-identityinterface). 
 
- Plus we add 2 methods because of the default LoginForm and 1 validator.
 
 
 
```php
 
<?php
 
 
namespace app\models;
 
 
use Yii;
 
 
class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface {
 
 
    // When user detail is being edited we will only modify attribute password_new
 
    // Why? We dont want to load password-hash from DB and display it to the user
 
    // We only want him to see empty field and if it is filled in, password is changed on background
 
    public $password_new;
 
    public $password_new_repeat;
 
 
    // Use this scenario in UserController->actionCreate() right after: $model = new User() like this:
 
    // $model->scenario = User::SCENARIO_CREATE;
 
    // This will force the user to enter the password when new user is created
 
    // When user is edited, new password is not needed
 
    const SCENARIO_CREATE = "user-create";
 
 
    // ----- Default 3 model-methods by GII:
 
 
    public static function tableName() {
 
        return 'user';
 
    }
 
 
    public function rules() {
 
        return [
 
            [['username', 'email'], 'required'],
 
            [['password_new_repeat', 'password_new'], 'required', "on" => self::SCENARIO_CREATE],
 
            [['username', 'email'], 'string', 'max' => 45],
 
            ['email', 'email'],
 
            [['password', 'authKey'], 'string', 'max' => 60],
 
            [['password', 'password_new_repeat', 'password_new'], 'safe'],
 
            ['password_new_repeat', 'compare', 'operator' => '==', 'compareAttribute' => 'password_new'],
 
            ['password_new', 'compare', 'operator' => '==', 'compareAttribute' => 'password_new_repeat'],
 
            
 
            ['password_new_repeat', 'setPasswordWhenChanged'],
 
        ];
 
    }
 
 
    public function attributeLabels() {
 
        return [
 
            'id' => Yii::t('app', 'ID'),
 
            'username' => Yii::t('app', 'Username'),
 
            'password' => Yii::t('app', 'Password'),
 
            'password_new' => Yii::t('app', 'New password'),
 
            'password_new_repeat' => Yii::t('app', 'Repeat new password'),
 
            'authKey' => Yii::t('app', 'Auth Key'),
 
            'email' => Yii::t('app', 'Email'),
 
        ];
 
    }
 
 
    // ----- Password validator
 
 
    public function setPasswordWhenChanged($attribute_name, $params) {
 
 
        if (trim($this->password_new_repeat) === "") {
 
            return true;
 
        }
 
 
        if ($this->password_new_repeat === $this->password_new) {
 
            $this->password = sha1($this->password_new_repeat);
 
        }
 
 
        return true;
 
    }
 
 
    // ----- IdentityInterface methods:
 
 
    public static function findIdentity($id) {
 
        return static::findOne($id);
 
    }
 
 
    public static function findIdentityByAccessToken($token, $type = null) {
 
        return static::findOne(['access_token' => $token]);
 
    }
 
 
    public function getId() {
 
        return $this->id;
 
    }
 
 
    public function getAuthKey() {
 
        return $this->authKey;
 
    }
 
 
    public function validateAuthKey($authKey) {
 
        return $this->authKey === $authKey;
 
    }
 
 
    // ----- Because of default LoginForm:
 
 
    public static function findByUsername($username) {
 
        return static::findOne(['username' => $username]);
 
    }
 
 
    public function validatePassword($password) {
 
        return $this->password === sha1($password);
 
    }
 
 
}
 
 
```
 
 
Validators vs JavaScript:
 
- There are 2 types of validators. All of them are used in method **rules**, but as you can see, the validator **setPasswordWhenChanged** is my custom validator and needs a special method. *(I just abused a validator to set the password value, no real validation happens inside)*
 
- If a validator does not need this special method, it is automatically converted into JavaScript and is used on the web page when you are typing.
 
- If a validator needs the method, it cannot be converted into JavaScript so the rule is checked  only in the moment when user sends the form to the server - after successful JavaScript validation.
 
 
Now you can also create **CRUD** for the User model using GII:
 
 
- [http://localhost/basic/web/index.php?r=gii](http://localhost/basic/web/index.php?r=gii).
 
 
CRUD = Create Read Update Delete = views and controller. On the GII page enter following values:
 
 
- Model Class = app\models\User
 
- Search Model Class = app\models\UserSearch
 
- Controller Class = app\controllers\UserController
 
- View Path can be empty or you can set: views\user
 
- Again enable i18n
 
 
And then you can edit users on this URL: [http://localhost/basic/web/index.php?r=user](http://localhost/basic/web/index.php?r=user) ... but it is not all. You have to modify the view-files so that correct input fields are displayed!
 
 
Open folder views\user and do following:
 
 
- \_form.php - rename input **password** to **password_new** then duplicate it and rename to **password_new_repeat**. Remove **authKey**.
 
- \_search.php - remove **password** and **authKey**.
 
- index.php - remove **password** and **authKey**.
 
- view.php - remove **password** and **authKey**.
 
 
Plus do not forget to use the new scenario in UserController->actionCreate() like this:
 
 
```php
 
public function actionCreate()
 
{
 
  $model = new User();
 
  $model->scenario = User::SCENARIO_CREATE; // the new scenario!
 
  // ...
 
```
 
 
 
**i18n translations**
 
---
 
Translations are fairly simple, but I probably didnt read manuals carefully so it took me some time. Note that now I am only describing translations which are saved in files. I do not use DB translations yet. Maybe later.
 
 
**1 - Translating short texts and captions**
 
 
First create following folders and file. 
 
 
- "C:\xampp\htdocs\basic\messages\cs-CZ\app.php"
 
 
*(Note that cs-CZ is for Czech Lanuage. For German you should use de-DE etc. Use any other language if you want.)*
 
 
The idea behind is that in the code there are used only English texts and if you want to change from English to some other language this file will be used.
 
 
Now go to file config/web.php, find section "components" and paste the i18n section:
 
 
```php
 
    'components' => [
 
        'i18n' => [
 
          'translations' => [
 
            '*' => [
 
              'class' => 'yii\i18n\PhpMessageSource',
 
              'basePath' => '@app/messages',
 
              'sourceLanguage' => 'en-US',
 
              'fileMap' => [
 
                'app' => 'app.php'
 
              ],
 
            ],
 
          ],
 
        ], // end of 'i18n'
 
 
        // ... other configurations
 
 
    ], // end of 'components'
 
    
 
```
 
 
Explanation of the asterisk * can be found in article [https://www.yiiframework.com/doc/guide/2.0/en/tutorial-i18n](https://www.yiiframework.com/doc/guide/2.0/en/tutorial-i18n)
 
 
You surely saw that in views and models there are translated-texts saved like this:
 
 
```php
 
Yii::t('app', 'New password'),
 
```
 
 
It means that this text belongs to category "app" and its English version (and also its ID) is "New password". So this ID will be searched in the file you just created. In my case it was the Czech file:
 
 
- "C:\xampp\htdocs\basic\messages\cs-CZ\app.php"
 
 
Therefore open the file and paste there following code:
 
 
```php
 
<?php
 
return [
 
    'New password' => 'Nové heslo',
 
];
 
?>
 
```
 
 
Now you can open the page for adding a new user and you will see than so far nothing changed :-)
 
 
- [http://localhost/basic-/web/index.php?r=user%2Fcreate](http://localhost/basic-/web/index.php?r=user%2Fcreate)
 
 
We must change the language ... For now let's do it in a primitive and permanent way again in file config/web.php
 
 
```php
 
$config = [
 
    // use your language
 
    // also accessible via Yii::$app->language
 
    'language' => 'cs-CZ',
 
    
 
    // This attribute is not necessary.
 
    // en-US is default value
 
    'sourceLanguage' => 'en-US',
 
    
 
    // ... other configs
 
```
 
 
**2 - Translating long texts and whole views**
 
 
If you have a view with long texts and you want to translate it into a 2nd language, it is not good idea to use the previous approach, because it uses the English text as the ID. 
 
 
It is better to translate the whole view. How? ... Just create a sub-folder next to the view and give it name which will be identical to the target-lang-ID. In my case the 2nd language is Czech so I created following folder and copied my view in it. So now I have 2 identical views with identical names:
 
 
- "C:\xampp\htdocs\basic\views\site\about.php" ... English
 
- "C:\xampp\htdocs\basic\views\site\cs-CZ\about.php" ... Czech
 
 
Yii will automatically use the Czech version if needed.
 
 
**Switching languages + session + lang-dropdown in the top menu**
 
---
 
First lets add to file config/params.php attributes with list of supported languages:
 
 
```php
 
<?php
 
return [
 
    // ...
 
    'allowedLanguages' => [
 
        'en-US' => "English",
 
        'cs-CZ' => "Česky",
 
    ],
 
    'langSwitchUrl' => '/site/set-lang',
 
];
 
 
```
 
 
This list can be displayed in the main menu. Edit file:
 
 
- C:\xampp\htdocs\basic\views\layouts\main.php
 
 
And above the Nav::widget add few rows:
 
 
```php
 
    $listOfLanguages = [];
 
    $langSwitchUrl = Yii::$app->params["langSwitchUrl"];
 
    foreach (Yii::$app->params["allowedLanguages"] as $langId => $langName) {
 
        $listOfLanguages[] = ['label' => Yii::t('app', $langName), 'url' => [$langSwitchUrl, 'langID' => $langId]];
 
    }
 
```
 
 
and then add one item into Nav::widge
 
 
```php
 
    echo Nav::widget([
 
        // ...
 
        'items' => [
 
            // ...
 
            ['label' => Yii::t('app', 'Language'),'items' => $listOfLanguages],
 
            // ...
 
```
 
 
Now in the top-right corner you can see a new drop-down-list with list of 2 languages. If one is selected, action "site/setLang" is called so we have to create it in SiteController.
 
 
*Note that this approach will always redirect user to the new action and his work will be lost. Nevertheless this approach is very simple so I am using it in small projects. More complex projects may require an ajax call when language is changed and then updating texts using javascript so reload is not needed and user's work is preserved. But I expect that when someone opens the web, he/she sets the language immediately and then there is no need for further changes.*
 
 
The setLang action looks like this:
 
 
```php
 
    public function actionSetLang($langID = "") {
 
        $allowedLanguages = Yii::$app->params["allowedLanguages"];
 
        $langID = trim($langID);
 
        if ($langID !== "" && array_key_exists($langID, $allowedLanguages)) {
 
            Yii::$app->session->set('langID', $langID);
 
        }
 
        return $this->redirect(['site/index']);
 
    }
 
```
 
 
As you can see when the language is changed, redirection to site/index happens. Also mind that we are not modifying the attribute from config/web.php using Yii::$app->language, but we are saving the value into the session. The reason is that PHP deletes memory after every click, only session is kept. 
 
 
We then can use the langID-value in other controllers using new method beforeAction:
 
 
```php
 
    public function beforeAction($action) {
 
 
        if (!parent::beforeAction($action)) {
 
            return false;
 
        }
 
 
        Yii::$app->language = Yii::$app->session->get('langID');
 
 
        return true;
 
    }
 
```
 
 
.. or you can create one parent-controller named for example BaseController. All other controllers will extend it. 
 
 
```php
 
<?php
 
 
namespace app\controllers;
 
 
use Yii;
 
use yii\web\Controller;
 
 
class BaseController extends Controller {
 
 
    public function beforeAction($action) {
 
 
        if (!parent::beforeAction($action)) {
 
            return false;
 
        }
 
 
        Yii::$app->language = Yii::$app->session->get('langID');
 
 
        return true;
 
    }
 
 
}
 
 
```
 
 
As you can see in the snippet above, other controllers must contain row "use app\controllers\BaseController" + "extends BaseController" 
 
 
 
**Simple access rights**
 
---
 
Every controller can allow different users/guests to use different actions. Method behaviors() can be used to do this. If you generate the controller using GII the method will be present and you will just add the "access-part"  like this:
 
 
```php
 
 
// don't forget to add this import:
 
use yii\filters\AccessControl;
 
 
public function behaviors() {
 
  return [
 
    // ...
 
    'access' => [
 
      'class' => AccessControl::className(),
 
      'rules' => [
 
        [
 
          'allow' => true,
 
          'roles' => ['@'], // logged in users
 
          // 'roles' => ['?'], // guests
 
          // 'matchCallback' => function ($rule, $action) {
 
            // all logged in users are redirected to some other page
 
            // just for demonstration of matchCallback
 
            // return $this->redirect('index.php?r=user/create');
 
          // }
 
        ],
 
      ],
 
      // All guests are redirected to site/index in current controller:
 
      'denyCallback' => function($rule, $action) {
 
        Yii::$app->response->redirect(['site/index']);
 
      },
 
    ],
 
  ];
 
}
 
```
 
 
.. This is all I needed so far. I will add more complex snippet as soon as I need it ...
 
 
Details can be found here [https://www.yiiframework.com/doc/guide/2.0/en/security-authorization](https://www.yiiframework.com/doc/guide/2.0/en/security-authorization).
 
 
**Nice URLs**
 
---
 
Just uncomment section "urlManager" in config/web.php .. htaccess file is already included in the basic demo. In case of problems see [this link](https://stackoverflow.com/questions/26525320/enable-clean-url-in-yii2).
 
 
My problem was that images were not displayed when I enabled nice URLs. Smilar discussion [here](https://stackoverflow.com/questions/39197583/image-is-not-passing-in-carousel-in-yii2). 
 
 
```php
 
// Originally I used these img-paths:
 
<img src="..\web\imgs\myimg01.jpg"/>
 
 
/// Then I had to chage them to this:
 
Html::img(Yii::$app->request->baseUrl . '/imgs/myimg01.jpg')
 
 
// The important change is using the "baseUrl"
 
```
 
 
Note that **Yii::$app->request->baseUrl** returns "/myProject/web". No trailing slash.
 
 
**How to redirect web to subfolder /web**
 
---
 
... to be added on Friday ...
 
 
**Auto redirection from login to desired URL **
 
---
 
... to be added on Friday ...
 
 
**What to change when exporting to the Internet**
 
---
 
- Delete file web/index-test.php
 
- In file web/index.php comment you 2 first lines containing YII_DEBUG + YII_ENV
 
- Delete the text from view site/login which says "You may login with admin/admin or demo/demo."
 
 
**Saving contact inqueries into DB**
 
---
 
```sql
 
DROP TABLE IF EXISTS `contact` ;
 
 
CREATE TABLE IF NOT EXISTS `contact` (
 
  `id` INT NOT NULL AUTO_INCREMENT,
 
  `name` VARCHAR(45) NOT NULL,
 
  `email` VARCHAR(45) NOT NULL,
 
  `subject` VARCHAR(100) NOT NULL,
 
  `body` TEXT NOT NULL,
 
  PRIMARY KEY (`id`))
 
ENGINE = InnoDB;
 
```
 
 
- Create the DB table
 
- Generate Model + CRUD using GII
 
- In Site controller replace ContactForm with Contact (in section "use" and in actionContact) and in the action change the IF condition:
 
```php
 
use app\models\Contact;
 
// ... 
 
public function actionContact() {
 
    $model = new Contact();
 
    if ($model->load(Yii::$app->request->post()) && $model->save()) {
 
    // ...
 
```
 
- Open the new contact model and add one attribute and 2 rules:
 
 
```php
 
public $verifyCode;
 
// ...
 
  ['verifyCode', 'captcha'],
 
  ['email', 'email'],
 
 
// and translation for Captcha
 
'verifyCode' => Yii::t('app', 'Verification'),
 
```
 
 
- You can also delete one paragraph from view/site/contact
 
```php
 
<p>
 
Note that if you turn on the Yii debugger ...
 
```
 
 
Then some security - filtering users in the new ContactController:
 
 
```php
 
public function beforeAction($action) {
 
 
  if (!parent::beforeAction($action)) {
 
    return false;
 
  }
 
 
  $guestAllowedActions = [];
 
 
  if (Yii::$app->user->isGuest) {
 
    if (!in_array($action->actionMethod, $guestAllowedActions)) {
 
      return $this->redirect(['site/index']);
 
    }
 
  }
 
  
 
  return true;
 
}
 
```
 
 
**Tests - unit + opa**
 
---
 
... text ...
 
 
 
 
**Adding a google-like calendar**
 
---
 
 
I needed to show user a list of his events in a large calendar so I used library [fullcalendar](https://fullcalendar.io/).
 
 
Great demo which you can just copy and paste:
 
- https://fullcalendar.io/js/fullcalendar-3.0.0/demos/list-views.html
 
- ... just "view source" of the web and copy the js + html code, css and js files. 
 
 
```css
 
/*I added this style to hide vertical scroll-bars*/
 
.fc-scroller.fc-day-grid-container{
 
  overflow: hidden !important;
 
}
 
```
 
- Don't forget to use these files for example in your view like this:
 
 
```php
 
$this->registerCssFile('@web/css/fullcalendar/fullcalendar.css');
 
$this->registerCssFile('@web/css/fullcalendar/fullcalendar.print.css', ['media' => 'print']); 
 
 
$this->registerJsFile('@web/js/fullcalendar/moment.min.js', ['depends' => ['yii\web\JqueryAsset']]);
 
$this->registerJsFile('@web/js/fullcalendar/fullcalendar.min.js', ['depends' => ['yii\web\JqueryAsset']]);
 
 
// details here:
 
// https://www.yiiframework.com/doc/api/2.0/yii-web-view
 
```
 
 
... if you want to go pro, use NPM. The NPM way is described [here](https://fullcalendar.io/docs/getting-started).
 
 
API is  here:
 
https://fullcalendar.io/docs
 
... you can then enhace the calendar config from the example above
 
 
In order to make things work I had to force jQuery to be loaded before calendar scripts using file config/web.php like this
 
 
```php
 
   'components' => [
 
        
 
// ...
 

 
       'assetManager' => [
 
            'bundles' => [
 
                'yii\web\JqueryAsset' => [
 
                    'jsOptions' => [ 'position' => \yii\web\View::POS_HEAD ],
 
                ],
 
            ],
 
        ],
 
```
 
 
You can customize the calendar in many ways. For example different event-color is shown [here](https://fullcalendar.io/docs/event-colors-demo). Check the source code.
 
 
**Scenarios - UNKNOWN SCENARIO EXCEPTION**
 
---
 
I have been using scenarios a lot but today I spent 1 hour on a problem - I had 2 scenarios and one of them was just assigned to the model ...
 
 
```php
 
$model->scenario = "abc";
 
```
 
 
... but had no **rule** defined yet. I wanted to implement the rule later, but I didnt know that when you set a scenario to your model it **must** be used in method rules() or defined in method scenarios(). So take this into consideration. I expected that when the scenario has no rules it will just be skipped or deleted. 
-
 
Hi all!
 
 
**Please note, that this article will be updated regularly as I have more and more snippets so come back in a few weeks**
 
 
This snippet guide works with the basic Yii demo application and enhances it. It continues in my series of simple Yii tutorials. Previous two contain basic info about MVC concept, exporting to Excel and other topics so read them as well, but they are meant for Yii v1. I started with them cca in year 2011:
 
 
- [https://www.yiiframework.com/wiki/250/yii-for-beginners](https://www.yiiframework.com/wiki/250/yii-for-beginners)
 
- [https://www.yiiframework.com/wiki/462/yii-for-beginners-2](https://www.yiiframework.com/wiki/462/yii-for-beginners-2)
 
 
... and today I am beginning with Yii 2 so I will also gather my snippets and publish them here so we all can quickly setup the yii-basic-demo just by copying and pasting. This is my goal - to show how-to without long descriptions.
 
 
I was suprised that the Yii 2 demo application does not contain some basic functionalities (like login via DB, translations etc) which must be implemented in the most of web projects so I will focus on them. Plus I will talk about GitLab. 
 
 
If you find any problems in my snippets, let me know, please.
 
 
.
 
 
.
 
 
**Prerequisities**
 
---
 
Skip this paragraph if you know how to run your Yii demo project...
 
 
I work with Win10 + [XAMPP Server](https://www.apachefriends.org/download.html) so I will expect this configuration. Do not forget to start the server and enable Apache + MySQL in the dialog. Then test that following 2 URLs work for you
 
 
- [http://localhost/](http://localhost/)
 
- [http://localhost/phpmyadmin/](http://localhost/phpmyadmin/)
 
 
You should also download the [Yii basic demo application](https://www.yiiframework.com/download) and place it into the **htdocs** folder. In my case it is here:
 
 
- C:\xampp\htdocs
 
 
And your index.php should be here:
 
 
- C:\xampp\htdocs\basic\web\index.php
 
 
If you set things correctly up, following URL will open your demo application. Now it will probably throw an exception:
 
 
- [http://localhost/basic/web/](http://localhost/basic/web/)
 
 
**The Exception is removed by entering any text into attribute 'cookieValidationKey' in file**:
 
 
- C:\xampp\htdocs\basic\config\web.php
 
 
Dont forget to connect Yii to the DB. It is done in file:
 
 
- C:\xampp\htdocs\basic\config\db.php
 
 
... but it should work out-of-the-box if you use DB name "yii2basic" which is also used in examples below ...
 
 
.
 
 
.
 
 
**Yii demo app + GitLab**
 
---
 
 
Once you download and run the basic app, I recommend to push it into [GitLab](https://gitlab.com/). You will probably need a SSH certificate which can be generated [like this](https://www.huber.xyz/?p=275) using [PuTTYgen](https://www.puttygen.com/). When I work with Git I use [TortoiseGIT](https://www.puttygen.com/) which integrates all git functionalities into the context menu in Windows File Explorer.
 
 
First go to GitLab web and [create a new project](https://gitlab.com/projects/new). Then you might need to fight a bit, because the process of connecting your PC to GIT seems to be quite complicated. At least for me.
 
 
Once things work, just create an empty folder, right click it and select Git Clone. Enter your git path, best is this format:
 
 
- git@gitlab.com:{username}/{projectName}.git
 
- or you can use also this URL:
 
- https://gitlab.com/{username}/{projectName}.git
 
- or you can use HTTP:
 
- http://gitlab.com/{username}/{projectName}.git
 
 
 
Note: What works for me the best is using the following command to clone my project and system asks me for the password. Other means of connection usually refuse me. Then I can start using TortoiseGIT.
 
 
```
 
git clone https://{username}@gitlab.com/{username}/{myProjectName}.git
 
```
 
 
When cloned, copy the content of the "basic" folder into the new empty git-folder and push everything except for folder "vendor". (It contains 75MB and 7000 files so you dont want to have it in GIT)
 
 
Then you can start to modify you project, for example based on this "tutorial".
 
 
Thanks to .gitignore files only 115 files are uploaded. Te vendor-folder can be recreated using command
 
```
 
composer install
 
```
 
which only needs file **composer.json** to exist.
 
 
 
**Automatical copying from GitLab to FTP**
 
---
 
 
I found these two pages where things are explained: [link](https://www.savjee.be/2019/04/gitlab-ci-deploy-to-ftp-with-lftp/) [link](https://stackoverflow.com/questions/49632077/use-gitlab-pipeline-to-push-data-to-ftpserver) and I just copied something.
 
 
You only need to create 1 file in your GitLab repository. It is named .gitlab-ci.yml and it should contain following code:
 
 
```
 
variables:
 
  HOST: "ftp url"
 
  USERNAME: "user"
 
  PASSWORD: "password"
 
  TARGETFOLDER: "relative path if needed, or just ./"
 
 
deploy:
 
  script:
 
    - apt-get update -qq && apt-get install -y -qq lftp
 
    - lftp -c "set ftp:ssl-allow no; open -u $USERNAME,$PASSWORD $HOST; mirror -Rnev ./ $TARGETFOLDER --ignore-time --parallel=10 --exclude-glob .git* --exclude .git/ --exclude vendor --exclude web/assets --exclude web/index.php --exclude web/index-test.php" 
 
  only:
 
    - master
 
```
 
 
I just added some exclusions (listed below) and will probably add **--delete** in the future. Read linked webs.
 
- exclude vendor = huge folder with 3rd party SW which is not in GIT
 
- exclude web/assets = also some cache
 
- exclude web/index.php = in GIT is your devel index with DEBUG mode enabled. You dont wanna have this file in productive environment
 
- exclude web/index-test.php = tests are only on your computer and in GIT
 
 
.
 
 
.
 
 
**User management + DB creation + login via DB**
 
---
 
To create DB with users, use following command. I recommend charset **utf8_unicode_ci** (or utf8mb4_unicode_ci) as it allows you to use [more international characters](https://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci).
 
 
```sql
 
CREATE DATABASE IF NOT EXISTS `yii2basic` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
 
 
USE `yii2basic`;
 
 
CREATE TABLE IF NOT EXISTS `user` (
 
  `id` INT NOT NULL AUTO_INCREMENT,
 
  `username` VARCHAR(45) NOT NULL,
 
  `password` VARCHAR(60) NOT NULL,
 
  `email`    VARCHAR(60) NOT NULL,
 
  `authKey`  VARCHAR(60),
 
  PRIMARY KEY (`id`))
 
ENGINE = InnoDB;
 
 
INSERT INTO `user` (`id`, `username`, `password`, `email`, `authKey`) VALUES (NULL, 'user01', '0497fe4d674fe37194a6fcb08913e596ef6a307f', 'user01@gmail.com', NULL);
 
```
 
 
If you must use MyISAM instead of InnoDB, just change the word InnoDB into MYISAM.
 
 
Then replace existing model User with following snippet
 
 
- The model was generated by Gii and originally had 3 methods: tableName(), rules(), attributeLabels()
 
- In order to use the DB for login, we needed to implement IdentityInterface which requires [5 new methods](https://www.yiiframework.com/doc/api/2.0/yii-web-identityinterface). 
 
- Plus we add 2 methods because of the default LoginForm and 1 validator.
 
 
 
```php
 
<?php
 
 
namespace app\models;
 
 
use Yii;
 
 
class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface {
 
 
    // When user detail is being edited we will only modify attribute password_new
 
    // Why? We dont want to load password-hash from DB and display it to the user
 
    // We only want him to see empty field and if it is filled in, password is changed on background
 
    public $password_new;
 
    public $password_new_repeat;
 
 
    // Use this scenario in UserController->actionCreate() right after: $model = new User() like this:
 
    // $model->scenario = User::SCENARIO_CREATE;
 
    // This will force the user to enter the password when new user is created
 
    // When user is edited, new password is not needed
 
    const SCENARIO_CREATE = "user-create";
 
 
    // ----- Default 3 model-methods by GII:
 
 
    public static function tableName() {
 
        return 'user';
 
    }
 
 
    public function rules() {
 
        return [
 
            [['username', 'email'], 'required'],
 
            [['password_new_repeat', 'password_new'], 'required', "on" => self::SCENARIO_CREATE],
 
            [['username', 'email'], 'string', 'max' => 45],
 
            ['email', 'email'],
 
            [['password', 'authKey'], 'string', 'max' => 60],
 
            [['password', 'password_new_repeat', 'password_new'], 'safe'],
 
            ['password_new_repeat', 'compare', 'operator' => '==', 'compareAttribute' => 'password_new'],
 
            ['password_new', 'compare', 'operator' => '==', 'compareAttribute' => 'password_new_repeat'],
 
            
 
            ['password_new_repeat', 'setPasswordWhenChanged'],
 
        ];
 
    }
 
 
    public function attributeLabels() {
 
        return [
 
            'id' => Yii::t('app', 'ID'),
 
            'username' => Yii::t('app', 'Username'),
 
            'password' => Yii::t('app', 'Password'),
 
            'password_new' => Yii::t('app', 'New password'),
 
            'password_new_repeat' => Yii::t('app', 'Repeat new password'),
 
            'authKey' => Yii::t('app', 'Auth Key'),
 
            'email' => Yii::t('app', 'Email'),
 
        ];
 
    }
 
 
    // ----- Password validator
 
 
    public function setPasswordWhenChanged($attribute_name, $params) {
 
 
        if (trim($this->password_new_repeat) === "") {
 
            return true;
 
        }
 
 
        if ($this->password_new_repeat === $this->password_new) {
 
            $this->password = sha1($this->password_new_repeat);
 
        }
 
 
        return true;
 
    }
 
 
    // ----- IdentityInterface methods:
 
 
    public static function findIdentity($id) {
 
        return static::findOne($id);
 
    }
 
 
    public static function findIdentityByAccessToken($token, $type = null) {
 
        return static::findOne(['access_token' => $token]);
 
    }
 
 
    public function getId() {
 
        return $this->id;
 
    }
 
 
    public function getAuthKey() {
 
        return $this->authKey;
 
    }
 
 
    public function validateAuthKey($authKey) {
 
        return $this->authKey === $authKey;
 
    }
 
 
    // ----- Because of default LoginForm:
 
 
    public static function findByUsername($username) {
 
        return static::findOne(['username' => $username]);
 
    }
 
 
    public function validatePassword($password) {
 
        return $this->password === sha1($password);
 
    }
 
 
}
 
 
```
 
 
Validators vs JavaScript:
 
- There are 2 types of validators. All of them are used in method **rules**, but as you can see, the validator **setPasswordWhenChanged** is my custom validator and needs a special method. *(I just abused a validator to set the password value, no real validation happens inside)*
 
- If a validator does not need this special method, it is automatically converted into JavaScript and is used on the web page when you are typing.
 
- If a validator needs the method, it cannot be converted into JavaScript so the rule is checked  only in the moment when user sends the form to the server - after successful JavaScript validation.
 
 
Now you can also create **CRUD** for the User model using GII:
 
 
- [http://localhost/basic/web/index.php?r=gii](http://localhost/basic/web/index.php?r=gii).
 
 
CRUD = Create Read Update Delete = views and controller. On the GII page enter following values:
 
 
- Model Class = app\models\User
 
- Search Model Class = app\models\UserSearch
 
- Controller Class = app\controllers\UserController
 
- View Path can be empty or you can set: views\user
 
- Again enable i18n
 
 
And then you can edit users on this URL: [http://localhost/basic/web/index.php?r=user](http://localhost/basic/web/index.php?r=user) ... but it is not all. You have to modify the view-files so that correct input fields are displayed!
 
 
Open folder views\user and do following:
 
 
- \_form.php - rename input **password** to **password_new** then duplicate it and rename to **password_new_repeat**. Remove **authKey**.
 
- \_search.php - remove **password** and **authKey**.
 
- index.php - remove **password** and **authKey**.
 
- view.php - remove **password** and **authKey**.
 
 
Plus do not forget to use the new scenario in UserController->actionCreate() like this:
 
 
```php
 
public function actionCreate()
 
{
 
  $model = new User();
 
  $model->scenario = User::SCENARIO_CREATE; // the new scenario!
 
  // ...
 
```
 
 
.
 
 
.
 
 
**i18n translations**
 
---
 
Translations are fairly simple, but I probably didnt read manuals carefully so it took me some time. Note that now I am only describing translations which are saved in files. I do not use DB translations yet. Maybe later.
 
 
**1 - Translating short texts and captions**
 
 
First create following folders and file. 
 
 
- "C:\xampp\htdocs\basic\messages\cs-CZ\app.php"
 
 
*(Note that cs-CZ is for Czech Lanuage. For German you should use de-DE etc. Use any other language if you want.)*
 
 
The idea behind is that in the code there are used only English texts and if you want to change from English to some other language this file will be used.
 
 
Now go to file config/web.php, find section "components" and paste the i18n section:
 
 
```php
 
    'components' => [
 
        'i18n' => [
 
          'translations' => [
 
            '*' => [
 
              'class' => 'yii\i18n\PhpMessageSource',
 
              'basePath' => '@app/messages',
 
              'sourceLanguage' => 'en-US',
 
              'fileMap' => [
 
                'app' => 'app.php'
 
              ],
 
            ],
 
          ],
 
        ], // end of 'i18n'
 
 
        // ... other configurations
 
 
    ], // end of 'components'
 
    
 
```
 
 
Explanation of the asterisk * can be found in article [https://www.yiiframework.com/doc/guide/2.0/en/tutorial-i18n](https://www.yiiframework.com/doc/guide/2.0/en/tutorial-i18n)
 
 
You surely saw that in views and models there are translated-texts saved like this:
 
 
```php
 
Yii::t('app', 'New password'),
 
```
 
 
It means that this text belongs to category "app" and its English version (and also its ID) is "New password". So this ID will be searched in the file you just created. In my case it was the Czech file:
 
 
- "C:\xampp\htdocs\basic\messages\cs-CZ\app.php"
 
 
Therefore open the file and paste there following code:
 
 
```php
 
<?php
 
return [
 
    'New password' => 'Nové heslo',
 
];
 
?>
 
```
 
 
Now you can open the page for adding a new user and you will see than so far nothing changed :-)
 
 
- [http://localhost/basic-/web/index.php?r=user%2Fcreate](http://localhost/basic-/web/index.php?r=user%2Fcreate)
 
 
We must change the language ... For now let's do it in a primitive and permanent way again in file config/web.php
 
 
```php
 
$config = [
 
    // use your language
 
    // also accessible via Yii::$app->language
 
    'language' => 'cs-CZ',
 
    
 
    // This attribute is not necessary.
 
    // en-US is default value
 
    'sourceLanguage' => 'en-US',
 
    
 
    // ... other configs
 
```
 
 
**2 - Translating long texts and whole views**
 
 
If you have a view with long texts and you want to translate it into a 2nd language, it is not good idea to use the previous approach, because it uses the English text as the ID. 
 
 
It is better to translate the whole view. How? ... Just create a sub-folder next to the view and give it name which will be identical to the target-lang-ID. In my case the 2nd language is Czech so I created following folder and copied my view in it. So now I have 2 identical views with identical names:
 
 
- "C:\xampp\htdocs\basic\views\site\about.php" ... English
 
- "C:\xampp\htdocs\basic\views\site\cs-CZ\about.php" ... Czech
 
 
Yii will automatically use the Czech version if needed.
 
 
.
 
 
.
 
 
**Switching languages + session + lang-dropdown in the top menu**
 
---
 
First lets add to file config/params.php attributes with list of supported languages:
 
 
```php
 
<?php
 
return [
 
    // ...
 
    'allowedLanguages' => [
 
        'en-US' => "English",
 
        'cs-CZ' => "Česky",
 
    ],
 
    'langSwitchUrl' => '/site/set-lang',
 
];
 
 
```
 
 
This list can be displayed in the main menu. Edit file:
 
 
- C:\xampp\htdocs\basic\views\layouts\main.php
 
 
And above the Nav::widget add few rows:
 
 
```php
 
    $listOfLanguages = [];
 
    $langSwitchUrl = Yii::$app->params["langSwitchUrl"];
 
    foreach (Yii::$app->params["allowedLanguages"] as $langId => $langName) {
 
        $listOfLanguages[] = ['label' => Yii::t('app', $langName), 'url' => [$langSwitchUrl, 'langID' => $langId]];
 
    }
 
```
 
 
and then add one item into Nav::widge
 
 
```php
 
    echo Nav::widget([
 
        // ...
 
        'items' => [
 
            // ...
 
            ['label' => Yii::t('app', 'Language'),'items' => $listOfLanguages],
 
            // ...
 
```
 
 
Now in the top-right corner you can see a new drop-down-list with list of 2 languages. If one is selected, action "site/setLang" is called so we have to create it in SiteController.
 
 
*Note that this approach will always redirect user to the new action and his work will be lost. Nevertheless this approach is very simple so I am using it in small projects. More complex projects may require an ajax call when language is changed and then updating texts using javascript so reload is not needed and user's work is preserved. But I expect that when someone opens the web, he/she sets the language immediately and then there is no need for further changes.*
 
 
The setLang action looks like this:
 
 
```php
 
    public function actionSetLang($langID = "") {
 
        $allowedLanguages = Yii::$app->params["allowedLanguages"];
 
        $langID = trim($langID);
 
        if ($langID !== "" && array_key_exists($langID, $allowedLanguages)) {
 
            Yii::$app->session->set('langID', $langID);
 
        }
 
        return $this->redirect(['site/index']);
 
    }
 
```
 
 
As you can see when the language is changed, redirection to site/index happens. Also mind that we are not modifying the attribute from config/web.php using Yii::$app->language, but we are saving the value into the session. The reason is that PHP deletes memory after every click, only session is kept. 
 
 
We then can use the langID-value in other controllers using new method beforeAction:
 
 
```php
 
    public function beforeAction($action) {
 
 
        if (!parent::beforeAction($action)) {
 
            return false;
 
        }
 
 
        Yii::$app->language = Yii::$app->session->get('langID');
 
 
        return true;
 
    }
 
```
 
 
.. or you can create one parent-controller named for example BaseController. All other controllers will extend it. 
 
 
```php
 
<?php
 
 
namespace app\controllers;
 
 
use Yii;
 
use yii\web\Controller;
 
 
class BaseController extends Controller {
 
 
    public function beforeAction($action) {
 
 
        if (!parent::beforeAction($action)) {
 
            return false;
 
        }
 
 
        Yii::$app->language = Yii::$app->session->get('langID');
 
 
        return true;
 
    }
 
 
}
 
 
```
 
 
As you can see in the snippet above, other controllers must contain row "use app\controllers\BaseController" + "extends BaseController" 
 
 
.
 
 
.
 
 
**Simple access rights**
 
---
 
Every controller can allow different users/guests to use different actions. Method behaviors() can be used to do this. If you generate the controller using GII the method will be present and you will just add the "access-part"  like this:
 
 
```php
 
 
// don't forget to add this import:
 
use yii\filters\AccessControl;
 
 
public function behaviors() {
 
  return [
 
    // ...
 
    'access' => [
 
      'class' => AccessControl::className(),
 
      'rules' => [
 
        [
 
          'allow' => true,
 
          'roles' => ['@'], // logged in users
 
          // 'roles' => ['?'], // guests
 
          // 'matchCallback' => function ($rule, $action) {
 
            // all logged in users are redirected to some other page
 
            // just for demonstration of matchCallback
 
            // return $this->redirect('index.php?r=user/create');
 
          // }
 
        ],
 
      ],
 
      // All guests are redirected to site/index in current controller:
 
      'denyCallback' => function($rule, $action) {
 
        Yii::$app->response->redirect(['site/index']);
 
      },
 
    ],
 
  ];
 
}
 
```
 
 
.. This is all I needed so far. I will add more complex snippet as soon as I need it ...
 
 
Details can be found here [https://www.yiiframework.com/doc/guide/2.0/en/security-authorization](https://www.yiiframework.com/doc/guide/2.0/en/security-authorization).
 
 
.
 
 
.
 
 
**Nice URLs**
 
---
 
Just uncomment section "urlManager" in config/web.php .. htaccess file is already included in the basic demo. In case of problems see [this link](https://stackoverflow.com/questions/26525320/enable-clean-url-in-yii2).
 
 
My problem was that images were not displayed when I enabled nice URLs. Smilar discussion [here](https://stackoverflow.com/questions/39197583/image-is-not-passing-in-carousel-in-yii2). 
 
 
```php
 
// Originally I used these img-paths:
 
<img src="..\web\imgs\myimg01.jpg"/>
 
 
/// Then I had to chage them to this:
 
Html::img(Yii::$app->request->baseUrl . '/imgs/myimg01.jpg')
 
 
// The important change is using the "baseUrl"
 
```
 
 
Note that **Yii::$app->request->baseUrl** returns "/myProject/web". No trailing slash.
 
 
.
 
 
.
 
 
**How to redirect web to subfolder /web**
 
---
 
Note: If you are using the advanced demo app, [this](https://stackoverflow.com/questions/37451324/how-to-change-base-url-and-enable-prettyurl-in-yii2) link can be interesting for you.
 
 
Yii 2 has the speciality that index.php is hidden in the web folder. I didnt find in the official documentation the important info - how to hide the folder, because user is not interested in it ...
 
 
Our demo application is placed in folder:
 
- C:\xampp\htdocs\basic\web\index.php
 
 
Now you will need 2 files named .htaccess
 
- C:\xampp\htdocs\basic\web\\.htaccess
 
- C:\xampp\htdocs\basic\\.htaccess
 
 
The first one is mentioned in chapter **Nice URLs** and looks like this:
 
 
```
 
RewriteEngine on
 
RewriteCond %{REQUEST_FILENAME} !-d
 
RewriteCond %{REQUEST_FILENAME} !-f
 
RewriteRule . index.php [L]
 
```
 
 
The second is simpler:
 
 
```
 
RewriteEngine on
 
RewriteRule ^(.*)$ web/$1 [L]
 
```
 
 
... it only adds the word "web" into all URLs. But first we have to remove the word from URLs. Open file config/web.php and find section **request**. Add attribute **baseUrl**:
 
 
```
 
'request' => [
 
  // 'cookieValidationKey' => ...
 
  'baseUrl' => '/basic', // add this line
 
],
 
```
 
Now things will work for you. But it might be needed to use different value for devel and productive environment. Productive web is usually in the root-folder so baseUrl should be en empty string. I did it like this:
 
 
```php
 
$baseUrlWithoutWebFolder = "";
 
if (YII_ENV_DEV) {
 
  $baseUrlWithoutWebFolder = '/basic';
 
}
 
 
// ...
 
 
'request' => [
 
  // 'cookieValidationKey' => ...
 
  'baseUrl' => $baseUrlWithoutWebFolder,
 
],
 
 
```
 
 
I will test this and if I find problems and solutions I will add them.
 
 
.
 
 
.
 
 
**Auto redirection from login to desired URL **
 
---
 
... to be added  ...
 
 
.
 
 
.
 
 
**What to change when exporting to the Internet**
 
---
 
- Delete file web/index-test.php
 
- In file web/index.php comment you 2 first lines containing YII_DEBUG + YII_ENV
 
- Delete the text from view site/login which says "You may login with admin/admin or demo/demo."
 
 
.
 
 
.
 
 
**Saving contact inqueries into DB**
 
---
 
```sql
 
DROP TABLE IF EXISTS `contact` ;
 
 
CREATE TABLE IF NOT EXISTS `contact` (
 
  `id` INT NOT NULL AUTO_INCREMENT,
 
  `name` VARCHAR(45) NOT NULL,
 
  `email` VARCHAR(45) NOT NULL,
 
  `subject` VARCHAR(100) NOT NULL,
 
  `body` TEXT NOT NULL,
 
  PRIMARY KEY (`id`))
 
ENGINE = InnoDB;
 
```
 
 
- Create the DB table
 
- Generate Model + CRUD using GII
 
- In Site controller replace ContactForm with Contact (in section "use" and in actionContact) and in the action change the IF condition:
 
```php
 
use app\models\Contact;
 
// ... 
 
public function actionContact() {
 
    $model = new Contact();
 
    if ($model->load(Yii::$app->request->post()) && $model->save()) {
 
    // ...
 
```
 
- Open the new contact model and add one attribute and 2 rules:
 
 
```php
 
public $verifyCode;
 
// ...
 
  ['verifyCode', 'captcha'],
 
  ['email', 'email'],
 
 
// and translation for Captcha
 
'verifyCode' => Yii::t('app', 'Verification'),
 
```
 
 
- You can also delete one paragraph from view/site/contact
 
```php
 
<p>
 
Note that if you turn on the Yii debugger ...
 
```
 
 
Then some security - filtering users in the new ContactController:
 
 
```php
 
public function beforeAction($action) {
 
 
  if (!parent::beforeAction($action)) {
 
    return false;
 
  }
 
 
  $guestAllowedActions = [];
 
 
  if (Yii::$app->user->isGuest) {
 
    if (!in_array($action->actionMethod, $guestAllowedActions)) {
 
      return $this->redirect(['site/index']);
 
    }
 
  }
 
  
 
  return true;
 
}
 
```
 
 
.
 
 
.
 
 
**Tests - unit + opa**
 
---
 
... text ...
 
 
.
 
 
.
 
 
**Adding a google-like calendar**
 
---
 
 
I needed to show user a list of his events in a large calendar so I used library [fullcalendar](https://fullcalendar.io/).
 
 
Great demo which you can just copy and paste:
 
- https://fullcalendar.io/js/fullcalendar-3.0.0/demos/list-views.html
 
- ... just "view source" of the web and copy the js + html code, css and js files. 
 
 
```css
 
/*I added this style to hide vertical scroll-bars*/
 
.fc-scroller.fc-day-grid-container{
 
  overflow: hidden !important;
 
}
 
```
 
- Don't forget to use these files for example in your view like this:
 
 
```php
 
$this->registerCssFile('@web/css/fullcalendar/fullcalendar.css');
 
$this->registerCssFile('@web/css/fullcalendar/fullcalendar.print.css', ['media' => 'print']); 
 
 
$this->registerJsFile('@web/js/fullcalendar/moment.min.js', ['depends' => ['yii\web\JqueryAsset']]);
 
$this->registerJsFile('@web/js/fullcalendar/fullcalendar.min.js', ['depends' => ['yii\web\JqueryAsset']]);
 
 
// details here:
 
// https://www.yiiframework.com/doc/api/2.0/yii-web-view
 
```
 
 
... if you want to go pro, use NPM. The NPM way is described [here](https://fullcalendar.io/docs/getting-started).
 
 
API is  here:
 
https://fullcalendar.io/docs
 
... you can then enhace the calendar config from the example above
 
 
In order to make things work I had to force jQuery to be loaded before calendar scripts using file config/web.php like this
 
 
```php
 
   'components' => [
 
        
 
// ...
 

 
       'assetManager' => [
 
            'bundles' => [
 
                'yii\web\JqueryAsset' => [
 
                    'jsOptions' => [ 'position' => \yii\web\View::POS_HEAD ],
 
                ],
 
            ],
 
        ],
 
```
 
 
You can customize the calendar in many ways. For example different event-color is shown [here](https://fullcalendar.io/docs/event-colors-demo). Check the source code.
 
 
.
 
 
.
 
 
**Scenarios - UNKNOWN SCENARIO EXCEPTION**
 
---
 
I have been using scenarios a lot but today I spent 1 hour on a problem - I had 2 scenarios and one of them was just assigned to the model ...
 
 
```php
 
$model->scenario = "abc";
 
```
 
 
... but had no **rule** defined yet. I wanted to implement the rule later, but I didnt know that when you set a scenario to your model it **must** be used in method rules() or defined in method scenarios(). So take this into consideration. I expected that when the scenario has no rules it will just be skipped or deleted. 
 
 
.
 
 
.
 
 
**Richtext / wysiwyg HTML editor - Summernote**
 
---
 
If you want to allow user to enter html-formatted text, you need to use some HTML wysiwyg editor, because ordinary TextArea can only work with plain text. It seems to me that [Summernote](https://summernote.org/getting-started/#simple-example) is the simplest addon available:
 
 
```javascript
 
// Add following code to file layouts/main.php .. 
 
// But make sure jquery is already loaded !! 
 
// - Read about this topic in chapter "Adding a google-like calendar"
 
 
<!-- include summernote css/js -->
 
<link href="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.12/summernote.css" rel="stylesheet">
 
<script src="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.12/summernote.js"></script>
 
 
// And then in any view you can use this code:
 
 
<script>
 
$(document).ready(function() {
 
  $('#summernote1').summernote();
 
  $('#summernote2').summernote();
 
});
 
</script>
 
<div id="summernote1">Hello Summernote</div>
 
 
<form method="post">
 
  <textarea id="summernote2" name="editordata"></textarea>
 
</form>
 
 
```
 
 
On this page I showed how to save Contacts inqueries into database. If you want to use the richtext editor in this section, open view contact/\_form.php and just add this JS code:
 
 
```javascript
 
<script>
 
$(document).ready(function() {
 
  $('#contact-body').summernote();
 
});
 
</script>
 
```
 
 
It will be saved to DB as HTML code. But this might be also a source of problems, because user can inject some dangerous HTML code. So keep this in mind.
 
 
Now you will also have to modify view contact/view.php like this in order to see nice formatted text:
 
 
```php
 
DetailView::widget([
 
  'model' => $model,
 
  'attributes' => [
 
    // ...
 
    'body:html',
 
  ],
 
])
 
```
 
... to discover all possible formatters, check all asXXX() functions on [this](https://www.yiiframework.com/doc/api/2.0/yii-i18n-formatter) page: 
 
 
.
 
 
.
 
 
**SEO optimization**
 
---
 
This is not really a YII topic but as my article is some kind of a code-library I will paste it here as well.
 
To test your SEO score you can use special webs. For example [seotesteronline](https://suite.seotesteronline.com/seo-checker), but only once per day. 
 
It will show some statistics and recommend enhancements so that your web is nicely shown on FB and Twitter or found by Google.
 
 
Important are for example OG meta tags or [TWITTER meta tags](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/summary). They are basicly the same. Read more [here](https://css-tricks.com/essential-meta-tags-social-media/). You can test them at [iframely.com](http://debug.iframely.com).
 
 
Basic tags are following and you should place them to head:
 
- Note that Twitter is using attribute "name" instead of "property" which is defined in OG
 
- btw OG was introduced by Facebook. Twitter can process it as well, but SEO optimizers will report an error when Twitter's tags are missing.
 
 
```html
 
 
<!DOCTYPE html>
 
<html lang="<?= Yii::$app->language ?>">
 
<head>
 
 
  <meta property="og:site_name" content="European Travel, Inc.">
 
  <meta property="og:title" content="European Travel Destinations">
 
  <meta property="og:description" content="Offering tour packages for individuals or groups.">
 
  <meta property="og:image" content="http://euro-travel-example.com/thumbnail.jpg">
 
  <meta property="og:url" content="http://euro-travel-example.com/index.htm">
 
  <meta name="twitter:card" content="summary_large_image">
 
 
  <!--  Non-Essential, But Recommended -->
 
  <meta property="og:site_name" content="European Travel, Inc.">
 
  <meta name="twitter:image:alt" content="Alt text for image">
 
 
  <!--  Non-Essential, But Required for Analytics -->
 
  <meta property="fb:app_id" content="your_app_id" />
 
  <meta name="twitter:site" content="@website-username">
 
  
 
  <!-- seotesteronline.com will also want you to add these: -->
 
  <meta name="description" content="blah blah">
 
  <meta property="og:type" content="website">
 
  <meta name="twitter:title" content="blah blah">
 
  <meta name="twitter:description" content="blah blah">
 
  <meta name="twitter:image" content="http://something.jpg">
 
```
 
 
Do not forget about file robots.txt and sitemap.xml:
 
 
```
 
// robots.txt can contain this:
 
User-agent: *
 
Allow: /
 
 
Sitemap: http://www.example.com/sitemap.xml
 
```
 
 
```
 
// And file sitemap.xml
 
<?xml version="1.0" encoding="UTF-8"?>
 
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
 
        xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
 
  <url>
 
    <loc>http://example.com/someFile.html</loc>
 
    <image:image>
 
      <image:loc>http://example.com/someImg.jpg</image:loc>
 
    </image:image>
 
  </url> 
 
</urlset> 
 
```
 
 
You can also minify [here](https://www.willpeavy.com/tools/minifier/) or [here](http://minifycode.com/html-minifier/) all your files. Adding "microdata" can help as well, but I have never used it. On the other hand what I do is that I compress images using these two sites [tinyjpg.com](https://tinyjpg.com/) and [tinypng.com](https://tinypng.com/).
 
 
.
 
 
.
 
 
**Other useful links**
 
---
 
 
- [SVG to CSS-background-image convertor](https://websemantics.uk/tools/svg-to-background-image-conversion/)
 
 
.
 
 
.
 
 
**jQuery + draggable/droppable on mobile devices (Android)**
 
---
 
 
JQuery and its UI extension provide drag&drop functionalities, but these do not work on Android or generally on mobile devices. You can use one more dependency called [touch-punch](http://touchpunch.furf.com) to fix the problem. It should be loaded after jQuery and UI.
 
 
```
 
<!-- jQuery + UI -->
 
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
 
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
 
 
<!-- http://touchpunch.furf.com/ -->
 
<!-- Use this file locally -->
 
<script src="./jquery.ui.touch-punch.min.js"></script>
 
```
 
 
And then standard code should work:
 
 
```
 
<!doctype html>
 
 
<html lang="en">
 
  <head>
 
    <meta charset="utf-8">
 
 
    <title>Title</title>
 
 
    <!-- jQuery + UI -->
 
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
 
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
 
 
    <!-- http://touchpunch.furf.com/ -->
 
    <script src="./jquery.ui.touch-punch.min.js"></script>
 
 
    <style>
 
      .draggable {
 
        width: 100px;
 
        height: 100px;
 
        border: 1px solid red;
 
      }
 
 
      .droppable {
 
        width: 300px;
 
        height: 300px;
 
        border: 1px solid blue;
 
      }
 
 
      .over {
 
        background-color: gold;
 
      }
 
    </style>
 
  </head>
 
 
  <body>
 
    <div class="draggable my1">draggable my1</div>
 
    <div class="draggable my2">draggable my2</div>
 
    <div class="droppable myA">droppable myA</div>
 
    <div class="droppable myB">droppable myB</div>
 
  </body>
 
 
 
  <script>
 
    $( function() {
 
 
      // All draggables will return to their original position if not dropped to correct droppable
 
      // ... and will always stay in the area of BODY
 
      $(".draggable").draggable({ revert: "invalid", containment: "body" });
 
 
      // Demonstration of how particular droppables can accept only particular draggables
 
      $( ".droppable.myA" ).droppable({
 
        accept: ".draggable.my1",
 
        drop: function( event, ui ) {
 
 
          // positioning the dropped box into the target area
 
          var dropped = ui.draggable;
 
          var droppedOn = $(this);
 
          $(dropped).detach().css({top: 0,left: 0}).appendTo(droppedOn);    
 
          $(this).removeClass("over");
 
        },
 
        over: function(event, elem) {
 
          $(this).addClass("over");
 
          console.log("over");
 
        },
 
        out: function(event, elem) {
 
          $(this).removeClass("over");
 
        }
 
      });
 
 
      // Demonstration of how particular droppables can accept only particular draggables
 
      $( ".droppable.myB" ).droppable({
 
        accept: ".draggable.my2",
 
        drop: function( event, ui ) {
 
 
          // positioning the dropped box into the target area
 
          var dropped = ui.draggable;
 
          var droppedOn = $(this);
 
          $(dropped).detach().css({top: 0,left: 0}).appendTo(droppedOn);    
 
          $(this).removeClass("over");
 
        },
 
        over: function(event, elem) {
 
          $(this).addClass("over");
 
          console.log("over");
 
        },
 
        out: function(event, elem) {
 
          $(this).removeClass("over");
 
        }
 
      });
 
 
    });
 
  </script>
 
 
</html>
 
```
 
 
.
 
 
.
 
 
**Enhancing Gii**
 
---
 
If you do not like entering long model-paths and controller-paths in CRUD-generator, you can modify text boxes in "\vendor\yiisoft\yii2-gii\src\generators\crud\form.php" and enter default paths and then only manually add the name of the model.
 
 
```php
 
if (!$generator->modelClass) {
 
echo $form->field($generator, 'modelClass')->textInput(['value' => 'app\\models\\']);
 
echo $form->field($generator, 'searchModelClass')->textInput(['value' => 'app\\models\\*Search']);
 
echo $form->field($generator, 'controllerClass')->textInput(['value' => 'app\\controllers\\*Controller']);
 
} else {
 
echo $form->field($generator, 'modelClass');
 
echo $form->field($generator, 'searchModelClass');
 
echo $form->field($generator, 'controllerClass');
 
}
 
```
 
 
.
 
 
.
 
 
**Webproject outsite docroot (htdocs) folder (Windows)**
 
---
 
If you need to store you project for example in folder D:\GIT\EmployerNr1\ProjectNr2, you can. Just modify 2 files and restart Apache (I am using XAMPP under Win):
 
 
- C:\Windows\System32\drivers\etc\hosts
 
 
```
 
127.0.0.1 myFictiveUrl.local
 
```
 
 
- C:\xampp\apache\conf\extra\httpd-vhosts.conf
 
 
```
 
<VirtualHost *:80>
 
  DocumentRoot "D:\GIT\EmployerNr1\ProjectNr2"
 
  ServerName myFictiveUrl.local
 
  ServerAlias myFictiveUrl.local
 
  <Directory "D:\GIT\EmployerNr1\ProjectNr2">
 
    Options Indexes FollowSymLinks
 
    AllowOverride All
 
    Order allow,deny
 
    Allow from all
 
    # New directive needed in Apache 2.4.3:
 
    Require all granted
 
  </Directory>
 
</VirtualHost>
 
```
 
 
You can then use http://myFictiveUrl.local in your browser
 
 
.
 
 
.
 
 
**Modal window + ajax**
 
---
 
Let's have a GridView (list of users) with edit-button which will open the edit-form in a modal window. Once user-detail is changed, ajax validation will be executed. If something is wrong, the field will be highlighted. If everything is OK and saved, modal window will be closed and the GridView will be updated.
 
 
Let's add the button to the GridView in the view **index.php** and let's wrap the GridView into the Pjax. Also ID is added to the GridView so it can be refreshed later via JS:
 
 
```php
 
<?php yii\widgets\Pjax::begin();?>
 
<?= GridView::widget([
 
  'dataProvider' => $dataProvider,
 
  'filterModel' => $searchModel,
 
  'id' => 'user-list-GridView',
 
  'columns' => [
 
    ['class' => 'yii\grid\SerialColumn'],
 
      'id',
 
      'username',
 
      'email:email',
 
      ['class' => 'yii\grid\ActionColumn',
 
        'buttons' => [
 
          'user_ajax_update_btn' => function ($url, $model, $key) {
 
            return Html::a ( '<span class="glyphicon glyphicon-share" aria-hidden="true"></span> ', 
 
  ['user/update', 'id' =>  $model->id], 
 
  ['class' => 'openInModal', 'data-modal-titl' => 'Some text'] 
 
    );
 
          },
 
        ],
 
        'template' => '{update} {view} {delete} {user_ajax_update_btn}'
 
      ],
 
  ],
 
]); ?>
 
<?php yii\widgets\Pjax::end();?>
 
```
 
 
 
Plus add (to the end of this view) following JS code:
 
 
```php
 
<?php
 
// This section can be moved to "\views\layouts\main.php"
 
yii\bootstrap\Modal::begin([
 
  'header' => '<span id="modalTitle">Title</span>',
 
  'id' => 'modalDialog1',
 
  'size' => 'modal-lg',
 
]);
 
echo "<div id='modalContent'></div>";
 
yii\bootstrap\Modal::end();
 
 
$this->registerJs(
 
  "$('a.openInModal').click(function(e){  
 
  e.preventDefault();
 
  $('#modalDialog1 #modalTitle').text('aaa');
 
  $('#modalDialog1').modal('show')
 
    .find('#modalContent')
 
    .load($(this).attr('href'));
 
  return false;
 
  });",
 
  yii\web\View::POS_READY,
 
  'modalHandler'
 
);
 
?>
 
```
 
 
Now we need to modify the **updateAction**:
 
 
```php
 
public function actionUpdate($id)
 
{
 
  $model = $this->findModel($id);
 
 
  if ($model->load(Yii::$app->request->post()) && $model->save()) {
 
    if (Yii::$app->request->isAjax) {
 
      return "<script>"
 
        . "$.pjax.reload({container:'#user-list-GridView'});"
 
        . "$('#modalDialog1').modal('hide');"
 
        . "</script>";
 
    }
 
 
    return $this->redirect(['view', 'id' => $model->id]);
 
  }
 
 
  if (Yii::$app->request->isAjax) {
 
    return $this->renderAjax('update', [
 
      'model' => $model,
 
    ]);
 
  }
 
    
 
  return $this->render('update', [
 
        'model' => $model,
 
  ]);
 
}
 
```
 
 
 
And file \_form.php:
 
 
```php
 
<?php yii\widgets\Pjax::begin([
 
  'id' => 'user-detail-Pjax', 
 
  'enablePushState' => false, 
 
  'enableReplaceState' => false
 
]);  ?>
 
 
<?php $form = ActiveForm::begin([
 
  'id'=>'user-detail-ActiveForm',
 
  'options' => ['data-pjax' => 1 ]
 
  ]); ?>
 
 
<?= $form->field($model, 'username')->textInput(['maxlength' => true]) ?>
 
 
<?= $form->field($model, 'password')->passwordInput(['maxlength' => true]) ?>
 
 
<?= $form->field($model, 'email')->textInput(['maxlength' => true]) ?>
 
 
<?= $form->field($model, 'authKey')->textInput(['maxlength' => true]) ?>
 
 
<div class="form-group">
 
    <?= Html::submitButton(Yii::t('app', 'Save'), ['class' => 'btn btn-success']) ?>
 
</div>
 
 
<?php ActiveForm::end(); ?>
 
 
<?php yii\widgets\Pjax::end() ?>
 
```
 
 
**Simple Bootstrap themes**
 
---
 
There is this page [bootswatch.com](https://bootswatch.com) which provides simple bootstrap themes. It is enough to replace one CSS file - you can do it in file "views/layouts/main.php" just by adding following row before < /head > tag:
 
 
```html
 
<link href="https://bootswatch.com/3/united/bootstrap.min.css" rel="stylesheet">
 
 
</head>
 
```
 
 
Note that currently Yii2 is using Bootstrap3 so when searching for themes, dont forget to switch to section [Bootstrap 3](https://bootswatch.com/3/).
 
 
Important: Yii2 is using navbar with classes "navbar-inverse navbar-fixed-top". If you are using themes from Bootswatch, change the navbar class to "navbar navbar-default navbar-fixed-top" otherwise the top menu-bar will have weird color. This is also done in file "views/layouts/main.php" like this:
 
 
```php
 
    NavBar::begin([
 
        // ...
 
        'options' => [
 
            'class' => 'navbar navbar-default navbar-fixed-top',
 
        ],
 
    ]);
 
```
 
 
Note: If you want to download the theme, you should link it like this:
 
 
```html
 
<link href="<?=Yii::$app->getUrlManager()->getBaseUrl()?>/css/bootstrap-bootswatch-united.min.css" rel="stylesheet">
 
```
 
 
Now you technically do not need the original bootstrap.css file so you can remove it in "basic/config/web.php" by adding the assetManager section to "components":
 
 
```php
 
'components' => [
 
  // https://stackoverflow.com/questions/26734385/yii2-disable-bootstrap-js-jquery-and-css
 
  'assetManager' => [
 
    'bundles' => [
 
'yii\bootstrap\BootstrapAsset' => [
 
  'css' => [],
 
 ],
 
     ],
 
   ],
 
```
 
 
**Yii2 + Composer**
 
---
 
Once composer is installed, you might want to use it to download Yii, but following command might not work:
 
 
```
 
php composer.phar create-project yiisoft/yii2-app-basic basic
 
```
 
 
Change it to:
 
 
```
 
composer create-project yiisoft/yii2-app-basic basic
 
```
 
 
.. and run it. If you are in the desired folder right now, you can use . (dot) instead of the last "word":
 
 
```
 
composer create-project yiisoft/yii2-app-basic .
 
```
 
 
**Using DatePicker**
 
 
Run this command:
 
 
```
 
composer require --prefer-dist yiisoft/yii2-jui
 
```
 
 
and then use this code in your view:
 
 
 
```php
 
<?= $form->field($model, 'date_deadline')->widget(\yii\jui\DatePicker::classname(), [
 
    //'language' => 'en',
 
    'dateFormat' => 'yyyy-MM-dd',
 
    'options' => ['class' => 'form-control']
 
]) ?>
 
```
 
 
Read more at [the official documentation](https://www.yiiframework.com/extension/yiisoft/yii2-jui) and on [GIT](https://github.com/yiisoft/yii2-jui)
 
 
**Favicon**
 
---
 
Favicon is already included, but it nos used in the basic project. Just type this into views/layouts/main.php:
 
 
```html
 
<link rel="icon" type="image/png" sizes="16x16" href="favicon.ico">
 
```
 
 
Or you can use the official yii-favicon:
 
 
```html
 
<link rel="apple-touch-icon" sizes="180x180" href="https://www.yiiframework.com/favico/apple-touch-icon.png">
 
<link rel="icon" type="image/png" sizes="32x32" href="https://www.yiiframework.com/favico/favicon-32x32.png">
 
<link rel="icon" type="image/png" sizes="16x16" href="https://www.yiiframework.com/favico/favicon-16x16.png">
 
```
 
 
**GridView + DatePicker in filter + filter reset**
 
---
 
If you are using DatePicker as described above, you can use it also in GridView as a filter, but it will not work properly. Current filter-value will not be visible and resetting the filter wont be possible. Use following in views/xxx/index.php to solve the issue:
 
 
```php
 
function getDatepickerFilter($searchModel, $attribute) {
 
  $name = basename(get_class($searchModel)) . "[$attribute]";
 
  $result = \yii\jui\DatePicker::widget(['language' => 'en', 'dateFormat' => 'php:Y-m-d', 'name'=>$name, 'value'=>$searchModel->$attribute, 'options' => ['class' => 'form-control'] ]);
 
  if (trim($searchModel->$attribute)!=='') {
 
    $result = '<div style="display:flex;flex-direction:column">' . $result
 
    . '<div class="btn btn-danger btn-xs glyphicon glyphicon-remove" onclick="$(this).prev(\'input\').val(\'\').trigger(\'change\')"></div></div>';
 
  }
 
  return $result;
 
}
 
 
// ...
 
 
<?= GridView::widget([
 
  'dataProvider' => $dataProvider,
 
  'filterModel' => $searchModel,
 
  'columns' => [
 
  // ...
 
  [
 
    'attribute' => 'myDateCol',
 
    'value' => 'myDateCol',
 
    'label'=>'My date label',
 
    'filter' => getDatepickerFilter($searchModel,'myDateCol'),
 
    'format' => 'html'
 
  ],
 
        
 
  // ...
 
        
 
```
 
 
**Drop down list for foreign-key column**
 
--
 
 
Do you need to specify for example currency using a predefined list, but your view contains only a simple text-input where you must manually enter currency_id from table Currency? 
 
 
Read how to enhance it. 
 
 
```php
 
use yii\helpers\ArrayHelper;
 
use app\models\Currency; // My example uses Currency model
 
 
$currencies = Currency::find()->asArray()->all();
 
 
// 'id' = the primary key column
 
// 'name' = the column with text to be dispalyed to user
 
// https://www.yiiframework.com/doc/api/2.0/yii-helpers-basearrayhelper#map()-detail
 
$currencies = ArrayHelper::map($currencies, 'id', 'name'); 
 
 
<?= $form->field($model, 'id_currency')->dropDownList($currencies) ?>
 
```
 
 
Note: In other views you will need models with predefined relations to reach the correct value. Relations can be created using GII (when they are defined in DB) or [manually](https://www.yiiframework.com/doc/guide/2.0/en/db-active-record#relational-data).
 
 
**GridViev - Variable page size**
 
---
 
GridView cannot display DropDownList which could be used by the user to change the number of rows per page. You have to add it manually like this:
 
 
When you are creating a new model using Gii, you can select if you want to create the SearchModel as well. Do it, it is usefull for example in this situation. Then add following rows to the model:
 
 
```php
 
// file models/InvoiceSearch.php
 
 
use yii\helpers\Html; // add this row
 
 
class InvoiceSearch extends Invoice
 
{
 
  public $pageSize = null // add this row
 
  // ...
 
  
 
  // This method already exists:
 
  public function rules()
 
  {
 
    return [ // ...
 
      ['pageSize', 'safe'], // add this row
 
      // ...
 
  
 
  // Add this function:
 
  public function getPageSizeDropDown($htmlOptions = [], $prefixHtml = '', $suffixHtml = '', $labelPrefix = '') {
 
    return $prefixHtml . Html::activeDropDownList($this, 'pageSize',
 
      [
 
        10 => $labelPrefix.'10', 
 
        20 => $labelPrefix.'20', 
 
        50 => $labelPrefix.'50', 
 
        100 => $labelPrefix.'100', 
 
        150 => $labelPrefix.'150', 
 
        200 => $labelPrefix.'200', 
 
        300 => $labelPrefix.'300', 
 
        500 => $labelPrefix.'500', 
 
        1000 => $labelPrefix.'1000'
 
      ],$htmlOptions ) . $suffixHtml;
 
    }
 
 
    // Add this function:
 
    public function getPageSizeDropDownID($prefix = '#') {
 
      return $prefix . Html::getInputId($this, 'pageSize');
 
    }
 
    
 
    // This method already exists:
 
    public function search($params)
 
    {
 
        // Remember to call load() first and then you can work with pageSize
 
        $this->load($params);
 
        
 
        // Add following rows:
 
        if (!isset($this->pageSize)) {
 
          // Here we make sure that the dropDownLst will have correct value preselected
 
          $this->pageSize = $dataProvider->pagination->defaultPageSize;
 
        } 
 
        $dataProvider->pagination->pageSize = (int)$this->pageSize; 
 
        
 
```
 
 
And then in your views/xxx/index.php use following:
 
 
```php
 
$pageSizeDropDown = $searchModel->getPageSizeDropDown(['class' => 'form-control', 'style'=>'width: 20rem'],'','','Rows per page: ');
 
 
echo GridView::widget([
 
  'dataProvider' => $dataProvider,
 
  'filterModel' => $searchModel,
 
  'layout'=>'{summary}<br>{items}<br><div style="display:flex; background-color: #f9f9f9; padding: 0px 3rem;"><div style="flex-grow: 2;">{pager}</div><div style="align-self:center;">'.$pageSizeDropDown.'</div></div>',
 
  'pager' => [ 'maxButtonCount' => 20 ],
 
  
 
  'filterSelector' => $searchModel->getPageSizeDropDownID(),
 
  // filterSelector is the core solution of this problem. It refreshes the grid.
 
```
 
 
 
4 0
3 followers
Viewed: 44 867 times
Version: 2.0
Category: Tutorials
Written by: rackycz
Last updated by: rackycz
Created on: Sep 19, 2019
Last updated: a day ago
Update Article

Revisions

View all history