Yii v2 snippet guide

You are viewing revision #201 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version or see the changes made in this revision.

« previous (#200)next (#202) »

  1. Intro
  2. Prerequisities
  3. Yii demo app + GitLab
  4. Automatical copying from GitLab to FTP
  5. User management + DB creation + login via DB
  6. i18n translations
  7. Switching languages + session + lang-dropdown in the top menu
  8. Simple access rights
  9. Nice URLs
  10. How to redirect web to subfolder /web
  11. Auto redirection from login to desired URL
  12. What to change when exporting to the Internet
  13. Saving contact inqueries into DB
  14. Tests - unit + opa
  15. Adding a google-like calendar
  16. Scenarios - UNKNOWN SCENARIO EXCEPTION
  17. Richtext / wysiwyg HTML editor - Summernote
  18. SEO optimization
  19. Other useful links
  20. jQuery + draggable/droppable on mobile devices (Android)
  21. Enhancing Gii
  22. Webproject outsite docroot (htdocs) folder (Windows)
  23. Modal window + ajax
  24. Simple Bootstrap themes
  25. Yii2 + Composer
  26. Favicon
  27. GridView + DatePicker in filter + filter reset

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:

... 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 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

You should also download the Yii basic demo application 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:

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. You will probably need a SSH certificate which can be generated like this using PuTTYgen. When I work with Git I use TortoiseGIT which integrates all git functionalities into the context menu in Windows File Explorer.

First go to GitLab web and create a new project. 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:

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 link 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.

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.
  • Plus we add 2 methods because of the default LoginForm and 1 validator.
<?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:

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 ... 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:

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:

    '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

You surely saw that in views and models there are translated-texts saved like this:

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
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 :-)

We must change the language ... For now let's do it in a primitive and permanent way again in file config/web.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
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:

    $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

    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:

    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:

    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

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:


// 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.

.

.

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.

My problem was that images were not displayed when I enabled nice URLs. Smilar discussion here.

// 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 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:

$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

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:
    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:
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
    <p>
    Note that if you turn on the Yii debugger ...
    

Then some security - filtering users in the new ContactController:

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.

Great demo which you can just copy and paste:

/*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:
$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.

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

   '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. 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 ...

$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 is the simplest addon available:

// 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:

<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:

DetailView::widget([
  'model' => $model,
  'attributes' => [
    // ...
    'body:html',
  ],
])

... to discover all possible formatters, check all asXXX() functions on this 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, 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. They are basicly the same. Read more here. You can test them at 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.

<!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 or here 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 and tinypng.com.

.

.

Other useful links

.

.

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 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.

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 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
// 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:

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 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 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:

<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.

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:

    NavBar::begin([
        // ...
        'options' => [
            'class' => 'navbar navbar-default navbar-fixed-top',
        ],
    ]);

Note: If you want to download the theme, you should link it like this:

<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":

'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:

<?= $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 and on GIT

Favicon

Favicon is already included, but it nos used in the basic project. Just type this into views/layouts/main.php:

<link rel="icon" type="image/png" sizes="16x16" href="favicon.ico">

Or you can use the official yii-favicon:

<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:

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'
            ],
        
        // ...
        
7 0
4 followers
Viewed: 276 090 times
Version: 2.0
Category: Tutorials
Written by: rackycz
Last updated by: rackycz
Created on: Sep 19, 2019
Last updated: a year ago
Update Article

Revisions

View all history

Related Articles