Live News for Yii Framework News, fresh extensions and wiki articles about Yii framework. Sun, 03 Dec 2023 17:03:34 +0000 Zend_Feed_Writer 2 (http://framework.zend.com) https://www.yiiframework.com/ [news] Yii Factory 1.2 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/592/yii-factory-1-2 https://www.yiiframework.com/news/592/yii-factory-1-2 vjik vjik

Version 1.2 of Yii Factory package is released. There are one improvement, the container is made optional.

]]>
0
[news] Yii HTML 3.3 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/591/yii-html-3-3 https://www.yiiframework.com/news/591/yii-html-3-3 vjik vjik

Version 3.3 of Yii HTML package is released. There are some improvements and fixes:

  • added class for tag html and method Html::html();
  • replaced constant PHP_EOL to "\n";
  • don't add "class" attribute in Html::addCssClass() if passed array contains null classes only.
]]>
0
[wiki] How to add a DropDown Language Picker to the Menu Fri, 24 Nov 2023 21:03:39 +0000 https://www.yiiframework.com/wiki/2577/how-to-add-a-dropdown-language-picker-to-the-menu https://www.yiiframework.com/wiki/2577/how-to-add-a-dropdown-language-picker-to-the-menu JQL JQL

How To Add Internationalisation to the Menu in Yii2 Basic (Bootstrap 5)

  1. Create the required Files
  2. Edit the /config/web.php file
  3. Edit all the files in the "views" folder and any sub folders
  4. Create the texts to be translated
  5. Create a Menu Item (Dropdown) to Change the Language
  6. Optional Items

Yii comes with internationalisation (i18n) "out of the box". There are instructions in the manual as to how to configure Yii to use i18n, but little information all in one place on how to fully integrate it into the bootstrap menu. This document attempts to remedy that.

Ensure that your system is set up to use i18n. From the Yii2 Manual:

Yii uses the PHP intl extension to provide most of its I18N features, such as the date and number formatting of the yii\i18n\Formatter class and the message formatting using yii\i18n\MessageFormatter. Both classes provide a fallback mechanism when the intl extension is not installed. However, the fallback implementation only works well for English target language. So it is highly recommended that you install intl when I18N is needed.

Create the required Files

First you need to create a configuration file.

Decide where to store it (e.g. in the ./messages/ directory with the name create_i18n.php). Create the directory in the project then issue the following command from Terminal (Windows: CMD) from the root directory of your project:

./yii message/config-template ./messages/create_i18n.php

or for more granularity:

./yii message/config --languages=en-US --sourcePath=@app --messagePath=messages ./messages/create_i18n.php

In the newly created file, alter (or create) the array of languages to be translated:

  // array, required, list of language codes that the extracted messages
  // should be translated to. For example, ['zh-CN', 'de'].
  'languages' => [
    'en-US',
    'fr',
    'pt'
  ],

If necessary, change the root directory in create_i18n.php to point to the messages directory - the default is messages. Note, if the above file is in the messages directory (recommended) then don't alter this 'messagePath' => __DIR__,. If you alter the directory for messages to, say, /config/ (not a good idea) you can use the following:

  // Root directory containing message translations.
  'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'config',

The created file should look something like this after editing the languages you need:

<?php

return [
  // string, required, root directory of all source files
  'sourcePath' => __DIR__ . DIRECTORY_SEPARATOR . '..',
  // array, required, list of language codes (in alphabetical order) that the extracted messages
  // should be translated to. For example, ['zh-CN', 'de'].
  'languages' => [
    // to localise a particular language use the language code followed by the dialect in CAPS
    'en-US',  // USA English
    'es',
    'fr',
    'it',
    'pt',
  ],
  /* 'languages' => [
    'af', 'ar', 'az', 'be', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'el', 'es', 'et', 'fa', 'fi', 'fr', 'he', 'hi',
    'pt-BR', 'ro', 'hr', 'hu', 'hy', 'id', 'it', 'ja', 'ka', 'kk', 'ko', 'kz', 'lt', 'lv', 'ms', 'nb-NO', 'nl',
    'pl', 'pt', 'ru', 'sk', 'sl', 'sr', 'sr-Latn', 'sv', 'tg', 'th', 'tr', 'uk', 'uz', 'uz-Cy', 'vi', 'zh-CN',
    'zh-TW'
    ], */
  // string, the name of the function for translating messages.
  // Defaults to 'Yii::t'. This is used as a mark to find the messages to be
  // translated. You may use a string for single function name or an array for
  // multiple function names.
  'translator' => ['\Yii::t', 'Yii::t'],
  // boolean, whether to sort messages by keys when merging new messages
  // with the existing ones. Defaults to false, which means the new (untranslated)
  // messages will be separated from the old (translated) ones.
  'sort' => false,
  // boolean, whether to remove messages that no longer appear in the source code.
  // Defaults to false, which means these messages will NOT be removed.
  'removeUnused' => false,
  // boolean, whether to mark messages that no longer appear in the source code.
  // Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks.
  'markUnused' => true,
  // array, list of patterns that specify which files (not directories) should be processed.
  // If empty or not set, all files will be processed.
  // See helpers/FileHelper::findFiles() for pattern matching rules.
  // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
  'only' => ['*.php'],
  // array, list of patterns that specify which files/directories should NOT be processed.
  // If empty or not set, all files/directories will be processed.
  // See helpers/FileHelper::findFiles() for pattern matching rules.
  // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
  'except' => [
    '.*',
    '/.*',
    '/messages',
    '/migrations',
    '/tests',
    '/runtime',
    '/vendor',
    '/BaseYii.php',
  ],
  // 'php' output format is for saving messages to php files.
  'format' => 'php',
  // Root directory containing message translations.
  'messagePath' => __DIR__,
  // boolean, whether the message file should be overwritten with the merged messages
  'overwrite' => true,
  /*
    // File header used in generated messages files
    'phpFileHeader' => '',
    // PHPDoc used for array of messages with generated messages files
    'phpDocBlock' => null,
   */

  /*
    // Message categories to ignore
    'ignoreCategories' => [
    'yii',
    ],
   */

  /*
    // 'db' output format is for saving messages to database.
    'format' => 'db',
    // Connection component to use. Optional.
    'db' => 'db',
    // Custom source message table. Optional.
    // 'sourceMessageTable' => '{{%source_message}}',
    // Custom name for translation message table. Optional.
    // 'messageTable' => '{{%message}}',
   */

  /*
    // 'po' output format is for saving messages to gettext po files.
    'format' => 'po',
    // Root directory containing message translations.
    'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
    // Name of the file that will be used for translations.
    'catalog' => 'messages',
    // boolean, whether the message file should be overwritten with the merged messages
    'overwrite' => true,
   */
];

Edit the /config/web.php file

In the web.php file, below 'id' => 'basic', add:

  'language' => 'en',
  'sourceLanguage' => 'en',

Note: you should always use the 'sourceLanguage' => 'en' as it is, usually, easier and cheaper to translate from English into another language. If the sourceLanguage is not set it defaults to 'en'.

Add the following to the 'components' => [...] section:

    'i18n' => [
      'translations' => [
        'app*' => [
          'class' => 'yii\i18n\PhpMessageSource',  // Using text files (usually faster) for the translations
          //'basePath' => '@app/messages',  // Uncomment and change this if your folder is not called 'messages'
          'sourceLanguage' => 'en',
          'fileMap' => [
            'app' => 'app.php',
            'app/error' => 'error.php',
          ],
          //  Comment out in production version
          //  'on missingTranslation' => ['app\components\TranslationEventHandler', 'handleMissingTranslation'],
        ],
      ],
    ],

Edit all the files in the "views" folder and any sub folders

Now tell Yii which text you want to translate in your view files. This is done by adding Yii::t('app', 'text to be translated') to the code.

For example, in /views/layouts/main.php, change the menu labels like so:

    'items' => [
          //  ['label' => 'Home', 'url' => ['/site/index']],	// Orignal code
          ['label' => Yii::t('app', 'Home'), 'url' => ['/site/index']],
          ['label' => Yii::t('app', 'About'), 'url' => ['/site/about']],
          ['label' => Yii::t('app', 'Contact'), 'url' => ['/site/contact']],
          Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'Login'), 'url' => ['/site/login']] : '<li class="nav-item">'
            . Html::beginForm(['/site/logout'])
            . Html::submitButton(
             // 'Logout (' . Yii::$app->user->identity->username . ')', // change this line as well to the following:
              Yii::t('app', 'Logout ({username}'), ['username' => Yii::$app->user->identity->username]),
              ['class' => 'nav-link btn btn-link logout']
            )
            . Html::endForm()
            . '</li>',
        ],

Create the texts to be translated

To create the translation files, run the following, in Terminal, from the root directory of your project:

./yii message ./messages/create_i18n.php

Now, get the messages translated. For example in the French /messages/fr/app.php

  'Home' => 'Accueil',
  'About' => 'À propos',

Create a Menu Item (Dropdown) to Change the Language

This takes a number of steps.

1. Create an array of languages required

A key and a name is required for each language.

The key is the ICU language code in lowercase (with optional country code in uppercase) e.g.

French: fr or French Canada: fr-CA

Portuguese: pt or Portuguese Brazil: pt-BR

The name is the name of the language in that language. e.g. for French: 'Français', for Japanese: '日本の'. This is important as the user may not understand the browser's current language.

In /config/params.php create an array named languages with the languages required. For example:

  /* 		List of languages and their codes
   *
   * 		format:
   * 		'Language Code' => 'Language Name',
   * 		e.g.
   * 		'fr' => 'Français',
   *
   * 		please use alphabetical order of language code
   * 		Use the language name in the "user's" Language
   *            e.g.
   *            'ja' => '日本の',
   */
  'languages' => [
//    'da' => 'Danske',
//    'de' => 'Deutsche',
//    'en' => 'English', // NOT REQUIRED the sourceLanguage (i.e. the default)
    'en-GB' => 'British English',
    'en-US' => 'American English',
    'es' => 'Español',
    'fr' => 'Français',
    'it' => 'Italiano',
//    'ja' => '日本の',  // Japanese with the word "Japanese" in Kanji
//    'nl' => 'Nederlandse',
//    'no' => 'Norsk',
//    'pl' => 'Polski',
    'pt' => 'Português',
//    'ru' => 'Русский',
//    'sw' => 'Svensk',
//    'zh' => '中国的',
  ],
2. Create an Action

In /controllers/SiteController.php, the default controller, add an "Action" named actionLanguage(). This "Action" changes the language and sets a cookie so the browser "remembers" the language for page requests and return visits to the site.

  /**
   * Called by the ajax handler to change the language and
   * Sets a cookie based on the language selected
   *
   */
  public function actionLanguage()
  {
    $lang = Yii::$app->request->post('lang');
    // If the language "key" is not NULL and exists in the languages array in params.php, change the language and set the cookie
    if ($lang !== NULL && array_key_exists($lang, Yii::$app->params['languages']))
    {
      $expire = time() + (60 * 60 * 24 * 365); //  1 year - alter accordingly
      Yii::$app->language = $lang;
      $cookie = new yii\web\Cookie([
        'name' => 'lang',
        'value' => $lang,
        'expire' => $expire,
      ]);
      Yii::$app->getResponse()->getCookies()->add($cookie);
    }
    Yii::$app->end();
  }

Remember to set the method to POST. In behaviors(), under actions, set 'language' => ['post'], like so:

      'verbs' => [
        'class' => VerbFilter::class,
        'actions' => [
          'logout' => ['post'],
          'language' => ['post'],
        ],
      ],
3. Create a Language Handler

Make sure that the correct language is served for each request.

In the /components/ directory, create a file named: LanguageHandler.php and add the following code to it:

<?php

/*
 * Copyright ©2023 JQL all rights reserved.
 * http://www.jql.co.uk
 */
/*
  Created on : 19-Nov-2023, 13:23:54
  Author     : John Lavelle
  Title      : LanguageHandler
 */

namespace app\components;

use yii\helpers\Html;

class LanguageHandler extends \yii\base\Behavior
{

	public function events()
	{
		return [\yii\web\Application::EVENT_BEFORE_REQUEST => 'handleBeginRequest'];
	}

	public function handleBeginRequest($event)
	{
		if (\Yii::$app->getRequest()->getCookies()->has('lang') && array_key_exists(\Yii::$app->getRequest()->getCookies()->getValue('lang'), \Yii::$app->params['languages']))
		{
      //  Get the language from the cookie if set
			\Yii::$app->language = \Yii::$app->getRequest()->getCookies()->getValue('lang');
		}
		else
		{
			//	Use the browser language - note: some systems use an underscore, if used, change it to a hyphen
			\Yii::$app->language = str_replace('_', '-', HTML::encode(locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE'])));
		}
	}

}

/* End of file LanguageHandler.php */
/* Location: ./components/LanguageHandler.php */
4. Call LanguageHandler.php from /config/web.php

"Call" the LanguageHandler.php file from /config/web.php by adding the following to either just above or just below 'params' => $params,

  //	Update the language on selection
  'as beforeRequest' => [
    'class' => 'app\components\LanguageHandler',
  ],
5. Add the Language Menu Item to /views/layouts/main.php

main.php uses Bootstrap to create the menu. An item (Dropdown) needs to be added to the menu to allow the user to select a language.

Add use yii\helpers\Url; to the "uses" section of main.php.

Just above echo Nav::widget([...]) add the following code:

// Get the languages and their keys, also the current route
      foreach (Yii::$app->params['languages'] as $key => $language)
      {
        $items[] = [
          'label' => $language, // Language name in it's language - already translated
          'url' => Url::to(['site/index']), // Route
          'linkOptions' => ['id' => $key, 'class' => 'language'], // The language "key"
        ];
      }

In the section:

echo Nav::widget([...])`

between

'options' => ['class' => 'navbar-nav ms-auto'], // ms-auto aligns the menu right`

and

'items' => [...]

add:

'encodeLabels' => false, // Required to enter HTML into the labels

like so:

      echo Nav::widget([
        'options' => ['class' => 'navbar-nav ms-auto'], // ms-auto aligns the menu right
        'encodeLabels' => false, // Required to enter HTML into the labels
        'items' => [
          ['label' => Yii::t('app', 'Home'), 'url' => ['/site/index']],
        ...

Now add the Dropdown. This can be placed anywhere in 'items' => [...].

// Dropdown Nav Menu: https://www.yiiframework.com/doc/api/2.0/yii-widgets-menu
        [
          'label' => Yii::t('app', 'Language')),
          'url' => ['#'],
          'options' => ['class' => 'language', 'id' => 'languageTop'],
          'encodeLabels' => false, // Optional but required to enter HTML into the labels for images
          'items' => $items, // add the languages into the Dropdown
        ],

The code in main.php for the NavBar should look something like this:

      NavBar::begin([
        'brandLabel' => Yii::$app->name,  // set in /config/web.php
        'brandUrl' => Yii::$app->homeUrl,
        'options' => ['class' => 'navbar-expand-md navbar-dark bg-dark fixed-top']
      ]);
      // Get the languages and their keys, also the current route
      foreach (Yii::$app->params['languages'] as $key => $language)
      {
        $items[] = [
          'label' => $language, // Language name in it's language
          'url' => Url::to(['site/index']), // Current route so the page refreshes
          'linkOptions' => ['id' => $key, 'class' => 'language'], // The language key
        ];
      }
      echo Nav::widget([
        'options' => ['class' => 'navbar-nav ms-auto'], // ms-auto aligns the menu right
        'encodeLabels' => false, // Required to enter HTML into the labels
        'items' => [
          ['label' => Yii::t('app', 'Home'), 'url' => ['/site/index']],
          ['label' => Yii::t('app', 'About'), 'url' => ['/site/about']],
          ['label' => Yii::t('app', 'Contact'), 'url' => ['/site/contact']],
          // Dropdown Nav Menu: https://www.yiiframework.com/doc/api/2.0/yii-widgets-menu
          [
            'label' => Yii::t('app', 'Language') ,
            'url' => ['#'],
            'options' => ['class' => 'language', 'id' => 'languageTop'],
            'encodeLabels' => false, // Required to enter HTML into the labels
            'items' => $items, // add the languages into the Dropdown
          ],
          Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'Login'), 'url' => ['/site/login']] : '<li class="nav-item">'
            . Html::beginForm(['/site/logout'])
            . Html::submitButton(
//              'Logout (' . Yii::$app->user->identity->username . ')',
              Yii::t('app', 'Logout ({username})', ['username' => Yii::$app->user->identity->username]),
              ['class' => 'nav-link btn btn-link logout']
            )
            . Html::endForm()
            . '</li>',
        ],
      ]);
      NavBar::end();

If country flags or images are required next to the language name see Optional Items at the end of this document.

6. Trigger the Language change with an Ajax call

To call the Language Action actionLanguage() make an Ajax call in a JavaScript file.

Create a file in /public_html/js/ named language.js.

Add the following code to the file:

/*
 * Copyright ©2023 JQL all rights reserved.
 * http://www.jql.co.uk
 */

/**
 * Set the language
 *
 * @returns {undefined}
 */
$(function () {
  $(document).on('click', '.language', function (event) {
    event.preventDefault();
    let lang = $(this).attr('id');  // Get the language key
    /* if not the top level, set the language and reload the page */
    if (lang !== 'languageTop') {
      $.post(document.location.origin + '/site/language', {'lang': lang}, function (data) {
        location.reload(true);
      });
    }
  });
});

To add the JavaScript file to the Assets, alter /assets/AppAsset.php in the project directory. In public $js = [] add 'js/language.js', like so:

     public $js = [
       'js/language.js',
     ];

Internationalisation should now be working on your project.

Optional Items

The following are optional but may help both you and/or the user.

1. Check for Translations

Yii can check whether a translation is present for a particular piece of text in a Yii::t('app', 'text to be translated') block.

There are two steps:

A. In /config/web.php uncomment the following line:

  //  'on missingTranslation' => ['app\components\TranslationEventHandler', 'handleMissingTranslation'],

B. Create a TranslationEventHandler:

In /components/ create a file named: TranslationEventHandler.php and add the following code to it:


<?php

/**
 * TranslationEventHandler
 *
 * @copyright © 2023, John Lavelle  Created on : 14 Nov 2023, 16:05:32
 *
 *
 * Author     : John Lavelle
 * Title      : TranslationEventHandler
 */
// Change the Namespace (app, frontend, backend, console etc.) if necessary (default in Yii Basic is "app").

namespace app\components;

use yii\i18n\MissingTranslationEvent;

/**
 * TranslationEventHandler
 *
 *
 * @author John Lavelle
 * @since 1.0 // Update version number
 */
class TranslationEventHandler
{

  /**
   * Adds a message to missing translations in Development Environment only
   *
   * @param MissingTranslationEvent $event
   */
  public static function handleMissingTranslation(MissingTranslationEvent $event)
  {
    // Only check in the development environment
    if (YII_ENV_DEV)
    {
      $event->translatedMessage = "@MISSING: {$event->category}.{$event->message} FOR LANGUAGE {$event->language} @";
    }
  }
}

If there is a missing translation, the text is replaced with a message similar to the following text:

@MISSING: app.Logout (John) FOR LANGUAGE fr @

Here Yii has found that there is no French translation for:

Yii::t('app', 'Logout ({username})', ['username' => Yii::$app->user->identity->username]),
2. Add Country Flags to the Dropdown Menu

This is very useful and recommended as it aids the User to locate the correct language. There are a number of steps for this.

a. Create images of the flags.

The images should be 25px wide by 15px high. The images must have the same name as the language key in the language array in params.php. For example: fr.png or en-US.png. If the images are not of type ".png" change the code in part b. below to the correct file extension.

Place the images in a the directory /public_html/images/flags/.

b. Alter the code in /views/layouts/main.php so that the code for the "NavBar" reads as follows:

<header id="header">
      <?php
      NavBar::begin([
        'brandLabel' => Yii::$app->name,
        'brandUrl' => Yii::$app->homeUrl,
        'options' => ['class' => 'navbar-expand-md navbar-dark bg-dark fixed-top']
      ]);
      // Get the languages and their keys, also the current route
      foreach (Yii::$app->params['languages'] as $key => $language)
      {
        $items[] = [
	// Display the image before the language name
          'label' => Html::img('/images/flags/' . $key . '.png', ['alt' => 'flag ' . $language, 'class' => 'inline-block align-middle', 'title' => $language,]) . ' ' . $language, // Language name in it's language
          'url' => Url::to(['site/index']), // Route
          'linkOptions' => ['id' => $key, 'class' => 'language'], // The language key
        ];
      }
      echo Nav::widget([
        'options' => ['class' => 'navbar-nav ms-auto'], // ms-auto aligns the menu right
        'encodeLabels' => false, // Required to enter HTML into the labels
        'items' => [
          ['label' => Yii::t('app', 'Home'), 'url' => ['/site/index']],
          ['label' => Yii::t('app', 'About'), 'url' => ['/site/about']],
          ['label' => Yii::t('app', 'Contact'), 'url' => ['/site/contact']],
          // Dropdown Nav Menu: https://www.yiiframework.com/doc/api/2.0/yii-widgets-menu
          [
	  // Display the current language "flag" after the Dropdown title (before the caret)
            'label' => Yii::t('app', 'Language') . ' ' . Html::img('@web/images/flags/' . Yii::$app->language . '.png', ['class' => 'inline-block align-middle', 'title' => Yii::$app->language]),
            'url' => ['#'],
            'options' => ['class' => 'language', 'id' => 'languageTop'],
            'encodeLabels' => false, // Required to enter HTML into the labels
            'items' => $items, // add the languages into the Dropdown
          ],
          Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'Login'), 'url' => ['/site/login']] : '<li class="nav-item">'
            . Html::beginForm(['/site/logout'])
            . Html::submitButton(
//              'Logout (' . Yii::$app->user->identity->username . ')',
              Yii::t('app', 'Logout ({username})', ['username' => Yii::$app->user->identity->username]),
              ['class' => 'nav-link btn btn-link logout']
            )
            . Html::endForm()
            . '</li>',
        ],
      ]);
      NavBar::end();
      ?>
    </header>

That's it! Enjoy...

For further reading and information see:

Yii2 Internationalization Tutorial

PHP intl extensions

If you use this code, please credit me as follows:

Internationalization (i18n) Menu code provided by JQL, https://visualaccounts.co.uk ©2023 JQL

Licence (BSD-3-Clause Licence)

Copyright Notice

>Internationalization (i18n) Menu code provided by JQL, https://visualaccounts.co.uk ©2023 JQL all rights reserved

Redistribution and use in source and binary forms with or without modification are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

Neither the names of John Lavelle, JQL, Visual Accounts nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

"ALL JQL CODE & SOFTWARE INCLUDING WORLD WIDE WEB PAGES (AND THOSE OF IT'S AUTHORS) ARE SUPPLIED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE AUTHOR AND PUBLISHER AND THEIR AGENTS SPECIFICALLY DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. WITH RESPECT TO THE CODE, THE AUTHOR AND PUBLISHER AND THEIR AGENTS SHALL HAVE NO LIABILITY WITH RESPECT TO ANY LOSS OR DAMAGE DIRECTLY OR INDIRECTLY ARISING OUT OF THE USE OF THE CODE EVEN IF THE AUTHOR AND/OR PUBLISHER AND THEIR AGENTS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. WITHOUT LIMITING THE FOREGOING, THE AUTHOR AND PUBLISHER AND THEIR AGENTS SHALL NOT BE LIABLE FOR ANY LOSS OF PROFIT, INTERRUPTION OF BUSINESS, DAMAGE TO EQUIPMENT OR DATA, INTERRUPTION OF OPERATIONS OR ANY OTHER COMMERCIAL DAMAGE, INCLUDING BUT NOT LIMITED TO DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL OR OTHER DAMAGES."

]]>
0
[news] Yii HTML 3.2 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/590/yii-html-3-2 https://www.yiiframework.com/news/590/yii-html-3-2 vjik vjik

Version 3.2 of Yii HTML package is released. There are some improvements and fixes:

  • added $attributes parameter to Html::ul() and Html::ol();
  • allowed pass null as class to Html::addCssClass(), nulled classes will be ignored;
  • fixed loss of keys for named class in Html::addCssClass() when class in passed options is a string.
]]>
0
[extension] slideradmin Tue, 21 Nov 2023 11:20:25 +0000 https://www.yiiframework.com/extension/slideradmin https://www.yiiframework.com/extension/slideradmin pravi pravi

This is an application template created using yii2 basic application template to demonstrate the usage of my extention slideradmin

Installation

  1. Download the repo from Github and extract its contents into any folder.
  2. Run composer update command.
  3. Run migration command: php yii migrate --migrationPath="@vendor/siripravi/yii2-slideradmin/migrations"
  4. Done.
]]>
0
[news] Yii Config 1.4 Fri, 17 Nov 2023 12:05:52 +0000 https://www.yiiframework.com/news/589/yii-config-1-4 https://www.yiiframework.com/news/589/yii-config-1-4 vjik vjik

Yii Config package got a minor version.

In this version we add new option package-types that define package types for process by composer plugin. By default, it is "library" and "composer-plugin". You can override default value by own types:

"extra": {
    "config-plugin-options": {
        "package-types": ["library", "my-extension"]
    }
}
]]>
0
[news] Yii Widget 2.1 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/588/yii-widget-2-1 https://www.yiiframework.com/news/588/yii-widget-2-1 vjik vjik

Minor version of Yii Widget package is released.

  • Added widget themes.
  • Added protected method Widget::getThemeConfig() that allows to implement a logic of configuring a theme.
  • Fixed merge constructor arguments with array definition configuration into Widget::widget().
]]>
0
[news] Yii 1.1.29 is released Tue, 14 Nov 2023 09:46:45 +0000 https://www.yiiframework.com/news/587/yii-1-1-29-is-released https://www.yiiframework.com/news/587/yii-1-1-29-is-released marcovtwout marcovtwout

We are very pleased to announce that Yii Framework version 1.1.29 is released. You can download it at yiiframework.com/download/.

Yii 1.1.29 improves compatibility with PHP 8.2 and includes a security improvement. See the security advisory for more information.

This release is a release of Yii 1.1, which has reached maintenance mode and will only receive security and compatibility fixes. For the complete list of changes in this release, please see the change log. For upgrading, always make sure to read the upgrade instructions.

We would like to express our gratitude to all contributors who have spent their precious time helping improve Yii and made this release possible.

]]>
0
[news] Yii DB and drivers updates Sun, 12 Nov 2023 16:24:47 +0000 https://www.yiiframework.com/news/586/yii-db-and-drivers-updates https://www.yiiframework.com/news/586/yii-db-and-drivers-updates vjik vjik

Yii Database package was tagged along with its drivers.

Yii Database 1.2

  • Deprecate TableSchemaInterface::compositeForeignKey() and SchemaInterface::TYPE_JSONB
  • Enhanced documentation of batchInsert() and update() methods of DMLQueryBuilderInterface interface
  • Refactor Quoter
  • Move methods from concrete Command class to AbstractPdoCommand class
  • Typecast values in AbstractDMLQueryBuilder::batchInsert() if column names with table name and brackets
  • Typecast values in AbstractDMLQueryBuilder::batchInsert() if values with string keys
  • Fix collected debug actions
  • Fix Quoter::quoteTableName() for sub-query with alias
  • Quote aliases of CTE in WITH queries
  • Fix AbstractDMLQueryBuilder::batchInsert() for values as associative arrays

Yii DB PostgreSQL Driver 1.2

  • Fix incorrect convert string value for BIT type
  • Fix retrieving sequence name from default value
  • Refactor JsonExpressionBuilder, ArrayExpressionBuilder and ColumnSchema
  • Refactor related with Yii DB 1.2

Yii DB MySQL Driver 1.1

  • Remove QueryBuilder::getColumnType() child method as legacy code
  • Refactor insert default values
  • Refactor related with Yii DB 1.2

Yii DB SQLite Driver 1.1

  • Support json type
  • Fix foreign keys: support multiple foreign keys referencing to one table and possible null columns for reference
  • Refactor related with Yii DB 1.2

Yii DB MSSQL Server Driver 1.1

  • Remove RECURSIVE expression from CTE queries
  • Fix type boolean
  • Fix DDLQueryBuilder::alterColumn() for columns with default null
  • Refactor related with Yii DB 1.2

Yii DB Oracle Driver 1.2

  • Improve column type detection
  • Remove RECURSIVE expression from CTE queries
  • Refactor related with Yii DB 1.2
]]>
0
[news] Yii Event 2.1.0 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/585/yii-event-2-1-0 https://www.yiiframework.com/news/585/yii-event-2-1-0 rustamwin rustamwin

Minor version of Yii Event package was released. This release adds the ability to configure the events configuration group name in params.

]]>
0
[news] Yii Auth 3.1 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/584/yii-auth-3-1 https://www.yiiframework.com/news/584/yii-auth-3-1 vjik vjik

Minor version of Yii Auth package was released.

In this release:

  • Added memoization for WildcardPattern in Authentication middleware.
  • Added Language JetBrains attribute to $pattern property in HttpHeader::withPattern() that enables syntax highlighting for this property in PhpStorm.
  • Fixed bug with processing non-ASCII paths in the Authentication middleware.
  • Added debug collector for yiisoft/yii-debug.
  • Bumped required PHP version to 8.0.
  • Updated yiisoft/http dependency and added missed psr/http-factory dependency.
]]>
0
[extension] nicksdr/nkchartjs Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/nicksdr/nkchartjs https://www.yiiframework.com/extension/nicksdr/nkchartjs CarlosQS CarlosQS

nkchartjs

]]>
0
[news] Yii Strings 2.3 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/583/yii-strings-2-3 https://www.yiiframework.com/news/583/yii-strings-2-3 vjik vjik

Minor version of Yii Strings package was released.

In this release added stringable object support to NumericHelper::normalize().

]]>
0
[news] Yii Cache File Handler 3.1 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/582/yii-cache-file-handler-3-1 https://www.yiiframework.com/news/582/yii-cache-file-handler-3-1 vjik vjik

Minor version of Yii Cache File Handler package is released.

  • Added optional parameter $directoryMode to FileCache constructor and deprecate withDirectoryMode() method.

  • Minor refactoring with PHP 8 features usage.

]]>
0
[news] Yii VarDumper 1.7 Sun, 08 Oct 2023 11:36:51 +0000 https://www.yiiframework.com/news/581/yii-vardumper-1-7 https://www.yiiframework.com/news/581/yii-vardumper-1-7 vjik vjik

Minor version of Yii VarDumper package is released.

  • Add a middleware handler to control dumps' output destination and his implementations:

    • EchoHandler — uses echo to write to stdout stream (used by default);
    • StreamHandler — uses ext-sockets to sent dumps encoded with json_encode to a UDP socket;
    • CompositeHandler — helpful class to sent dumps to multiple handlers in a row.
  • Added dump() function.

]]>
0
[news] Queue extension 2.3.6 released Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/580/queue-extension-2-3-6-released https://www.yiiframework.com/news/580/queue-extension-2-3-6-released Bizley Bizley

Queue extension version 2.3.6 was released.

This release:

  • Added ability to configure keepalive and heartbeat for AMQP and AMQP interop
  • Added ability to push message with headers for AMQP interop driver
  • SignalLoop::$exitSignals now includes SIGQUIT
  • Fixed error with empty Redis payload

See the CHANGELOG for details.

]]>
0
[news] Debug extension 2.1.25 released Tue, 26 Sep 2023 15:58:07 +0000 https://www.yiiframework.com/news/579/debug-extension-2-1-25-released https://www.yiiframework.com/news/579/debug-extension-2-1-25-released Bizley Bizley

Debug extension version 2.1.25 was released.

This release:

  • Fix accessing toolbar data if it's not available.

See the CHANGELOG for details.

]]>
0
[news] Yii Middleware Dispatcher 5.2 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/578/yii-middleware-dispatcher-5-2 https://www.yiiframework.com/news/578/yii-middleware-dispatcher-5-2 rustamwin rustamwin

Minor version of Yii Middleware Dispatcher package is released.

This version adds support for invokable class names & implementations of Psr\Http\Server\RequestHandlerInterface.

]]>
0
[extension] exocet/yii2-upload-file-behavior Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/exocet/yii2-upload-file-behavior https://www.yiiframework.com/extension/exocet/yii2-upload-file-behavior xchwarze xchwarze

UploadFileBehavior for Yii2

  1. Features
  2. Installation
  3. Setup and Configuration
  4. Contribution

UploadFileBehavior is a Yii2 behavior designed to streamline the process of uploading files and/or images. It manages the processing and storage of files associated with an ActiveRecord model.

Features

  • File uploading for ActiveRecord models.
  • Customizable file saving steps.
  • Thumbnail generation.
  • Option to rename uploaded files.
  • Ability to delete files when associated records are deleted.
  • Automatic cleanup of directories during updates.

Installation

The preferred way to install this extension is through composer.

composer require "exocet/yii2-upload-file-behavior"

Setup and Configuration

  1. Include new behavior: `php use exocet\yii2UploadFileBehavior\UploadFileBehavior; `

  2. Add the upload file attribute to your model: `php public $upload_file; `

  3. Add safety rules for your image path or similar: `php [['image_path'], 'safe'], `

  4. Attach the behavior to your model: `php public function behaviors() {

     return [
         'uploadFileBehavior' => [
             'class' => UploadFileBehavior::className(),
             'nameOfAttributeStorage' => 'image_path',
             //... other configurations
         ],
     ];
    

    } `

Configuration Options

Here's a quick run-through of the configuration options:

  • modelAttributeForFile: (string) The model's attribute to receive the file from the form. Defaults to 'upload_file'.

  • modelAttributeForStorage: (string) The model's attribute to store the file path or reference. Defaults to 'images'.

  • newFileName: (bool|string) A new filename to save the uploaded file as. Defaults to false (meaning it won't rename).

  • steps: (array) Configurations detailing where and how to save the uploaded file. This can include thumbnail generation, different save paths, etc.

  • thumbnailPrefix: (string) Prefix for generated thumbnails. Defaults to 'thumb-'.

  • originalPrefix: (string) Prefix to use when saving a copy of the original image. Defaults to 'original-'.

  • scenarios: (array) Scenarios under which this behavior will be triggered. Defaults to ['default'].

  • deleteImageWithRecord: (bool) Whether or not to delete the file when the associated record is deleted. Defaults to false.

  • cleanDirWithUpdate: (bool) Whether or not to clean the upload directory when updating files. Defaults to false.

For more in-depth examples and how to set up the steps configuration, refer to the example given in the code comments.

Contribution

Feel free to contribute to this project by opening issues, pull requests, or providing feedback. Your contributions are welcome!

Designed with :heart: for Yii2 developers.

]]>
0
[news] Yii Strings 2.2 Wed, 20 Sep 2023 06:28:04 +0000 https://www.yiiframework.com/news/577/yii-strings-2-2 https://www.yiiframework.com/news/577/yii-strings-2-2 vjik vjik

Minor version of Yii Strings package was released.

In this release:

  • Add CombinedRegexp and MemoizedCombinedRegexp classes optimizes matching of multiple regular expressions.
  • Add methods StringHelper::trim(), StringHelper::ltrim() and StringHelper::rtrim().
  • Add $strict parameter to Inflector::toSnakeCase() method.
  • Raise required PHP version to 8.0.
  • Using fully-qualified function calls to improve performance.
  • Make minor refactoring.
]]>
0
[extension] exocet/yii2-chart-widget Tue, 05 Sep 2023 15:09:25 +0000 https://www.yiiframework.com/extension/exocet/yii2-chart-widget https://www.yiiframework.com/extension/exocet/yii2-chart-widget xchwarze xchwarze

yii2-widget-chart

  1. Resources
  2. Installation
  3. Example Usage
  4. Ajax Example Usage
  5. License

Wrapper for CHARTIST.JS library

Resources

Installation

The preferred way to install this extension is through composer

Either run

$ php composer.phar require --prefer-dist "exocet/yii2-chart-widget"

or add

{
	"require": {
  		"exocet/yii2-chart-widget": "~1.0"
	}
}

to the require section of your composer.json

Example Usage

<?php
/* @var $this yii\web\View */

use exocet\yii2\chart\Chart;
use yii\web\JsExpression;

$this->title = 'my example';
$this->params['breadcrumbs'][] = $this->title;

?>
<?php
echo Chart::widget([
           'type'              => Chart::TYPE_LINE,
           'disableCss'        => true, //disable chartist css
           'label'             => true, //enable labels plugin
           'labels'            => [1, 2, 3, 4],
           'series'            => [[100, 120, 180, 200]],
           'clientOptions'     => [
               'showLine' => false,
               'axisX'    => [
                   'labelInterpolationFnc' => new JsExpression('function(value, index) {
                       return index % 13 === 0 ? \'W\' + value : null;
                   }')
               ]
           ],
           'responsiveOptions' => [
               'screen and (min-width: 640px)' => [
                   'axisX' => [
                       'labelInterpolationFnc' => new JsExpression('function(value, index) {
                           return index % 4 === 0 ? \'W\' + value : null;
                       }')
                   ]
               ]
           ]
      ]);
?>

Ajax Example Usage

<?php
/* @var $this yii\web\View */

use exocet\yii2\chart\Chart;
use yii\web\JsExpression;
use yii\helpers\Url;

$this->title = 'my example';
$this->params['breadcrumbs'][] = $this->title;

?>
<?php
echo Chart::widget([
           'type'              => Chart::TYPE_LINE,
           'ajax'              => Url::to(['ajax-data']),
      ]);
?>

License

yii2-chart-widget is released under MIT license. See bundled LICENSE for details

]]>
0
[extension] exocet/yii2-bootstrap-material-design Thu, 07 Sep 2023 16:32:44 +0000 https://www.yiiframework.com/extension/exocet/yii2-bootstrap-material-design https://www.yiiframework.com/extension/exocet/yii2-bootstrap-material-design xchwarze xchwarze

yii2-bootstrap-material-design

  1. Installation
  2. Usage
  3. Widgets
  4. Gii support

Composer package for implementing FezVrasta's new bootstrap material design (MDB 6) in Yii2 https://github.com/mdbootstrap/mdb-ui-kit

Installation

The preferred way of installation is through Composer. `bash composer require exocet/yii2-bootstrap-material-design `

Usage

To load the MDB CSS and JS files integrate the MaterialAsset into your app. Two ways to achieve this is to register the asset in the main layout:

// @app/views/layouts/main.php

\exocet\bootstrap5md\MaterialCompatibleAsset::register($this); // include css and js
\exocet\bootstrap5md\FontawesomeAsset::register($this); // include icons (optional)
// further code

or as a dependency in your app wide AppAsset.php

// @app/assets/AppAsset.php

public $depends = [
    // include mdb and bootstrap 5 compatibility
    'exocet\bootstrap5md\MaterialCompatibleAsset',
    
    // include Fontawesome icons (optional)
    'exocet\bootstrap5md\FontawesomeAsset',

    // include material icons (optional)
    'exocet\bootstrap5md\MaterialIconsAsset',
    
    // more dependencies
    //...
];

Widgets

This add-on extends Bootstrap 5 by replacing dependencies with MDB dependencies and corrects the way html is generated in certain components to make them the way they are used with MDB.

For this we must overwrite the original AssetBundle as follows

// @app/config/web.php
'components' => [
    'assetManager' => [
        'bundles' => [
            'yii\bootstrap5\BootstrapAsset' => [
                'class' => \exocet\bootstrap5md\BootstrapAsset::class,
            ],
            'yii\bootstrap5\BootstrapPluginAsset' => [
                'class' => \exocet\bootstrap5md\BootstrapPluginAsset::class,
            ],
        ],
    ],

It is probably best to use it in combination with https://github.com/kartik-v/yii2-widgets

Gii support

If you are creating your CRUD controller and view files using Gii you can get materialized view files by integrating the adapted Gii templates.

// @app/config/main-local.php

$config['modules']['gii'] = [
    'class' => 'yii\gii\Module',      
    'allowedIPs' => ['127.0.0.1', '::1'],  
    'generators' => [
        'crud' => [
            'class' => 'yii\gii\generators\crud\Generator',
            'templates' => [
                'material-bootstrap' => '@vendor/exocet/yii2-bootstrap-material-design/src/generators/crud',
            ]
        ]
    ],
];

You can copy those templates to any location you wish for further customization. Make sure you adapt the path accordingly in your config.

]]>
0
[news] Yii 2.0.49 Wed, 30 Aug 2023 08:53:44 +0000 https://www.yiiframework.com/news/576/yii-2-0-49 https://www.yiiframework.com/news/576/yii-2-0-49 Bizley Bizley

We are pleased to announce the release of Yii Framework version 2.0.49.

Please refer to the instructions at https://www.yiiframework.com/download/ to install or upgrade to this version.

This release has a number of bugfixes and some small enhancements:

  • Fixed caching a MSSQL query with BLOB data type
  • Fixed yii\log\FileTarget to not export empty messages
  • Fixed the definition of dirty attributes in Active Record properties for a non-associative array in case of changing the order of elements
  • Allowed jQuery 3.7 to be installed
  • Added support Enums in Query Builder
  • Broadened the accepted type of Cookie::$expire to int|string|\DateTimeInterface|null

Thanks to all Yii community members who contribute to the framework, translators who keep documentation translations up to date and community members who answer questions at forums.

There are many active Yii communities so if you need help or want to share your experience, feel free to join them.

A complete list of changes can be found in the CHANGELOG.

]]>
0
[extension] ssiva/yii2-mpesa-sdk Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/ssiva/yii2-mpesa-sdk https://www.yiiframework.com/extension/ssiva/yii2-mpesa-sdk ssiva13 ssiva13

YII2 MPESA DARAJA SDK

This package provides a seamless integration of M-PESA Daraja APIs in Yii2 applications

  • B2C (Business to Customer)
  • C2B (Customer to Business)
  • B2B (Business to Business)
  • Account Balance inquiries
  • Transaction reversals queries
  • Transaction status queries.
Installation

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist ssiva/yii2-mpesa-sdk

or

composer require --prefer-dist ssiva/yii2-mpesa-sdk

or add

"ssiva/yii2-mpesa-sdk": "*"

to the require section of your composer.json file.

Configuration

Set up the config values as required

  • Copy the file mpesa.php to @app/config/mpesa.php and set you config values.
  • Add the component configuration to config/web.php as below
    • Require the copied config file `php $mpesa = require DIR.'/mmpesa.php'; `
    • Add the required value to the components array `php 'mpesaDaraja' => $mpesa, ` The library is now ready for use.
Usage Examples
<?php
namespace YOURNAMESPACE;

class CheckoutController extends Controller {
   
   public function actionCheckout(
        $mpesaDaraja = Yii::$app->mpesaDaraja->getDaraja();
        
        // authenticate
        $mpesaDaraja->authenticate();
        
        // STK Push
        $stkParams = [
            'Amount' => '2',
            'PartyA' => '2547XXXXXXXX',
            'PhoneNumber' => '2547XXXXXXXX',
            'AccountReference' => '13',
            'TransactionDesc' => 'Shopping',
        ];
       $mpesaDaraja->stkPush($stkParams);
       
       // stk push status query
       $stkQueryParams = [
         'CheckoutRequestID' => "ws_CO_290320231617432767XXXXXXXX",
       ];
       $mpesaDaraja->stkPushQuery($stkQueryParams);
       
       // transaction status query
       $statusParams = [
         'Remarks' => "Status test for RCC3LAPCEL",
         "TransactionID" => "RCC3LAPCEL",
         "Occasion" => "Optional Value for Occasion"
       ];
       $mpesaDaraja->transactionStatus($statusParams);

   }
}

]]>
0
[extension] dddphp/state-machine Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/dddphp/state-machine https://www.yiiframework.com/extension/dddphp/state-machine jiaweipan jiaweipan

State Machine for PHP

  1. Installation (via composer)
  2. User Guide

The State Machine library is aimed to provide a easy use, lightweight, flexible and extensible, and type safe PHP state machine implementation for enterprise usage.

Installation (via composer)

{
    "require": {
        "dddphp/state-machine": "*"
    }
}

User Guide

Get Starting
  • State Machine Builder

    • State machine builder is used to generate state machine definition. StateMachineBuilder can be created by StateMachineBuilderFactory.
    • The StateMachineBuilder is composed of TransitionBuilder (InternalTransitionBuilder / ExternalTransitionBuilder) which is used to build transition between states.
    • The internal state is implicitly built during transition creation or state action creation.
    • All the state machine instances created by the same state machine builder share the same definition data for memory usage optimize.
    • State machine builder generate state machine definition in a lazy manner. When builder create first state machine instance, the state machine definition will be generated which is time consumed. But after state machine definition generated, the following state machine instance creation will be much faster. Generally, state machine builder should be reused as much as possible.

    In order to create a state machine, user need to create state machine builder first. For example:

    `php $builder = StateMachineBuilderFactory::create(); `

  • Fluent API After state machine builder was created, we can use fluent API to define state/transition/action of the state machine.

    `php $builder->externalTransition()

      ->from(self::STATEA)
      ->to(self::STATEB)
      ->on(self::EVENTGoToB)
      ->when($this->checkCondition())
      ->perform($this->doAction());
    

    `

    An external transition is built between state 'A' to state 'B' and triggered on received event 'GoToB'.

    `php $builder->internalTransition()

      ->within(self::STATEA)
      ->on(self::INTERNAL_EVENT)
      ->when($this->checkCondition())
      ->perform($this->doAction());
    

    `

    An internal transition with priority set to high is build inside state 'A' on event 'INTERNAL_EVENT' perform '$this->doAction()'. The internal transition means after transition complete, no state is exited or entered. The transition priority is used to override original transition when state machine extended.

    `php $builder->externalTransition()

          ->from(self::STATEC)
          ->to(self::STATED)
          ->on(self::EVENTGoToD)
          ->when(
              new class () implements ConditionInterface {
                  public function isSatisfied($context): bool
                  {
                      echo "Check condition : " . $context . "\n";
                      return true;
                  }
                  public function name(): string
                  {
                      return '';
                  }
              })
          ->perform(
              new class () implements ActionInterface {
                  public function execute($from, $to, $event, $context): void
                  {
                      echo $context . " from:" . $from . " to:" . $to . " on:" . $event;
                  }
              };
          );
    

    `

    An conditional transition is built from state 'C' to state 'D' on event 'GoToD' when external context satisfied the condition restriction, then call action method.

  • New State Machine Instance

    After user defined state machine behaviour, user could create a new state machine instance through builder. Note, once the state machine instance is created from the builder, the builder cannot be used to define any new element of state machine anymore.

    New state machine from state machine builder.

    $stateMachine = $builder->build(self::MACHINE_ID);
    
  • Trigger Transitions

    After state machine was created, user can fire events along with context to trigger transition inside state machine. e.g.

    $target = $stateMachine->fire(self::STATE1, self::EVENT1, $this->context);
    
]]>
0
[news] Yii Access 2.0 Mon, 07 Aug 2023 07:13:53 +0000 https://www.yiiframework.com/news/575/yii-access-2-0 https://www.yiiframework.com/news/575/yii-access-2-0 vjik vjik

Major version of Yii Access package was released.

In this release:

  • type for $userId parameter of AccessCheckerInterface::userHasPermission() method is specified;
  • mininum PHP version was raised to 8.0.
]]>
0
[news] Rate limiter 3.0.0 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/news/574/rate-limiter-3-0-0 https://www.yiiframework.com/news/574/rate-limiter-3-0-0 samdark samdark

Major version of yiisoft/rate-limiter package was released.

In this release:

  • Revised architecture allowing concurrency-safe counter storages.
  • Added APCu counters storage that is suitable for high concurrency environments that might cause dirty reads.
  • Dependency upgrades.
  • Mininum PHP version was raised to 8.0.

Thanks to Pan Jiawei for handing this complicated work.

]]>
0
[extension] ishukrullo/yii2-errorsender Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/ishukrullo/yii2-errorsender https://www.yiiframework.com/extension/ishukrullo/yii2-errorsender iShukrullo iShukrullo

Send exceptions to telegram user.

  1. Installation
  2. Usage

On Exception, sends exception title and content to set telegram user. To get errors, first you need to hit start on bot ErrorGetterBot

Installation

The preferred way to install this extension is through composer.

Either run

composer require  ishukrullo/yii2-errorsender v1.2.1

or add

"ishukrullo/yii2-errorsender": "^v1.2.1"

to the require section of your composer.json file.

Usage

Once the extension is installed, add custom errorHandler class to component section of your configuration file (config/main.php):

        'errorHandler' => [
            'class' => 'ishukrullo\errorsender\ErrorSendToTelegram',
            'userId' => '460314569', // telegram user's id (don't forget to start the @ErrorGetterBot)
            'errorAction' => 'site/error',
        ],

]]>
0
[news] DB and drivers updates Tue, 25 Jul 2023 03:17:12 +0000 https://www.yiiframework.com/news/573/db-and-drivers-updates https://www.yiiframework.com/news/573/db-and-drivers-updates samdark samdark

Database package was tagged along with its drivers.

  • Legacy array type-casting syntax was removed from db. Param should now be used instead.
  • Drivers got more tests, refactoring and more edge-cases handled.

Here's the list CHANGELOGs:

]]>
0
[extension] mrmuminov/yii2-playmobile-uz Mon, 26 Jun 2023 10:20:31 +0000 https://www.yiiframework.com/extension/mrmuminov/yii2-playmobile-uz https://www.yiiframework.com/extension/mrmuminov/yii2-playmobile-uz DarkShade DarkShade

Yii2 PlaymobileUz SMS-shlyuz

  1. Installation
  2. Usage

Yii2 PlaymobileUz SMS-shlyuz

Installation

The preferred way to install this extension is through composer.

Run composer require mrmuminov/yii2-playmobile-uz

or add

"mrmuminov/yii2-playmobile-uz": "^2.0.0"

to the require section of your composer.json file.

Usage

Example code in example folder

]]>
0
[extension] biladina/yii2-ajaxcrud-bs4 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/biladina/yii2-ajaxcrud-bs4 https://www.yiiframework.com/extension/biladina/yii2-ajaxcrud-bs4 biladina biladina

yii2-ajaxcrud

  1. Features
  2. Installation
  3. Bootstrap 4
  4. Usage
  5. Translate
  6. Reload Multiple Pjax

Original work by johitvn.

But we need to work with Bootstrap 4, so we create this repository. If johitvn update his repo, we will delete this repository.

Latest Stable Version License Total Downloads

Gii CRUD template for Single Page Ajax Administration for yii2

index

create

view

update

delete

Features

  • Create, read, update, delete in onpage with Ajax
  • Bulk delete suport
  • Pjax widget suport
  • Export function(pdf,html,text,csv,excel,json)
  • Support Boostrap 4/5
  • Added translations, available right now only English and Indonesia
  • Reload multiple Pjax

Installation

The default installation is using Bootstrap 5.

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist biladina/yii2-ajaxcrud-bs4 "~3.0"

or add

"biladina/yii2-ajaxcrud-bs4": "~3.0"

to the require section of your composer.json file.

Bootstrap 4

If you still need the Boostrap 4 version, you can use version 2

php composer.phar require --prefer-dist biladina/yii2-ajaxcrud-bs4 "~2.0"

or add

"biladina/yii2-ajaxcrud-bs4": "~2.0"

to the require section of your composer.json file.

Usage

For first you must enable Gii module Read more about Gii code generation tool

Because this extension used kartik-v/yii2-grid extensions so we must config gridview module before

Let's add into modules config in your main config file `php 'modules' => [

'gridview' =>  [
    'class' => '\kartik\grid\Module'
]       

] `

Add translation to the config `php 'components' => [

'i18n' => [
    'translations' => [
        'yii2-ajaxcrud' => [
            'class' => 'yii\i18n\PhpMessageSource',
            'basePath' => '@yii2ajaxcrud/ajaxcrud/messages',
            'sourceLanguage' => 'en',
        ],
    ]
]

] `

Add bsVersion to the params file `php return [

'bsVersion' => '5.x',

]; `

You can then access Gii through the following URL:

http://localhost/path/to/index.php?r=gii

and you can see Ajax CRUD Generator

Translate

Default translation is english, you can pull request new translation and you can change via config. Open your config main.php, change the language and translation sourceLanguage

Available Translation :

  • English
  • Indonesia
'language' => 'id-ID',

'components' => [
    'i18n' => [
        'translations' => [
            'yii2-ajaxcrud' => [
                'class' => 'yii\i18n\PhpMessageSource',
                'basePath' => '@yii2ajaxcrud/ajaxcrud/messages',
                'sourceLanguage' => 'id',
            ],
        ]
    ]
]

Reload Multiple Pjax

If you need to reload multiple GridView Pjax via Ajax respond from controller, you can add another Pjax ID separated by comma.

return [
    'forceReload'=>'#crud-pjax1,#crud-pjax2', // you can add more Pjax ID that you want to reload via ajax respond.
    'title'=> Yii::t('yii2-ajaxcrud', 'Create New')." Content",
    'content'=>'<span class="text-success">'.Yii::t('yii2-ajaxcrud', 'Create').' Content '.Yii::t('yii2-ajaxcrud', 'Success').'</span>',
    'footer'=> Html::button(Yii::t('yii2-ajaxcrud', 'Close'), ['class'=>'btn btn-default pull-left','data-dismiss'=>"modal"]).
        Html::a(Yii::t('yii2-ajaxcrud', 'Create More'), ['create'],['class'=>'btn btn-primary','role'=>'modal-remote'])
];
]]>
0
[extension] yus-ham/yii2-resend Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/yus-ham/yii2-resend https://www.yiiframework.com/extension/yus-ham/yii2-resend suphm suphm

Yii 2 extension for Resend

You need this library? just install it through composer

composer require yus-ham/yii2-resend --prefer-dist -o

Also don't forget to configure

$config['components']['mailer'] = [
    'class' => 'yusham\resend\Mailer',
    'useFileTransport' => false,
    'viewPath' => '@app/mail',
    'transport' => [
        'apiKey' => '<YOUR_API_KEY>'
    ],
];

And you can then send an email as usually `php Yii::$app->mailer->compose('contact/html')

 ->setFrom('from@domain.com')
 ->setTo($form->email)
 ->setSubject($form->subject)
 ->send();

]]>
0
[extension] kazda01/yii2-search Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/kazda01/yii2-search https://www.yiiframework.com/extension/kazda01/yii2-search kazda01 kazda01

Yii2 search

  1. Installation
  2. Usage
  3. Options
  4. Examples
  5. License
  6. Authors
  7. Tests

A simple search engine that allows the user to search for models by defined attributes and rules.

Tests Total Downloads License

Search example

After specifying the rules, the user can search for multiple Models by multiple attributes at once. The results are then dynamically listed under the input. For each search result, its IdentifyingString and the name of the attribute in which a match was found is displayed. The exact match is also highlighted in bold.

Installation

The preferred way to install this extension is through composer.

composer require kazda01/yii2-search "@dev"

Usage

1. Add Module and configure search parameters

After adding the project via composer, you need to add the SearchModule in the web.php modules and configure searching parameters. If you want to have multiple different search engines (e.g. Invoice and User search engine), add the Module multiple times, ModuleID will be used for distinguishing.

The ModuleID will also be used to create the search route. The final requests will go to website.com/ModuleID/search?search=query. It is important to add the ModuleID to bootstrap so that the route is added to the UrlManager with priority over your site's routes (this action is not needed if your routes do not conflict with the search route).

return [
    ...
    'bootstrap' => ['<ModuleID>'],
    'modules' => [
        '<ModuleID>' => [
            'class' => '\kazda01\search\SearchModule',
            'searchConfig' => [
                '<ModelSearch>' => [
                    'columns' => ['<ModelAttribute>', '<ModelAttribute>'],
                    'matchTitle' => '<matchTitle>',
                    'matchText' => function($model){
                        return 'Model: ' . $model->id;
                    }
                ],
            ]
        ],
    ],
    ...
];

All available settings are described here.

2. Add Search Model classes

Search Model classes must be generated. They can be simply made by Gii CRUD generator. You can restrict what results certain users can search/display.

3. Generate search input

In a view, add this line.

<?= SearchInput::widget(['search_id' => 'main-search']); ?>

All available settings are described here.

Options

Module config
option type default value description
allowGet boolean false Whether the search route (/ModuleID/search) should accept the GET method in addition to POST.
rules Rules array [] Rules array as described by Yii docs. Can be used to restrict access to certain searches as shown in examples below.
searchResultClass string 'rounded' Class list of the search result.
searchConfig searchConfig array [] Configuration of search fields - how to search the modules.
Search Config
option type default value description
columns array[string] [] The columns/attributes in which the search engine should search for a match.
matchTitle string\|function() None (required) Title/header that will be above results from this search_id. If you want to use Yii::t() function, pass a function that returns Yii2 translation function (see example at the bottom).
matchText function($model) None (required) Text that will be displayed for every match. Should be some string that idetifies the found model.
route string '<tablename>/view' Route to which the user should be directed after clicking.
routeParams function($model) Primary key Route params to be used in Url::toRoute([$route, $routeParams])
only_if function($model) - A function that receives a module as input, this record is only outputted in the results if this function returns true.
group_by boolean\|string\|array[string] false Whether to use GROUP BY clause to filter the same records. If true, the columns from columns will be used for GROUP BY. If it is a String or an array of Strings, those will be used as GROUP BY columns.
Input widget Config
option default value description
search_id None (required) Used to identify which Search config to use.
placeholder 'Search' Input placeholder.
formClass '' Class list of the wrapper form.
buttonClass 'btn btn-dark search-button' Class list of the search button.
buttonContent SVG of magnifying glass icon Content of the search button.
inputClass 'form-control p-1 no-outline' Class list of the search input.
wrapperClass '' Class list of the wrapper div.
widgetClass 'mx-auto shadow p-2 position-absolute bg-white' Class list of the widget div, generated by ajax call.

Examples

Example web.php

Search invoice-search allows all users to search

  • Invoice models by their invoice_number attribute

Search main-search allows logged in users to search

  • Invoice models by their invoice_number attribute
  • InvoiceItem models by their description attribute
  • Company models by their name, vat_id and state attribute

By clicking on Invoice result, user gets redirected to Invoice detail page.

By clicking on InvoiceItem result, user gets redirected to the detail page of Invoice which has the item.

By clicking on Company result, user gets redirected to Invoice listing with set parameter in url to filter the Invoices by Company customer.

Company results are also grouped by name, vat_id and state attributes. They are also filtered by Companies, that are recorded as a Customer on any invoice, not supplier.

return [
    ...
    'bootstrap' => ['main-search', 'invoice-search'],
    'modules' => [
        'main-search' => [
            'class' => '\kazda01\search\SearchModule',
            'allowGet' => true,
            'rules' => [
                [
                    'allow' => true,
                    'roles' => ['@'],
                    'actions' => ['index'], // allows only logged in users
                ],
            ],
            'searchConfig' => [
                'CompanySearch' => [
                    'columns' => ['name', 'vat_id', 'state'],
                    'matchTitle' => function(){
                        return Yii::t('app', 'Company')
                    },
                    'matchText' => function($model){
                        return $model->name . ', ' . $model->vat_id;
                    }
                    'route' => 'invoice/index',
                    'route_params' => function ($model) {
                        return ['InvoiceSearch[fk_company_customer]' => $model->name];
                    },
                    'only_if' => function ($model) {
                        return \app\models\Invoice::find()->where(['fk_company_customer' => $model->id])->count() > 0;
                    },
                    'group_by' => true,
                ],
                'InvoiceSearch' => [
                    'columns' => ['invoice_number'],
                    'matchTitle' => 'Invoice',
                    'matchText' => function($model){
                        return 'Invoice number ' . $model->invoice_number;
                    }
                ],
                'InvoiceItemSearch' => [
                    'columns' => ['description'],
                    'matchTitle' => 'Invoice item',
                    'matchText' => function($model){
                        return 'Invoice number: ' . $model->invoice->invoice_number;
                    }
                    'route' => 'invoice/view',
                    'route_params' => function ($model) {
                        return ['id' => $model->invoice->id];
                    }
                ],
            ]
        ],
        'invoice-search' => [
            'class' => '\kazda01\search\SearchModule',
            'searchConfig' => [
                'InvoiceSearch' => [
                    'columns' => ['invoice_number'],
                    'matchTitle' => 'Invoice',
                    'matchText' => function($model){
                        return 'Invoice number: ' . $model->invoice_number;
                    }
                ],
            ]
        ]
    ],
    ...
];
Example view.php
<div class="row g-0 justify-content-center">
    <?= SearchInput::widget([
        'search_id' => 'main-search',
        'placeholder' => Yii::t('app', 'Invoice number, customer name, job description..'),
        'wrapperClass' => 'col-11 col-lg-8 col-xl-7 mx-auto',
        'inputClass' => 'form-control p-2 no-outline',
    ]); ?>
</div>

License

MIT

Authors

Tests

Every push to the repository triggers the CI test pipeline. You can run them locally by running these commands in the project root directory.

vendor/bin/phpstan analyse ./src
vendor/bin/phpcs ./src --extensions=php --colors --standard=PSR12 -n
vendor/bin/phpunit tests
]]>
0
[extension] cebe/yii2-loki-log-target Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/cebe/yii2-loki-log-target https://www.yiiframework.com/extension/cebe/yii2-loki-log-target CeBe CeBe

yii2-loki-log-target

  1. Requirements
  2. Install
  3. Usage

Grafana Loki Log Target for Yii2.

Requirements

  • PHP 7.1 or higher (works with PHP 8)
  • Yii 2

Install

composer require cebe/yii2-loki-log-target

Usage

Add the log target to your application config:

// ...
'components' => [
    // ...
    'log' => [
        // ...
        'targets' => [
            [
                'class' => \cebe\lokilogtarget\LokiLogTarget::class,
                //'enabled' => YII_ENV_PROD,

                'lokiPushUrl' => 'https://loki.example.com/loki/api/v1/push',
                'lokiAuthUser' => 'loki', // HTTP Basic Auth User
                'lokiAuthPassword' => '...', // HTTP Basic Auth Password

                'levels' => ['error', 'warning', 'info'],

                // optionally exclude categories
                'except' => [
                    'yii\db\Connection::open',
                    'yii\db\Command::execute',
                    'yii\httpclient\StreamTransport::send',
                ],

                // optionally re-map log level for certain categories
                'levelMap' => [
                    // yii category
                    'yii\web\HttpException:404' => [
                        // yii level => loki level
                        // set loki level to false, to drop messages with that category
                        '*' => 'info',
                    ],
                    'yii\web\HttpException:401' => [
                        // yii level => loki level
                        // set loki level to false, to drop messages with that category
                        '*' => 'warning',
                    ],
                ],

            ],
        ],
    ],
]

See also https://www.yiiframework.com/doc/guide/2.0/en/runtime-logging#log-targets.

]]>
0
[extension] diecoding/yii2-flysystem Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/diecoding/yii2-flysystem https://www.yiiframework.com/extension/diecoding/yii2-flysystem die-coding die-coding

Yii2 Flysystem

  1. Table of Contents
  2. Instalation
  3. Dependencies
  4. Dev. Dependencies
  5. Configuration
  6. Additional Configuration
  7. Usage
  8. Using Traits

The League Flysystem for local and remote filesystems library for Yii2.

This extension provides Flysystem 3 integration for the Yii framework. Flysystem is a filesystem abstraction which allows you to easily swap out a local filesystem for a remote one.

Latest Stable Version Total Downloads Latest Stable Release Date Quality Score Build Status License PHP Version Require

Yii2 Flysystem uses league/flysystem

Table of Contents

Instalation

Package is available on Packagist, you can install it using Composer.

composer require diecoding/yii2-flysystem "^1.0"

or add to the require section of your composer.json file.

"diecoding/yii2-flysystem": "^1.0"

Dependencies

Dev. Dependencies

Configuration

Local Filesystem

Configure application components as follows

return [
    // ...
    'components' => [
        // ...
        'fs' => [
            'class' => \diecoding\flysystem\LocalComponent::class,
            'path' => dirname(dirname(__DIR__)) . '/storage', // or you can use @alias
            'secret' => 'my-secret', // for secure route url
            // 'action' => '/site/file', // action route
            // 'prefix' => '',
        ],
    ],
];
AsyncAws S3 Filesystem

Either run

composer require league/flysystem-async-aws-s3:^3.0

or add

"league/flysystem-async-aws-s3": "^3.0"

to the require section of your composer.json file and configure application components as follows

return [
    // ...
    'components' => [
        // ...
        'fs' => [
            'class' => \diecoding\flysystem\AsyncAwsS3Component::class,
            'endpoint' => 'http://your-endpoint',
            'bucket' => 'my-bucket',
            'accessKeyId' => 'my-key',
            'accessKeySecret' => 'my-secret',
            // 'sharedCredentialsFile' => '~/.aws/credentials',
            // 'sharedConfigFile' => '~/.aws/config',
            // 'region' => 'us-east-1',
            // 'endpointDiscoveryEnabled' => false,
            // 'pathStyleEndpoint' => false,
            // 'sendChunkedBody' => false,
            // 'debug' => false,
            // 'prefix' => '',
        ],
    ],
];
AWS S3 Filesystem

Either run

composer require league/flysystem-aws-s3-v3:^3.0

or add

"league/flysystem-aws-s3-v3": "^3.0"

to the require section of your composer.json file and configure application components as follows

return [
    // ...
    'components' => [
        // ...
        'fs' => [
            'class' => \diecoding\flysystem\AwsS3Component::class,
            'endpoint' => 'http://your-endpoint',
            'key' => 'your-key',
            'secret' => 'your-secret',
            'bucket' => 'your-bucket',
            // 'region' => 'us-east-1'
            // 'version' => 'latest',
            // 'usePathStyleEndpoint' => false,
            // 'streamReads' => false,
            // 'options' => [],
            // 'credentials' => [],
            // 'debug' => false,
            // 'prefix' => '',
        ],
    ],
];
FTP Filesystem

Either run

composer require league/flysystem-ftp:^3.0

or add

"league/flysystem-ftp": "^3.0"

to the require section of your composer.json file and configure application components as follows

return [
    // ...
    'components' => [
        // ...
        'fs' => [
            'class' => \diecoding\flysystem\FtpComponent::class,
            'host' => 'hostname',
            'root' => '/root/path/', // or you can use @alias
            'username' => 'username',
            'password' => 'password',
            // 'port' => 21,
            // 'ssl' => false,
            // 'timeout' => 90,
            // 'utf8' => false,
            // 'passive' => true,
            // 'transferMode' => FTP_BINARY,
            // 'systemType' => null, // 'windows' or 'unix'
            // 'ignorePassiveAddress' => null, // true or false
            // 'timestampsOnUnixListingsEnabled' => false,
            // 'recurseManually' => true,
            // 'useRawListOptions' => null, // true or false
            // 'passphrase' => 'secret', // for secure route url
            // 'action' => '/site/file', // action route
            // 'prefix' => '',
        ],
    ],
];
SFTP Filesystem

Either run

composer require league/flysystem-sftp-v3:^3.0

or add

"league/flysystem-sftp-v3": "^3.0"

to the require section of your composer.json file and configure application components as follows

return [
    // ...
    'components' => [
        'fs' => [
            'class' => \diecoding\flysystem\SftpComponent::class,
            'host' => 'hostname',
            'username' => 'username',
            'password' => null, // password (optional, default: null) set to null if privateKey is used
            // 'privateKey' => '/path/to/my/private_key', // private key (optional, default: null) can be used instead of password, set to null if password is set
            // 'passphrase' => 'super-secret-password', // passphrase (optional, default: null), set to null if privateKey is not used or has no passphrase
            // 'port' => 22,
            // 'useAgent' => true,
            // 'timeout' => 10,
            // 'maxTries' => 4,
            // 'hostFingerprint' => null,
            // 'connectivityChecker' => null, // connectivity checker (must be an implementation of `League\Flysystem\PhpseclibV2\ConnectivityChecker` to check if a connection can be established (optional, omit if you don't need some special handling for setting reliable connections)
            // 'preferredAlgorithms' => [],
            // 'root' => '/root/path/', // or you can use @alias
            // 'action' => '/site/file', // action route
            // 'prefix' => '',
        ],
    ],
];
WebDAV Filesystem

Either run

composer require league/flysystem-webdav:^3.0

or add

"league/flysystem-webdav": "^3.0"

to the require section of your composer.json file and configure application components as follows

return [
    // ...
    'components' => [
        // ...
        'fs' => [
            'class' => \diecoding\flysystem\WebDavComponent::class,
            'baseUri' => 'http://your-webdav-server.org/',
            'userName' => 'your_user',
            'password' => 'superSecret1234',
            // 'proxy' => '',
            // 'authType' => \Sabre\DAV\Client::AUTH_BASIC,
            // 'encoding' => \Sabre\DAV\Client::ENCODING_IDENTITY,
            // 'prefix' => '',
        ],
    ],
];
ZipArchive Filesystem

Either run

composer require league/flysystem-ziparchive:^3.0

or add

"league/flysystem-ziparchive": "^3.0"

to the require section of your composer.json file and configure application components as follows

return [
    // ...
    'components' => [
        // ...
        'fs' => [
            'class' => \diecoding\flysystem\ZipArchiveComponent::class,
            'pathToZip' => dirname(dirname(__DIR__)) . '/storage.zip', // or you can use @alias
            'secret' => 'my-secret', // for secure route url
            // 'action' => '/site/file', // action route
            // 'prefix' => '', // root directory inside zip file
        ],
    ],
];

Additional Configuration

URL File Action Settings

The following adapters have URL File Action generation capabilities:

  • Local Component
  • FTP Component
  • SFTP Component

Configure action in controller as follows

This example at SiteController for /site/file

class SiteController extends Controller
{
    //...
    public function actions()
    {
        return [
            // ...
            'file' => [
                'class' => \diecoding\flysystem\actions\FileAction::class,
                // 'component' => 'fs',
            ],
        ];
    }
}

Remember to configure action key in fs application components as follows

return [
    // ...
    'components' => [
        // ...
        'fs' => [
            // ...
            'action' => '/site/file', // action for get url file
        ],
    ],
];
Global Visibility Settings

Configure fs application component as follows

return [
    //...
    'components' => [
        //...
        'fs' => [
            //...
            'config' => [
                'visibility' => \League\Flysystem\Visibility::PRIVATE,
            ],
        ],
    ],
];

Usage

Writing or Updating Files

To write or update file

Yii::$app->fs->write('filename.ext', 'contents');

To write or update file using stream contents

$stream = fopen('/path/to/somefile.ext', 'r+');
Yii::$app->fs->writeStream('filename.ext', $stream);
Reading Files

To read file

$contents = Yii::$app->fs->read('filename.ext');

To retrieve a read-stream

$stream = Yii::$app->fs->readStream('filename.ext');
$contents = stream_get_contents($stream);
fclose($stream);
Checking if a File Exists

To check if a file exists

$exists = Yii::$app->fs->fileExists('filename.ext');
Deleting Files

To delete file

Yii::$app->fs->delete('filename.ext');
Getting Files Mime Type

To get file mime type

$mimeType = Yii::$app->fs->mimeType('filename.ext');
Getting Files Timestamp / Last Modified

To get file timestamp

$timestamp = Yii::$app->fs->lastModified('filename.ext');
Getting Files Size

To get file size

$byte = Yii::$app->fs->fileSize('filename.ext');
Creating Directories

To create directory

Yii::$app->fs->createDirectory('path/to/directory');

Directories are also made implicitly when writing to a deeper path

Yii::$app->fs->write('path/to/filename.ext');
Checking if a Directory Exists

To check if a directory exists

$exists = Yii::$app->fs->directoryExists('path/to/directory');
Deleting Directories

To delete directory

Yii::$app->fs->deleteDirectory('path/to/directory');
Checking if a File or Directory Exists

To check if a file or directory exists

$exists = Yii::$app->fs->has('path/to/directory/filename.ext');
Managing Visibility

Visibility is the abstraction of file permissions across multiple platforms. Visibility can be either public or private.

Yii::$app->fs->write('filename.ext', 'contents', [
    'visibility' => \League\Flysystem\Visibility::PRIVATE
]);

You can also change and check visibility of existing files

if (Yii::$app->fs->visibility('filename.ext') === \League\Flysystem\Visibility::PRIVATE) {
    Yii::$app->fs->setVisibility('filename.ext', \League\Flysystem\Visibility::PUBLIC);
}
Listing contents

To list contents

$contents = Yii::$app->fs->listContents();

foreach ($contents as $object) {
    echo $object['basename']
        . ' is located at' . $object['path']
        . ' and is a ' . $object['type'];
}

By default Flysystem lists the top directory non-recursively. You can supply a directory name and recursive boolean to get more precise results

$contents = Yii::$app->fs->listContents('path/to/directory', true);
Copy Files or Directories

To copy contents

Yii::$app->fs->copy('path/from/directory/filename.ext', 'path/to/directory/filename.ext', [
    'visibility' => \League\Flysystem\Visibility::PRIVATE
]);
Move Files or Directories

To move contents

Yii::$app->fs->move('path/from/directory/filename.ext', 'path/to/directory/filename.ext', [
    'visibility' => \League\Flysystem\Visibility::PRIVATE
]);
Get URL Files

To get url contents

Yii::$app->fs->publicUrl('path/to/directory/filename.ext');
Get URL Temporary Files / Presigned URL

To get temporary url contents

$expiresAt = new \DateTimeImmutable('+10 Minutes');

Yii::$app->fs->temporaryUrl('path/to/directory/filename.ext', $expiresAt);

The $expiresAt should be a valid and instance of PHP DateTimeInterface. Read PHP documentation for details.

Get MD5 Hash File Contents

To get MD5 hash of the file contents

Yii::$app->fs->checksum('path/to/directory/filename.ext');

Using Traits

Model Trait

Attach the Trait to the Model/ActiveRecord with some media attribute that will be saved in Flysystem (fs):

/**
 * @property string|null $file
 */
class Model extends \yii\db\ActiveRecord
{
    use \diecoding\flysystem\traits\ModelTrait;

    // ...

    public function rules()
    {
        return [
            ['image', 'string'], // Stores the filename
        ];
    }

    /**
     * @inheritdoc
     */
    protected function attributePaths()
    {
        return [
            'image' => 'images/'
        ];
    }

    // ...
}

Override the attributePaths() method to change the base path where the files will be saved on Flysystem (fs).

  • You can map a different path to each file attribute of your Model/ActiveRecord.
Using Trait Methods
$image = \yii\web\UploadedFile::getInstance($model, 'image');

// Save image_thumb.* to Flysystem (fs) on //my_bucket/images/ path
// The extension of the file will be determined by the submitted file type
// This allows multiple file types upload (png,jpg,gif,...)
// $model->image will hold "image_thumb.png" after this call finish with success
$model->saveUploadedFile($image, 'image', 'image_thumb');
$model->save();

// Save image_thumb.png to Flysystem (fs) on //my_bucket/images/ path
// The extension of the file will be determined by the submitted file type
// This force the extension to *.png
$model->saveUploadedFile($image, 'image', 'image_thumb.png', false);
$model->save();

// Remove the file with named saved on the image attribute
// Continuing the example, here "//my_bucket/images/my_image.png" will be deleted from Flysystem (fs)
$model->removeFile('image');
$model->save();

// Get the URL to the image on Flysystem (fs)
$model->getFileUrl('image');

// Get the presigned URL to the image on Flysystem (fs)
// The default duration is "+5 Minutes"
$model->getFilePresignedUrl('image');
Overriding Trait Methods
getFsComponent

The Flysystem (fs) MediaTrait depends on this component to be configured. The default configuration is to use this component on index 'fs', but you may use another value. For this cases, override the getFsComponent() method:

public function getFsComponent()
{
    return Yii::$app->get('my_fs_component');
}
attributePaths

The main method to override is attributePaths(), which defines a path in Flysystem (fs) for each attribute of yout model. Allowing you to save each attribute in a different Flysystem (fs) folder.

Here an example:

protected function attributePaths()
{
    return [
        'logo' => 'logos/',
        'badge' => 'images/badges/'
    ];
}

// or use another attribute, example: id
// ! Note: id must contain a value first if you don't want it to be empty

protected function attributePaths()
{
    return [
        'logo' => 'thumbnail/' . $this->id . '/logos/',
        'badge' => 'thumbnail/' . $this->id . '/images/badges/'
    ];
}
getPresignedUrlDuration

The default pressigned URL duration is set to "+5 Minutes", override this method and use your own expiration. Return must instance of DateTimeInterface

protected function getPresignedUrlDuration($attribute)
{
    return new \DateTimeImmutable('+2 Hours');
}

// or if you want to set the attribute differently

protected function getPresignedUrlDuration($attribute)
{
    switch ($attribute) {
        case 'badge':
            return new \DateTimeImmutable('+2 Hours');
            break;
        
        default:
            return new \DateTimeImmutable('+1 Days');
            break;
    }
}

The value should be a valid and instance of PHP DateTimeInterface. Read PHP documentation for details.

Read more docs: https://sugengsulistiyawan.my.id/docs/opensource/yii2/flysystem/

]]>
0
[extension] diecoding/yii2-slick Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/diecoding/yii2-slick https://www.yiiframework.com/extension/diecoding/yii2-slick die-coding die-coding

Yii2 Slick

  1. Table of Contents
  2. Instalation
  3. Dependencies
  4. Usage

The last carousel you'll ever need for Yii2

Latest Stable Version Total Downloads Latest Stable Release Date Quality Score Build Status License PHP Version Require

Yii2 Slick uses Slick
Demo: http://kenwheeler.github.io/slick

Table of Contents

Instalation

Package is available on Packagist, you can install it using Composer.

composer require diecoding/yii2-slick "^1.0"

or add to the require section of your composer.json file.

"diecoding/yii2-slick": "^1.0"

Dependencies

Usage

Read more demo or settings at http://kenwheeler.github.io/slick

use diecoding\slick\SlickCarousel;

echo SlickCarousel::widget([
    'items' => [ // (array) widget elements for the carousel
        // HTML content
        '<div><h3>1</h3></div>',
        '<div><h3>2</h3></div>',
        '<div><h3>3</h3></div>',
        '<div><h3>4</h3></div>',
        '<div><h3>5</h3></div>',
        '<div><h3>6</h3></div>',
    ],
    'containerOptions' => [],      // (array) HTML attributes to render on the container
    'containerTag'     => 'div',   // (string) HTML tag to render the container
    'itemOptions'      => [],      // (array) HTML attributes for the one item
    'itemTag'          => 'div',   // (string) HTML tag to render items for the carousel
    'skipCoreAssets'   => false,   // (bool) default `false`, `true` if use custom or external slick assets
    'pluginOptions'    => [        // (array) default `[]`, for option `$(#options['id']).slick(pluginOptions);`
        // @see https://github.com/kenwheeler/slick/#settings

        // 'accessibility'    => true,                                                         // boolean, default `true`
        // 'adaptiveHeight'   => false,                                                        // boolean, default `false`
        // 'appendArrows'     => $(element),                                                   // string, default `$(element)`
        // 'appendDots'       => $(element),                                                   // string, default `$(element)`
        // 'arrows'           => true,                                                         // boolean, default `true`
        // 'asNavFor'         => $(element),                                                   // string, default `$(element)`
        // 'autoplay'         => false,                                                        // boolean, default `false`
        // 'autoplaySpeed'    => 3000,                                                         // int, default `3000`
        // 'centerMode'       => false,                                                        // boolean, default `false`
        // 'centerPadding'    => '50px',                                                       // string, default `'50px'`
        // 'cssEase'          => 'ease',                                                       // string, default `'ease'`
        // 'customPaging'     => n/a,                                                          // function, default `n/a`
        // 'dots'             => false,                                                        // boolean, default `false`
        // 'dotsClass'        => 'slick-dots',                                                 // string, default `'slick-dots'`
        // 'draggable'        => true,                                                         // boolean, default `true`
        // 'easing'           => 'linear',                                                     // string, default `'linear'`
        // 'edgeFriction'     => 0.15,                                                         // integer, default `0.15`
        // 'fade'             => false,                                                        // boolean, default `false`
        // 'focusOnSelect'    => false,                                                        // boolean, default `false`
        // 'focusOnChange'    => false,                                                        // boolean, default `false`
        // 'infinite'         => true,                                                         // boolean, default `true`
        // 'initialSlide'     => 0,                                                            // integer, default `0`
        // 'lazyLoad'         => 'ondemand',                                                   // string, default `'ondemand'`
        // 'mobileFirst'      => false,                                                        // boolean, default `false`
        // 'nextArrow'        => <button type="button" class="slick-next">next</button>,       // string (html | jQuery selector) | object (DOM node | jQuery object), default `<button type="button" class="slick-next">next</button>`
        // 'pauseOnDotsHover' => false,                                                        // boolean, default `false`
        // 'pauseOnFocus'     => true,                                                         // boolean, default `true`
        // 'pauseOnHover'     => true,                                                         // boolean, default `true`
        // 'prevArrow'        => <button type="button" class="slick-prev">previous</button>,   // string (html | jQuery selector) | object (DOM node | jQuery object), default `<button type="button" class="slick-prev">previous</button>`
        // 'respondTo'        => 'window',                                                     // string, default `'window'`
        // 'responsive'       => null,                                                         // array, default `null`
        // 'rows'             => 1,                                                            // int, default `1`
        // 'rtl'              => false,                                                        // boolean, default `false`
        // 'slide'            => '',                                                           // string, default `''`
        // 'slidesPerRow'     => 1,                                                            // int, default `1`
        // 'slidesToScroll'   => 1,                                                            // int, default `1`
        // 'slidesToShow'     => 1,                                                            // int, default `1`
        // 'speed'            => 300,                                                          // int, default `300`
        // 'swipe'            => true,                                                         // boolean, default `true`
        // 'swipeToSlide'     => false,                                                        // boolean, default `false`
        // 'touchMove'        => true,                                                         // boolean, default `true`
        // 'touchThreshold'   => 5,                                                            // int, default `5`
        // 'useCSS'           => true,                                                         // boolean, default `true`
        // 'useTransform'     => true,                                                         // boolean, default `true`
        // 'variableWidth'    => false,                                                        // boolean, default `false`
        // 'vertical'         => false,                                                        // boolean, default `false`
        // 'verticalSwiping'  => false,                                                        // boolean, default `false`
        // 'waitForAnimate'   => true,                                                         // boolean, default `true`
        // 'zIndex'           => 1000,                                                         // number, default `1000`
    ],
    'pluginEvents' => [ // array default `[]`, JQuery events
        // @see https://github.com/kenwheeler/slick/#events

        // 'afterChange' => 'function(event, slick, currentSlide) {
        //     console.log("After slide change callback");
        // }',
        // 'beforeChange' => 'function(event, slick, currentSlide, nextSlide) {
        //     console.log("Before slide change callback");
        // }',
        // 'breakpoint' => 'function(event, slick, breakpoint) {
        //     console.log("Fires after a breakpoint is hit");
        // }',
        // 'destroy' => 'function(event, slick) {
        //     console.log("When slider is destroyed, or unslicked.");
        // }',
        // 'edge' => 'function(event, slick, direction) {
        //     console.log("Fires when an edge is overscrolled in non-infinite mode.");
        // }',
        // 'init' => 'function(event, slick) {
        //     console.log("When Slick initializes for the first time callback. Note that this event should be defined before initializing the slider.");
        // }',
        // 'reInit' => 'function(event, slick) {
        //     console.log("Every time Slick (re-)initializes callback");
        // }',
        // 'setPosition' => 'function(event, slick) {
        //     console.log("Every time Slick recalculates position");
        // }',
        // 'swipe' => 'function(event, slick, direction) {
        //     console.log("Fires after swipe/drag");
        // }',
        // 'lazyLoaded' => 'function(event, slick, image, imageSource) {
        //     console.log("Fires after image loads lazily");
        // }',
        // 'lazyLoadError' => 'function(event, slick, image, imageSource) {
        //     console.log("Fires after image fails to load");
        // }',
    ],
]);

Read more docs: https://sugengsulistiyawan.my.id/docs/opensource/yii2/slick/

]]>
0
[extension] diecoding/yii2-seeder Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/diecoding/yii2-seeder https://www.yiiframework.com/extension/diecoding/yii2-seeder die-coding die-coding

Yii2 Seeder

  1. Table of Contents
  2. Instalation
  3. Dependencies
  4. Usage

Create fast-fill database with fake data or bulk data for Yii2.

Latest Stable Version Total Downloads Latest Stable Release Date Quality Score Build Status License PHP Version Require

Table of Contents

Instalation

Package is available on Packagist, you can install it using Composer.

composer require diecoding/yii2-seeder "^1.0"

or add to the require section of your composer.json file.

"diecoding/yii2-seeder": "^1.0"

Dependencies

Usage

Config

Add controllerMap to your config file console/config/main.php or in yii2-basic config/console.php

Basic Config
// ...
'controllerMap' => [
    // ...
    'seeder' => [
        'class' => \diecoding\seeder\SeederController::class,
    ],
    // ...
],
// ...
Advanced Config
// ...
'controllerMap' => [
    // ...
    'seeder' => [
        'class' => \diecoding\seeder\SeederController::class,

        /** @var string the default command action. */
        'defaultAction' => 'seed',

        /** @var string seeder path, support path alias */
        'seederPath' => '@console/seeder',

        /** @var string seeder namespace */
        'seederNamespace' => 'console\seeder',

        /** 
         * @var string this class look like `$this->seederNamespace\Seeder` 
         * default seeder class run if no class selected, 
         * must instance of `\diecoding\seeder\TableSeeder` 
         */
        'defaultSeederClass' => 'Seeder',

        /** @var string tables path, support path alias */
        'tablesPath' => '@console/seeder/tables',

        /** @var string seeder table namespace */
        'tableSeederNamespace' => 'console\seeder\tables',

        /** @var string model namespace */
        'modelNamespace' => 'common\models',

        /** @var string path view template table seeder, support path alias */
        'templateSeederFile' => '@vendor/diecoding/yii2-seeder/src/views/Seeder.php';

        /** @var string path view template seeder, support path alias */
        'templateTableFile' => '@vendor/diecoding/yii2-seeder/src/views/TableSeeder.php';
    ],
    // ...
],
// ...
Commands

yii seeder Seed all tables in Seeder::run() or this seed call $defaultSeederClass::run

yii seeder [name] Seed a table

yii seeder [name]:[funtion_name] Seed a table and run a specific function from selected TableSeeder

yii seeder/create [model_name] Create a TableSeeder in console\seeder\tables\ModelNameTableSeeder

For seeder, if the model is not at the root of the common/models, just add the folder where the model is located inside the common/models directory. You can configure with $modelNamespace config in console\config.

Example:

yii seeder/create entity/user

entity is the folder where User (model) is located inside the common/models directory.

To change the default path for models, just change the $modelNamespace variable in SeederController

Only Seeders within Seeder::run() will be used in yii seeder command

Example
<?php

namespace console\seeder\tables;

use common\models\User;
use common\models\Province;
use diecoding\seeder\TableSeeder;

class UserTableSeeder extends TableSeeder
{
    // public $truncateTable = false;
    // public $locale = 'en_US';

    /**
     * Default execution
     *
     * @return void
     */
    public function run()
    {
        $province = Province::find()->all();

        $count = 100;
        for ($i = 0; $i < $count; $i++) { 
            $this->insert(User::tableName(), [
                'province_id' => $this->faker->randomElement($province)->id,
                'email'       => $this->faker->email,
                'name'        => $this->faker->name,
                'phone'       => $this->faker->phoneNumber,
                'company'     => $this->faker->company,
                'street'      => $this->faker->streetName,
                'zip_code'    => $this->faker->postcode,
                'city'        => $this->faker->city,
                'created_at'  => $this->faker->dateTime(),
                'updated_at'  => $this->faker->dateTime(),
                'status'      => User::STATUS_USER_ACTIVE
            ]);
        }
    }
}

By default, all TableSeeder truncate the table before inserting new data, if you didn't want that to happen in a Seeder, just overwrite $truncateTable:

public $truncateTable = false;

default in TableSeeder:

public $truncateTable = true;

// ...

// truncate table
$this->disableForeignKeyChecks();
$this->truncateTable(/* table names */);
$this->enableForeignKeyChecks();

By default, all TableSeeder faker locale/language get from Yii::$app->language, if you want to use another locale just overwrite $locale:

public $locale = 'en_US';

At the end of every Seeder, if any columns have been forgotten, a message with all the missing columns will appear

    > ########################### MISSING COLUMNS ############################
    > #                                                                      #
    > #    TABLE: {{%user}}                                                  #
    > # -------------------------------------------------------------------- #
    > #    - full_name => varchar(1)                                         #
    > #    - birth_date => date                                              #
    > #    - thumbnail => varchar(64)                                        #
    > # -------------------------------------------------------------------- #
    > #                                                                      #
    > ########################################################################
Seeder

Seeder will be created on first yii seeder if not exist

Here you will put all TableSeeder in ::run()

to run, use yii seeder or yii seeder [name]

Seeder template:

Seeder location in console\seeder

/**
 * Default Seeder
 */
class Seeder extends TableSeeder
{
    /**
     * Default execution
     *
     * @return void
     */
    public function run()
    {
        ModelTableSeeder::create()->run();
    }
}

Read more docs: https://sugengsulistiyawan.my.id/docs/opensource/yii2/seeder/

]]>
0
[extension] diecoding/yii2-dropify Mon, 10 Apr 2023 18:09:04 +0000 https://www.yiiframework.com/extension/diecoding/yii2-dropify https://www.yiiframework.com/extension/diecoding/yii2-dropify die-coding die-coding

Yii2 Dropify

  1. Table of Contents
  2. Instalation
  3. Dependencies
  4. Usage

Override your input files with style for Yii2

Latest Stable Version Total Downloads Latest Stable Release Date Quality Score Build Status License PHP Version Require

Yii2 Dropify uses Dropify
Demo: http://jeremyfagis.github.io/dropify

Table of Contents

Instalation

Package is available on Packagist, you can install it using Composer.

composer require diecoding/yii2-dropify "^1.0"

or add to the require section of your composer.json file.

"diecoding/yii2-dropify": "^1.0"

Dependencies

Usage

Forms/Views
use diecoding\dropify\Dropify;

// Simple
echo Dropify::widget([
    'name' => 'image',
]);

// Advanced
echo Dropify::widget([
    'name' => 'image',
    'options' => [ 
        // options for input widget
    ],
    'pluginOptions' => [ 
        // options for dropify, as output `$(#options['id']).dropify(pluginOptions);`
        // @see https://github.com/JeremyFagis/dropify#options
    ],
    'imgFileExtensions' = [
        // Animated Portable Network Graphics
        'apng',

        // AV1 Image File Format
        'avif',

        // Graphics Interchange Format
        'gif',

        // Joint Photographic Expert Group image
        'jpeg',
        'jpg',
        'jpeg',
        'jfif',
        'pjpeg',
        'pjp',

        // Portable Network Graphics
        'png',

        // Scalable Vector Graphics
        'svg',

        // Web Picture format
        'webp',

        // Bitmap file
        'bmp',

        // Microsoft Icon
        'ico',
        'cur',

        // Tagged Image File Format
        'tif',
        'tiff',
    ],
    'skipCoreAssets' => false, // (bool) default `false`, `true` if use custom or external dropify assets
]);

// Simple with $model / ActiveField
echo $form->field($model, 'image')->widget(Dropify::class);

// Advanced with $model / ActiveField
echo $form->field($model, 'image')->widget(Dropify::class, [
    'options' => [ 
        // options for input widget
    ],
    'pluginOptions' => [ 
        // options for dropify, as output `$(#options['id']).dropify(pluginOptions);`
        // @see https://github.com/JeremyFagis/dropify#options
    ],
    'imgFileExtensions' = [
        // Animated Portable Network Graphics
        'apng',

        // AV1 Image File Format
        'avif',

        // Graphics Interchange Format
        'gif',

        // Joint Photographic Expert Group image
        'jpeg',
        'jpg',
        'jpeg',
        'jfif',
        'pjpeg',
        'pjp',

        // Portable Network Graphics
        'png',

        // Scalable Vector Graphics
        'svg',

        // Web Picture format
        'webp',

        // Bitmap file
        'bmp',

        // Microsoft Icon
        'ico',
        'cur',

        // Tagged Image File Format
        'tif',
        'tiff',
    ],
    'skipCoreAssets' => false, // (bool) default `false`, `true` if use custom or external dropify assets
]);

Read more docs: https://sugengsulistiyawan.my.id/docs/opensource/yii2/dropify/

]]>
0
[extension] diecoding/yii2-barcode-generator Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/diecoding/yii2-barcode-generator https://www.yiiframework.com/extension/diecoding/yii2-barcode-generator die-coding die-coding

Yii2 Barcode Generator

  1. Table of Contents
  2. Instalation
  3. Dependencies
  4. Usage

The JavaScript Barcode Generator for Yii2

Latest Stable Version Total Downloads Latest Stable Release Date Quality Score Build Status License PHP Version Require

Yii2 Barcode Generator uses JsBarcode
Demo: https://lindell.me/JsBarcode/

Table of Contents

Instalation

Package is available on Packagist, you can install it using Composer.

composer require diecoding/yii2-barcode-generator "^1.0"

or add to the require section of your composer.json file.

"diecoding/yii2-barcode-generator": "^1.0"

Dependencies

Usage

Wiki as JavaScript Code at https://github.com/lindell/JsBarcode/wiki#barcodes

Simple Usage
use diecoding\barcode\generator\Barcode;

// CODE128 (auto) is the default mode
Barcode::widget([
  'value' => 'Hi world!',
]);

// CODE128
Barcode::widget([
  'value'  => 'Example1234',
  'format' => Barcode::CODE128
]);

// CODE128A
Barcode::widget([
  'value'  => 'EXAMPLE\n1234',
  'format' => Barcode::CODE128A
]);

// ...

Advanced Usage
use diecoding\barcode\generator\Barcode;

Barcode::widget([
  'value'         => '1234',
  'format'        => Barcode::PHARMACODE,
  'pluginOptions' => [
    'lineColor'    => '#0aa',
    'width'        => 4,
    'height'       => 40,
    'displayValue' => false
  ]
]);

// Enable encoding CODE128 as GS1-128/EAN-128.
Barcode::widget([
  'value'         => '12345678',
  'format'        => Barcode::CODE128C,
  'pluginOptions' => [
    'ean128' => true,
  ]
]);

// Change Element Tag, default svg, available svg, img, canvas
Barcode::widget([
  'tag'           => 'img',
  'value'         => '12345678',
  'format'        => Barcode::CODE128C,
  'pluginOptions' => [
    'ean128' => true,
  ]
]);

// Change Element Tag, add custom style element tag, hide value text
Barcode::widget([
  'tag'     => 'img',
  'value'   => '12345678',
  'options' => [
      'style' => "width: 4cm; height: 1cm;",
  ],
  'pluginOptions' => [
      'displayValue' => false,
  ],
]);

// ...

Read more docs: https://sugengsulistiyawan.my.id/docs/opensource/yii2/barcode-generator/

]]>
0
[extension] diecoding/yii2-aws-s3 Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/diecoding/yii2-aws-s3 https://www.yiiframework.com/extension/diecoding/yii2-aws-s3 die-coding die-coding

Yii2 AWS S3

  1. Table of Contents
  2. Instalation
  3. Dependencies
  4. Configuration
  5. Usage
  6. Using Traits

Amazon S3 or Amazon Simple Storage Service component for Yii2.

Latest Stable Version Total Downloads Latest Stable Release Date Quality Score Build Status License PHP Version Require

Yii2 AWS S3 uses SemVer.

Table of Contents

Instalation

Package is available on Packagist, you can install it using Composer.

composer require diecoding/yii2-aws-s3 "^1.0"

or add to the require section of your composer.json file.

"diecoding/yii2-aws-s3": "^1.0"

Dependencies

Configuration

  1. Add the component to config/main.php

    `php 'components' => [

     // ...
     's3' => [
         'class' => \diecoding\aws\s3\Service::class,
         'endpoint' => 'my-endpoint',
         'usePathStyleEndpoint' => true,
         'credentials' => [ // Aws\Credentials\CredentialsInterface|array|callable
             'key' => 'my-key',
             'secret' => 'my-secret',
         ],
         'region' => 'my-region',
         'defaultBucket' => 'my-bucket',
         'defaultAcl' => 'public-read',
     ],
     // ...
    

    ], `

Usage

Basic Usage
/** @var \diecoding\aws\s3\Service $s3 */
$s3 = Yii::$app->get('s3');
// or 
$s3 = Yii::$app->s3;

// Usage of the command factory and additional params
// ==================================================

/** @var \Aws\ResultInterface $result */
$result = $s3->commands()->get('filename.ext')->saveAs('/path/to/local/file.ext')->execute();

$result = $s3->commands()->put('filename.ext', 'body')->withContentType('text/plain')->execute();

$result = $s3->commands()->delete('filename.ext')->execute();

$result = $s3->commands()->upload('filename.ext', '/path/to/local/file.ext')->withAcl('private')->execute();

$result = $s3->commands()->restore('filename.ext', $days = 7)->execute();

$result = $s3->commands()->list('path/')->execute();

/** @var bool $exist */
$exist = $s3->commands()->exist('filename.ext')->execute();

/** @var string $url */
$url = $s3->commands()->getUrl('filename.ext')->execute();

/** @var string $signedUrl */
$signedUrl = $s3->commands()->getPresignedUrl('filename.ext', '+2 days')->execute();

// Short syntax
// ============

/** @var \Aws\ResultInterface $result */
$result = $s3->get('filename.ext');

$result = $s3->put('filename.ext', 'body');

$result = $s3->delete('filename.ext');

$result = $s3->upload('filename.ext', '/path/to/local/file.ext');

$result = $s3->restore('filename.ext', $days = 7);

$result = $s3->list('path/');

/** @var bool $exist */
$exist = $s3->exist('filename.ext');

/** @var string $url */
$url = $s3->getUrl('filename.ext');

/** @var string $signedUrl */
$signedUrl = $s3->getPresignedUrl('filename.ext', '+2 days');

// Asynchronous execution
// ======================

/** @var \GuzzleHttp\Promise\PromiseInterface $promise */
$promise = $s3->commands()->get('filename.ext')->async()->execute();

$promise = $s3->commands()->put('filename.ext', 'body')->async()->execute();

$promise = $s3->commands()->delete('filename.ext')->async()->execute();

$promise = $s3->commands()->upload('filename.ext', 'source')->async()->execute();

$promise = $s3->commands()->list('path/')->async()->execute();
Advanced Usage
/** @var \diecoding\aws\s3\Service $s3 */
$s3 = Yii::$app->get('s3');
// or 
$s3 = Yii::$app->s3;

/** @var \diecoding\aws\s3\commands\GetCommand $command */
$command = $s3->create(GetCommand::class);
$command->inBucket('my-another-bucket')->byFilename('filename.ext')->saveAs('/path/to/local/file.ext');

/** @var \Aws\ResultInterface $result */
$result = $s3->execute($command);

// or async
/** @var \GuzzleHttp\Promise\PromiseInterface $promise */
$promise = $s3->execute($command->async());
Custom Commands

Commands have two types: plain commands that's handled by the PlainCommandHandler and commands with their own handlers. The plain commands wrap the native AWS S3 commands.

The plain commands must implement the PlainCommand interface and the rest must implement the Command interface. If the command doesn't implement the PlainCommand interface, it must have its own handler.

Every handler must extend the Handler class or implement the Handler interface. Handlers gets the S3Client instance into its constructor.

The implementation of the HasBucket and HasAcl interfaces allows the command builder to set the values of bucket and acl by default.

To make the plain commands asynchronously, you have to implement the Asynchronous interface. Also, you can use the Async trait to implement this interface.

Consider the following command:

<?php

namespace app\components\s3\commands;

use diecoding\aws\s3\base\commands\traits\Options;
use diecoding\aws\s3\interfaces\commands\Command;
use diecoding\aws\s3\interfaces\commands\HasBucket;

class MyCommand implements Command, HasBucket
{
    use Options;

    protected $bucket;

    protected $something;

    public function getBucket()
    {
        return $this->bucket;
    }

    public function inBucket(string $bucket)
    {
        $this->bucket = $bucket;

        return $this;
    }

    public function getSomething()
    {
        return $this->something;
    }

    public function withSomething(string $something)
    {
        $this->something = $something;

        return $this;
    }
}

The handler for this command looks like this:

<?php

namespace app\components\s3\handlers;

use app\components\s3\commands\MyCommand;
use diecoding\aws\s3\base\handlers\Handler;

class MyCommandHandler extends Handler
{
    public function handle(MyCommand $command)
    {
        return $this->s3Client->someAction(
            $command->getBucket(),
            $command->getSomething(),
            $command->getOptions()
        );
    }
}

And usage this command:

/** @var \diecoding\aws\s3\Service $s3 */
$s3 = Yii::$app->get('s3');
// or 
$s3 = Yii::$app->s3;

/** @var \app\components\s3\commands\MyCommand $command */
$command = $s3->create(MyCommand::class);
$command->withSomething('some value')->withOption('OptionName', 'value');

/** @var \Aws\ResultInterface $result */
$result = $s3->execute($command);

Custom plain command looks like this:

<?php

namespace app\components\s3\commands;

use diecoding\aws\s3\interfaces\commands\HasBucket;
use diecoding\aws\s3\interfaces\commands\PlainCommand;

class MyPlainCommand implements PlainCommand, HasBucket
{
    protected $args = [];

    public function getBucket()
    {
        return $this->args['Bucket'] ?? '';
    }

    public function inBucket(string $bucket)
    {
        $this->args['Bucket'] = $bucket;

        return $this;
    }

    public function getSomething()
    {
        return $this->args['something'] ?? '';
    }

    public function withSomething($something)
    {
        $this->args['something'] = $something;

        return $this;
    }

    public function getName(): string
    {
        return 'AwsS3CommandName';
    }

    public function toArgs(): array
    {
        return $this->args;
    }
}

Any command can extend the ExecutableCommand class or implement the Executable interface that will allow to execute this command immediately: $command->withSomething('some value')->execute();.

Using Traits

Model Trait

Attach the Trait to the Model/ActiveRecord with some media attribute that will be saved in S3:

/**
 * @property string|null $file
 */
class Model extends \yii\db\ActiveRecord
{
    use \diecoding\aws\s3\traits\ModelTrait;

    // ...

    public function rules()
    {
        return [
            ['image', 'string'], // Stores the filename
        ];
    }

    /**
     * @inheritdoc
     */
    protected function attributePaths()
    {
        return [
            'image' => 'images/'
        ];
    }

    // ...
}

Override the attributePaths() method to change the base path where the files will be saved on AWS S3.

  • You can map a different path to each file attribute of your Model/ActiveRecord.
Using Trait Methods
$image = \yii\web\UploadedFile::getInstance($model, 'image');

// Save image_thumb.* to S3 on //my_bucket/images/ path
// The extension of the file will be determined by the submitted file type
// This allows multiple file types upload (png,jpg,gif,...)
// $model->image will hold "image_thumb.png" after this call finish with success
$model->saveUploadedFile($image, 'image', 'image_thumb');

// Save image_thumb.png to S3 on //my_bucket/images/ path
// The extension of the file will be determined by the submitted file type
// This force the extension to *.png
$model->saveUploadedFile($image, 'image', 'image_thumb.png', false);

// Get the URL to the image on S3
$model->getFileUrl('image');

// Get the presigned URL to the image on S3
// The default duration is "+5 Minutes"
$model->getFilePresignedUrl('image');

// Remove the file with named saved on the image attribute
// Continuing the example, here "//my_bucket/images/my_image.png" will be deleted from S3
$model->removeFile('image');
Overriding Trait Methods
getS3Component

The S3MediaTrait depends on this component to be configured. The default configuration is to use this component on index 's3', but you may use another value. For this cases, override the getS3Component() method:

public function getS3Component()
{
    return Yii::$app->get('my_s3_component');
}
attributePaths

The main method to override is attributePaths(), which defines a path in S3 for each attribute of yout model. Allowing you to save each attribute in a different S3 folder.

Here an example:

protected function attributePaths()
{
    return [
        'logo' => 'logos/',
        'badge' => 'images/badges/'
    ];
}

// or use another attribute, example: id
// ! Note: id must contain a value first if you don't want it to be empty

protected function attributePaths()
{
    return [
        'logo' => 'thumbnail/' . $this->id . '/logos/',
        'badge' => 'thumbnail/' . $this->id . '/images/badges/'
    ];
}
getPresignedUrlDuration

The default pressigned URL duration is set to "+5 Minutes", override this method and use your own expiration.

protected function getPresignedUrlDuration($attribute)
{
    return '+2 Hours';
}

// or if you want to set the attribute differently

protected function getPresignedUrlDuration($attribute)
{
    switch ($attribute) {
        case 'badge':
            return '+2 Hours';
            break;
        
        default:
            return '+1 Days';
            break;
    }
}

The value should be a valid PHP datetime operation. Read PHP documentation for details

isSuccessResponseStatus

The isSuccessResponseStatus() method validate the AWS response for status codes is 2**. If needed, you can override this validation:

protected function isSuccessResponseStatus($response)
{
    // Response is always valid
    return true;
}

Read more docs: https://sugengsulistiyawan.my.id/docs/opensource/yii2/aws-s3/

]]>
0
[extension] uzdevid/dashboard-modal-page Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/extension/uzdevid/dashboard-modal-page https://www.yiiframework.com/extension/uzdevid/dashboard-modal-page uzdevid uzdevid

Dashboard Modal Page

  1. Installation
  2. Usage

Dashboard Modal Page

Installation

The preferred way to install this extension is through composer.

Either run

composer require --prefer-dist dashboard/modal-page "*"

or add

"dashboard/modal-page": "*"

to the require section of your composer.json file.

Usage

Once the extension is installed, simply use it in your code by :

<?php echo \dashboard\modalpage\ModalPage::widget(); ?>```
]]>
0
[wiki] How to Create and Use Validator Using Regular expressions Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/wiki/2575/how-to-create-and-use-validator-using-regular-expressions https://www.yiiframework.com/wiki/2575/how-to-create-and-use-validator-using-regular-expressions aayushmhu aayushmhu

There are Multiple Ways to Create a Validator But here we use Regular Expression or JavaScript Regular Expression or RegExp for Creation Validators. In this article, we will see the most Frequently Used Expression

Step 1 : Create a New Class for Validator like below or Validator

See First Example 10 Digit Mobile Number Validation

<?php

namespace common\validators;

use yii\validators\Validator;

class MobileValidator extends Validator {

    public function validateAttribute($model, $attribute) {
        if (isset($model->$attribute) and $model->$attribute != '') {
             if (!preg_match('/^[123456789]\d{9}$/', $model->$attribute)) {
                $this->addError($model, $attribute, 'In Valid Mobile / Phone number');
            }
        }
    }

}

Here We can Writee Diffrent Diffrent Regular Expression as Per Requirement `php preg_match('/^[123456789]\d{9}$/', $model->$attribute) `

Step 2: How tO Use Validator

I Hope Everyone Know How to use a validator but here is a example how to use it.

Add a New Rule in your Model Class Like this `php [['mobile'],\common\validators\MobileValidator::class], [['mobile'], 'string', 'max' => 10],


So It's Very Simple to use a Custom Validator.


As I Told you Earlier that i show you some more Example for Using Regular Expression  Validator Just Replace these string in preg_match.

1. Aadhar Number Validator
```php
preg_match('/^[2-9]{1}[0-9]{3}[0-9]{4}[0-9]{4}$/', $model->$attribute)
  1. Bank Account Number Validator `php preg_match("/^[0-9]{9,18}+$/", $model->$attribute) `

  2. Bank IFSC Code Validator `php preg_match("/^[A-Z]{4}0[A-Z0-9]{6}$/", $model->$attribute) `

  3. Pan Card Number Validator `php preg_match('/^([a-zA-Z]){5}([0-9]){4}([a-zA-Z]){1}?$/', $model->$attribute) `

  4. Pin Code Validator `php preg_match('/^[0-9]{6}+$/', $model->$attribute) `

  5. GSTIN Validator `php preg_match("/^([0][1-9]|[1-2][0-9]|[3][0-5])([a-zA-Z]{5}[0-9]{4}[a-zA-Z]{1}[1-9a-zA-Z]{1}[zZ]{1}[0-9a-zA-Z]{1})+$/", $model->$attribute) `

This is Other Type of Custom Validator

  1. 500 Word Validator for a String
<?php

namespace common\validators;

use yii\validators\Validator;

/**
 * Class Word500Validator
 * @author Aayush Saini <aayushsaini9999@gmail.com>
 */
class Word500Validator extends Validator
{

    public function validateAttribute($model, $attribute)
    {
        if ($model->$attribute != '') {
            if (str_word_count($model->$attribute) > 500) {
                $this->addError($model, $attribute, $model->getAttributeLabel($attribute) . ' length can not exceeded 500 words.');
                \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
                return $model->errors;
            }
        }
    }
}

Now I assume that after reading this article you can create any type of validator as per your Requirement.

:) Thanks for Reading

]]>
0
[wiki] GridView show sum of columns in footer. Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/wiki/2574/gridview-show-sum-of-columns-in-footer https://www.yiiframework.com/wiki/2574/gridview-show-sum-of-columns-in-footer shivam4u shivam4u

GridView show sum of columns in footer `PHP use yii\grid\DataColumn;

/**

  • Sum of all the values in the column
  • @author shiv / class TSumColumn extends DataColumn { public function getDataCellValue($model, $key, $index) {

     $value = parent::getDataCellValue($model, $key, $index);
     if ( is_numeric($value))
     {
         $this->footer += $value;
     }
        
     return $value;
    

    } } `

Now you have to enable footer in GridView

echo GridView::widget([
        'dataProvider' => $dataProvider,
        'filterModel' => $searchModel,
        'showFooter' => true,

Also change the coulmn class

            [
                'class' => TSumColumn::class,
                'attribute' => 'amount'
            ],

You would see the total in footer of the grid. you can apply this to multiple columns if need

]]>
0
[wiki] Convert JSON data to html table for display on page Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/wiki/2573/convert-json-data-to-html-table-for-display-on-page https://www.yiiframework.com/wiki/2573/convert-json-data-to-html-table-for-display-on-page shivam4u shivam4u

I have a calls which help me display json directly in html table.

Json2Table::formatContent($json);

The code of Json2Table class is here

============================================

/**
 * Class convert Json to html table. It help view json data directly.
 * @author shiv
 *
 */
class Json2Table
{

    public static function formatContent($content, $class = 'table table-bordered')
    {
        $html = "";
        if ($content != null) {
            $arr = json_decode(strip_tags($content), true);
            
            if ($arr && is_array($arr)) {
                $html .= self::arrayToHtmlTableRecursive($arr, $class);
            }
        }
        return $html;
    }

    public static function arrayToHtmlTableRecursive($arr, $class = 'table table-bordered')
    {
        $str = "<table class='$class'><tbody>";
        foreach ($arr as $key => $val) {
            $str .= "<tr>";
            $str .= "<td>$key</td>";
            $str .= "<td>";
            if (is_array($val)) {
                if (! empty($val)) {
                    $str .= self::arrayToHtmlTableRecursive($val, $class);
                }
            } else {
                $val = nl2br($val);
                $str .= "<strong>$val</strong>";
            }
            $str .= "</td></tr>";
        }
        $str .= "</tbody></table>";
        
        return $str;
    }
}
]]>
0
[wiki] Aadhar Number Validator Sun, 03 Dec 2023 17:03:33 +0000 https://www.yiiframework.com/wiki/2572/aadhar-number-validator https://www.yiiframework.com/wiki/2572/aadhar-number-validator shivam4u shivam4u

In India have Aadhar number an we may need to valid it a input. So I created a validator for yii2

use yii\validators\Validator;

class TAadharNumberValidator extends Validator
{

    public $regExPattern = '/^\d{4}\s\d{4}\s\d{4}$/';

    public function validateAttribute($model, $attribute)
    {
        if (preg_match($this->regExPattern, $model->$attribute)) {
            $model->addError($attribute, 'Not valid Aadhar Card Number');
        }
    }
}
]]>
0
[wiki] Interview Questions For YII2 Wed, 01 Nov 2023 06:05:47 +0000 https://www.yiiframework.com/wiki/2570/interview-questions-for-yii2 https://www.yiiframework.com/wiki/2570/interview-questions-for-yii2 aayushmhu aayushmhu

Hey Everyone, In this post I Just shared my Experience what most of interviewer ask in YII2 Interview.

  1. What is Active Record? and How we use that?
  2. What is Components ?
  3. What is Helpers Functions?
  4. How to Update Data Model?
  5. Diffrence Between Authentication and Authorization ?
  6. How to Speed Up a Website?
  7. What is GII? or do you Use GII Module?
  8. What is diffrence between YII and YII2?
  9. How to Use Multiple Databases?
  10. How to Intergate a theme into Website?
  11. What is OOPS?
  12. What is final class in php?
  13. What is abstract class?
  14. What is inheritance?
  15. What is Interface?
  16. Do you have knowledege of Javascript and Jquery?
  17. What is trait?
  18. What is Bootstrapping?
  19. What is Diffrence Between advanced and basic of YII2?
  20. How to use YII2 as a Micro framework?
  21. What is REST APIs?, How to write in YII2?
  22. Directory Structure of YII2 Project?
  23. Diffrence Between render, renderFile, renderPartial, renderAjax, renderContent?

These are most common question a interviewer can be asked to you if you are going to a Interview.

If anyone have other question please share in comments!!!!

]]>
0
[wiki] How to send email via Gmail SMTP in Yii2 framework Wed, 04 Aug 2021 13:00:37 +0000 https://www.yiiframework.com/wiki/2569/how-to-send-email-via-gmail-smtp-in-yii2-framework https://www.yiiframework.com/wiki/2569/how-to-send-email-via-gmail-smtp-in-yii2-framework PELock PELock
  1. Gmail won't unblock your domain... thanks Google
  2. How to send emails to @gmail.com boxes anyway?
  3. 1. Setup a helper @gmail.com account
  4. 2. Add custom component in your configuration file
  5. 3. Add helper function
  6. 4. Usage
  7. 5. Know the limits
  8. 6. Gmail is not your friend

One of my sites has been flooded with spam bots and as a result - Gmail gave my mailing domain a bad score and I couldn't send emails to @gmail addresses anymore, not from my email, not from my system, not from any of other domains and websites I host...

Gmail won't unblock your domain... thanks Google

I did remove all the spambots activity from one of my sites, appealed the decision via Gmail support forums, but still, I'm blocked from contacting my customers that has mailboxes at @gmail.com and there seems to be no way to change the domain score back to where it was.

It's been almost 2 weeks and my domain score is stuck at bad in https://postmaster.google.com/

Thanks @Google :(

How to send emails to @gmail.com boxes anyway?

As a result, I had to figure way out to send purchases, expired licenses, and other notifications to my customers.

I'm using PHP Yii2 framework and it turns out it was a breeze.

1. Setup a helper @gmail.com account

We need a @gmail.com account to send the notifications. One thing is important. After you create the account, you need to enable Less Secure Apps Access option:

Gmail options

It allows us to send emails via Gmail SMTP server.

2. Add custom component in your configuration file

In your Yii2 framework directory, modify your configuration file /common/config/Main.php (I'm using Advanced Theme) and include custom mailing component (name it however you want):

<?php
return [
	'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',

	...

	'components' => [

		'mailerGmail' => [
			'class' => 'yii\swiftmailer\Mailer',
			'viewPath' => '@common/mail',
			'useFileTransport' => false,

			'transport' => [
				'class' => 'Swift_SmtpTransport',
				'host' => 'smtp.gmail.com',
				'username' => 'gmail.helper.account',
				'password' => 'PUT-YOUR-PASSWORD-HERE',
				'port' => '587',
				'encryption' => 'tls',
			],
		],
    ],
];

3. Add helper function

I have added a helper function to one of my components registered as Yii::$app->Custom. It returns default mailer instance depending on the delivery email domain name.

I have also updated the code to detect the cases where the email doesn't contain @gmail.com string in it but still is using Gmail MX servers to handle emailing.

Detection is based on checking domain mailing server records using PHP built-in function getmxrr() and if that fails I send remote GET query to Google DNS service API to check the MX records.

////////////////////////////////////////////////////////////////////////////////
//
// get default mailer depending on the provided email address
//
////////////////////////////////////////////////////////////////////////////////

public function getMailer($email)
{
	// detect if the email or domain is using Gmail to send emails
	if (Yii::$app->params['forwardGmail'])
	{
		// detect @gmail.com domain first
		if (str_ends_with($email, "@gmail.com"))
		{
			return Yii::$app->mailerGmail;
		}

		// extract domain name
		$parts = explode('@', $email);
		$domain = array_pop($parts);

		// check DNS using local server requests to DNS
		// if it fails query Google DNS service API (might have limits)
		if (getmxrr($domain, $mx_records))
		{
			foreach($mx_records as $record)
			{
				if (stripos($record, "google.com") !== false || stripos($record, "googlemail.com") !== false)
				{
					return Yii::$app->mailerGmail;
				}
			}

			// return default mailer (if there were records detected but NOT google)
			return Yii::$app->mailer;
		}

		// make DNS request
		$client = new Client();

		$response = $client->createRequest()
			->setMethod('GET')
			->setUrl('https://dns.google.com/resolve')
			->setData(['name' => $domain, 'type' => 'MX'])
			->setOptions([
				'timeout' => 5, // set timeout to 5 seconds for the case server is not responding
			])
			->send();

		if ($response->isOk)
		{
			$parser = new JsonParser();

			$data = $parser->parse($response);

			if ($data && array_key_exists("Answer", $data))
			{
				foreach ($data["Answer"] as $key => $value)
				{
					if (array_key_exists("name", $value) && array_key_exists("data", $value))
					{
						if (stripos($value["name"], $domain) !== false)
						{
							if (stripos($value["data"], "google.com") !== false || stripos($value["data"], "googlemail.com") !== false)
							{
								return Yii::$app->mailerGmail;
							}
						}
					}
				}
			}
		}
	}

	// return default mailer
	return Yii::$app->mailer;
}

If the domain ends with @gmail.com or the domain is using Gmail mailing systems the mailerGmail instance is used, otherwise the default mailing component Yii::$app->mailer is used.

4. Usage

    /**
     * Sends an email to the specified email address using the information collected by this model.
     *
     * @return boolean whether the email was sent
     */
    public function sendEmail()
    {
		// find all active subscribers
		$message = Yii::$app->Custom->getMailer($this->email)->compose();
	
		$message->setTo([$this->email => $this->name]);
		$message->setFrom([\Yii::$app->params['supportEmail'] => "Bartosz Wójcik"]);
		$message->setSubject($this->subject);
		$message->setTextBody($this->body);
	
		$headers = $message->getSwiftMessage()->getHeaders();
	
		// message ID header (hide admin panel)
		$msgId = $headers->get('Message-ID');
		$msgId->setId(md5(time()) . '@pelock.com');
	
		$result = $message->send();
	
		return $result;
    }

5. Know the limits

This is only the temporary solution and you need to be aware you won't be able to send bulk mail with this method, Gmail enforces some limitations on fresh mailboxes too.

6. Gmail is not your friend

It seems if your domain lands on that bad reputation scale there isn't any easy way out of it. I read on Gmail support forums, some people wait for more than a month for Gmail to unlock their domains without any result and communication back. My domain is not listed in any other blocked RBL lists (spam lists), it's only Gmail blocking it, but it's enough to understand how influential Google is, it can ruin your business in a second without a chance to fix it...

]]>
0
[wiki] JWT authentication tutorial Sun, 03 Oct 2021 17:59:49 +0000 https://www.yiiframework.com/wiki/2568/jwt-authentication-tutorial https://www.yiiframework.com/wiki/2568/jwt-authentication-tutorial allanbj allanbj

How to implement JWT

  1. The JWT Concept
  2. Scenarios
  3. User logs in for the first time, via the /auth/login endpoint:
  4. Token expired:
  5. My laptop got stolen:
  6. Why do we trust the JWT blindly?
  7. Implementation Steps
  8. Prerequisites
  9. Step-by-step setup
  10. Client-side examples

The JWT Concept

JWT is short for JSON Web Token. It is used eg. instead of sessions to maintain a login in a browser that is talking to an API - since browser sessions are vulnerable to CSRF security issues. JWT is also less complicated than setting up an OAuth authentication mechanism.

The concept relies on two tokens:

  • AccessToken - a short-lived JWT (eg. 5 minutes)

This token is generated using \sizeg\jwt\Jwt::class It is not stored server side, and is sent on all subsequent API requests through the Authorization header How is the user identified then? Well, the JWT contents contain the user ID. We trust this value blindly.

  • RefreshToken - a long-lived, stored in database

This token is generated upon login only, and is stored in the table user_refresh_token. A user may have several RefreshToken in the database.

Scenarios

User logs in for the first time, via the /auth/login endpoint:

In our actionLogin() method two things happens, if the credentials are correct:

  • The JWT AccessToken is generated and sent back through JSON. It is not stored anywhere server-side, and contains the user ID (encoded).
  • The RefreshToken is generated and stored in the database. It's not sent back as JSON, but rather as a httpOnly cookie, restricted to the /auth/refresh-token path.

The JWT is stored in the browser's localStorage, and have to be sent on all requests from now on. The RefreshToken is in your cookies, but can't be read/accessed/tempered with through Javascript (since it is httpOnly).

Token expired:

After some time, the JWT will eventually expire. Your API have to return 401 - Unauthorized in this case. In your app's HTTP client (eg. Axios), add an interceptor, which detects the 401 status, stores the failing request in a queue, and calls the /auth/refresh-token endpoint.

When called, this endpoint will receive the RefreshToken via the cookie. You then have to check in your table if this is a valid RefreshToken, who is the associated user ID, generate a new JWT and send it back as JSON.

Your HTTP client must take this new JWT, replace it in localStorage, and then cycle through the request queue and replay all failed requests.

My laptop got stolen:

If you set up an /auth/sessions endpoint, that returns all the current user's RefreshTokens, you can then display a table of all connected devices.

You can then allow the user to remove a row (i.e. DELETE a particular RefreshToken from the table). When the compromised token expires (after eg. 5 min) and the renewal is attempted, it will fail. This is why we want the JWT to be really short lived.

Why do we trust the JWT blindly?

This is by design the purpose of JWT. It is secure enough to be trustable. In big setups (eg. Google), the Authentication is handled by a separate authentication server. It's responsible for accepting a login/password in exchange for a token.

Later, in Gmail for example, no authentication is performed at all. Google reads your JWT and give you access to your email, provided your JWT is not dead. If it is, you're redirected to the authentication server.

This is why when Google authentication had a failure some time ago - some users were able to use Gmail without any problems, while others couldn't connect at all - JWT still valid versus an outdated JWT.

Implementation Steps

Prerequisites

  • Yii2 installed
  • An https enabled site is required for the HttpOnly cookie to work cross-site
  • A database table for storing RefreshTokens:
CREATE TABLE `user_refresh_tokens` (
	`user_refresh_tokenID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`urf_userID` INT(10) UNSIGNED NOT NULL,
	`urf_token` VARCHAR(1000) NOT NULL,
	`urf_ip` VARCHAR(50) NOT NULL,
	`urf_user_agent` VARCHAR(1000) NOT NULL,
	`urf_created` DATETIME NOT NULL COMMENT 'UTC',
	PRIMARY KEY (`user_refresh_tokenID`)
)
COMMENT='For JWT authentication process';
  • Install package: composer require sizeg/yii2-jwt
  • For the routes login/logout/refresh etc we'll use a controller called AuthController.php. You can name it what you want.

Step-by-step setup

  • Create an ActiveRecord model for the table user_refresh_tokens. We'll use the class name app\models\UserRefreshToken.

  • Disable CSRF validation on all your controllers:

Add this property: public $enableCsrfValidation = false;

  • Add JWT parameters in /config/params.php:
'jwt' => [
	'issuer' => 'https://api.example.com',  //name of your project (for information only)
	'audience' => 'https://frontend.example.com',  //description of the audience, eg. the website using the authentication (for info only)
	'id' => 'UNIQUE-JWT-IDENTIFIER',  //a unique identifier for the JWT, typically a random string
	'expire' => 300,  //the short-lived JWT token is here set to expire after 5 min.
],
  • Add JwtValidationData class in /components which uses the parameters we just set:
<?php
namespace app\components;

use Yii;

class JwtValidationData extends \sizeg\jwt\JwtValidationData {
	/**
	 * @inheritdoc
	 */
	public function init() {
		$jwtParams = Yii::$app->params['jwt'];
		$this->validationData->setIssuer($jwtParams['issuer']);
		$this->validationData->setAudience($jwtParams['audience']);
		$this->validationData->setId($jwtParams['id']);

		parent::init();
	}
}
  • Add component in configuration in /config/web.php for initializing JWT authentication:
	$config = [
		'components' => [
			...
			'jwt' => [
				'class' => \sizeg\jwt\Jwt::class,
				'key' => 'SECRET-KEY',  //typically a long random string
				'jwtValidationData' => \app\components\JwtValidationData::class,
			],
			...
		],
	];
  • Add the authenticator behavior to your controllers
    • For AuthController.php we must exclude actions that do not require being authenticated, like login, refresh-token, options (when browser sends the cross-site OPTIONS request).
	public function behaviors() {
    	$behaviors = parent::behaviors();

		$behaviors['authenticator'] = [
			'class' => \sizeg\jwt\JwtHttpBearerAuth::class,
			'except' => [
				'login',
				'refresh-token',
				'options',
			],
		];

		return $behaviors;
	}
  • Add the methods generateJwt() and generateRefreshToken() to AuthController.php. We'll be using them in the login/refresh-token actions. Adjust class name for your user model if different.
	private function generateJwt(\app\models\User $user) {
		$jwt = Yii::$app->jwt;
		$signer = $jwt->getSigner('HS256');
		$key = $jwt->getKey();
		$time = time();

		$jwtParams = Yii::$app->params['jwt'];

		return $jwt->getBuilder()
			->issuedBy($jwtParams['issuer'])
			->permittedFor($jwtParams['audience'])
			->identifiedBy($jwtParams['id'], true)
			->issuedAt($time)
			->expiresAt($time + $jwtParams['expire'])
			->withClaim('uid', $user->userID)
			->getToken($signer, $key);
	}

	/**
	 * @throws yii\base\Exception
	 */
	private function generateRefreshToken(\app\models\User $user, \app\models\User $impersonator = null): \app\models\UserRefreshToken {
		$refreshToken = Yii::$app->security->generateRandomString(200);

		// TODO: Don't always regenerate - you could reuse existing one if user already has one with same IP and user agent
		$userRefreshToken = new \app\models\UserRefreshToken([
			'urf_userID' => $user->id,
			'urf_token' => $refreshToken,
			'urf_ip' => Yii::$app->request->userIP,
			'urf_user_agent' => Yii::$app->request->userAgent,
			'urf_created' => gmdate('Y-m-d H:i:s'),
		]);
		if (!$userRefreshToken->save()) {
			throw new \yii\web\ServerErrorHttpException('Failed to save the refresh token: '. $userRefreshToken->getErrorSummary(true));
		}

		// Send the refresh-token to the user in a HttpOnly cookie that Javascript can never read and that's limited by path
		Yii::$app->response->cookies->add(new \yii\web\Cookie([
			'name' => 'refresh-token',
			'value' => $refreshToken,
			'httpOnly' => true,
			'sameSite' => 'none',
			'secure' => true,
			'path' => '/v1/auth/refresh-token',  //endpoint URI for renewing the JWT token using this refresh-token, or deleting refresh-token
		]));

		return $userRefreshToken;
	}
  • Add the login action to AuthController.php:
	public function actionLogin() {
		$model = new \app\models\LoginForm();
		if ($model->load(Yii::$app->request->getBodyParams()) && $model->login()) {
			$user = Yii::$app->user->identity;

			$token = $this->generateJwt($user);

			$this->generateRefreshToken($user);

			return [
				'user' => $user,
				'token' => (string) $token,
			];
		} else {
			return $model->getFirstErrors();
		}
	}
  • Add the refresh-token action to AuthController.php. Call POST /auth/refresh-token when JWT has expired, and call DELETE /auth/refresh-token when user requests a logout (and then delete the JWT token from client's localStorage).
	public function actionRefreshToken() {
		$refreshToken = Yii::$app->request->cookies->getValue('refresh-token', false);
		if (!$refreshToken) {
			return new \yii\web\UnauthorizedHttpException('No refresh token found.');
		}

		$userRefreshToken = \app\models\UserRefreshToken::findOne(['urf_token' => $refreshToken]);

		if (Yii::$app->request->getMethod() == 'POST') {
			// Getting new JWT after it has expired
			if (!$userRefreshToken) {
				return new \yii\web\UnauthorizedHttpException('The refresh token no longer exists.');
			}

			$user = \app\models\User::find()  //adapt this to your needs
				->where(['userID' => $userRefreshToken->urf_userID])
				->andWhere(['not', ['usr_status' => 'inactive']])
				->one();
			if (!$user) {
				$userRefreshToken->delete();
				return new \yii\web\UnauthorizedHttpException('The user is inactive.');
			}

			$token = $this->generateJwt($user);

			return [
				'status' => 'ok',
				'token' => (string) $token,
			];

		} elseif (Yii::$app->request->getMethod() == 'DELETE') {
			// Logging out
			if ($userRefreshToken && !$userRefreshToken->delete()) {
				return new \yii\web\ServerErrorHttpException('Failed to delete the refresh token.');
			}

			return ['status' => 'ok'];
		} else {
			return new \yii\web\UnauthorizedHttpException('The user is inactive.');
		}
	}
  • Adapt findIdentityByAccessToken() in your user model to find the authenticated user via the uid claim from the JWT:
	public static function findIdentityByAccessToken($token, $type = null) {
		return static::find()
			->where(['userID' => (string) $token->getClaim('uid') ])
			->andWhere(['<>', 'usr_status', 'inactive'])  //adapt this to your needs
			->one();
	}
  • Also remember to purge all RefreshTokens for the user when the password is changed, eg. in afterSave() in your user model:
	public function afterSave($isInsert, $changedOldAttributes) {
		// Purge the user tokens when the password is changed
		if (array_key_exists('usr_password', $changedOldAttributes)) {
			\app\models\UserRefreshToken::deleteAll(['urf_userID' => $this->userID]);
		}

		return parent::afterSave($isInsert, $changedOldAttributes);
	}
  • Make a page where user can delete his RefreshTokens. List the records from user_refresh_tokens that belongs to the given user and allow him to delete the ones he chooses.

Client-side examples

The Axios interceptor (using React Redux???):


let isRefreshing = false;
let refreshSubscribers: QueuedApiCall[] = [];
const subscribeTokenRefresh = (cb: QueuedApiCall) =>
  refreshSubscribers.push(cb);

const onRefreshed = (token: string) => {
  console.log("refreshing ", refreshSubscribers.length, " subscribers");
  refreshSubscribers.map(cb => cb(token));
  refreshSubscribers = [];
};

api.interceptors.response.use(undefined,
  error => {
    const status = error.response ? error.response.status : false;
    const originalRequest = error.config;

    if (error.config.url === '/auth/refresh-token') {
      console.log('REDIRECT TO LOGIN');
      store.dispatch("logout").then(() => {
          isRefreshing = false;
      });
    }

    if (status === API_STATUS_UNAUTHORIZED) {


      if (!isRefreshing) {
        isRefreshing = true;
        console.log('dispatching refresh');
        store.dispatch("refreshToken").then(newToken => {
          isRefreshing = false;
          onRefreshed(newToken);
        }).catch(() => {
          isRefreshing = false;
        });
      }

      return new Promise(resolve => {
        subscribeTokenRefresh(token => {
          // replace the expired token and retry
          originalRequest.headers["Authorization"] = "Bearer " + token;
          resolve(axios(originalRequest));
        });
      });
    }
    return Promise.reject(error);


  }
);

Thanks to Mehdi Achour for helping with much of the material for this tutorial.

]]>
0
[wiki] Yii v2 snippet guide III Mon, 17 Jul 2023 11:37:41 +0000 https://www.yiiframework.com/wiki/2567/yii-v2-snippet-guide-iii https://www.yiiframework.com/wiki/2567/yii-v2-snippet-guide-iii rackycz rackycz
  1. My articles
  2. Switching languages and Language in URL
  3. Search and replace
  4. Virtualization - Vagrant and Docker - why and how
  5. Running Yii project in Vagrant. (Simplified version)
  6. Running Yii project in Docker (Update: xDebug added below!)
  7. Enabling xDebug in Docker, yii demo application
  8. Docker - Custom php.ini
  9. How to enter Docker's bash (cli, command line)
  10. AdminLTE - overview & general research on the theme
  11. Creating custom Widget
  12. Tests - unit + functional + acceptance (opa) + coverage
  13. Microsoft Access MDB
  14. Migration batch insert csv

My articles

Articles are separated into more files as there is the max lenght for each file on wiki.

Switching languages and Language in URL

I already wrote how translations work. Here I will show how language can be switched and saved into the URL. So let's add the language switcher into the main menu:

echo Nav::widget([
 'options' => ['class' => 'navbar-nav navbar-right'],
 'items' => [
  ['label' => 'Language', 'items' => [
    ['label' => 'German' , 'url' => \yii\helpers\Url::current(['sys_lang' => 'de']) ],
    ['label' => 'English', 'url' => \yii\helpers\Url::current(['sys_lang' => 'en']) ],
   ],
  ]

Now we need to process the new GET parameter "sys_lang" and save it to Session in order to keep the new language. Best is to create a BaseController which will be extended by all controllers. Its content looks like this:

<?php
namespace app\controllers;
use yii\web\Controller;
class _BaseController extends Controller {
  public function beforeAction($action) {
    if (isset($_GET['sys_lang'])) {
      switch ($_GET['sys_lang']) {
        case 'de':
          $_SESSION['sys_lang'] = 'de-DE';
          break;
        case 'en':
          $_SESSION['sys_lang'] = 'en-US';
          break;
      }
    }
    if (!isset($_SESSION['sys_lang'])) {
      $_SESSION['sys_lang'] = \Yii::$app->sourceLanguage;
    }
    \Yii::$app->language = $_SESSION['sys_lang'];
    return true;
  }
}

If you want to have the sys_lang in the URL, right behind the domain name, following URL rules can be created in config/web.php:

'components' => [
 // ...
 'urlManager' => [
  'enablePrettyUrl' => true,
  'showScriptName' => false,
  'rules' => [
   // https://www.yiiframework.com/doc/api/2.0/yii-web-urlmanager#$rules-detail
   // https://stackoverflow.com/questions/2574181/yii-urlmanager-language-in-url
   // https://www.yiiframework.com/wiki/294/seo-conform-multilingual-urls-language-selector-widget-i18n
   '<sys_lang:[a-z]{2}>' => 'site',
   '<sys_lang:[a-z]{2}>/<controller:\w+>' => '<controller>',
   '<sys_lang:[a-z]{2}>/<controller:\w+>/<action:\w+>' => '<controller>/<action>',
  ],
 ],
],

Now the language-switching links will produce URL like this: http://myweb.com/en/site/index . Without the rules the link would look like this: http://myweb.com/site/index?sys_lang=en . So the rule works in both directions. When URL is parsed and controllers are called, but also when a new URL is created using the URL helper.

Search and replace

I am using Notepad++ for massive changes using Regex. If you press Ctrl+Shift+F you will be able to replace in all files.

Yii::t()

Yii::t('text'  ,  'text'   ) // NO
Yii::t('text','text') // YES

search: Yii::t\('([^']*)'[^']*'([^']*)'[^\)]*\)
replace with: Yii::t\('$1','$2'\)

URLs (in Notepad++)

return $this->redirect('/controller/action')->send(); // NO
return $this->redirect(['controller/action'])->send(); // YES

search: ->redirect\(['][/]([^']*)[']\)
replace: ->redirect\(['$1']\)

====

return $this->redirect('controller/action')->send(); // NO
return $this->redirect(['controller/action'])->send(); // YES

search: ->redirect\((['][^']*['])\)
replace: ->redirect\([$1]\)

PHP short tags

search: (<\?)([^p=]) // <?if ...
replace: $1php $2 // <?php if ...
// note that sometimes <?xml can be found and it is valid, keep it

View usage

search: render(Ajax|Partial)?\s*\(\s*['"]\s*[a-z0-9_\/]*(viewName)

Virtualization - Vagrant and Docker - why and how

Both Vagrant and Docker create a virtual machine using almost any OS or SW configuration you specify, while the source codes are on your local disk so you can easily modify them in your IDE under your OS.

Can be used not only for PHP development, but in any other situation.

What is this good for? ... Your production server runs a particular environment and you want to develop/test on the same system. Plus you dont have to install XAMPP, LAMP or other servers locally. You just start the virtual and its ready. Plus you can share the configuration of the virtual system with other colleagues so you all work on indentical environment. You can also run locally many different OS systems with different PHP versions etc.

Vagrant and Docker work just like composer or NPM. It is a library of available OS images and other SW and you just pick some combination. Whole configuration is defined in one text-file, named Vagrantfile or docker-compose.yml, and all you need is just a few commands to run it. And debugging is no problem.

Running Yii project in Vagrant. (Simplified version)

Info: This chapter works with PHP 7.0 in ScotchBox. If you need PHP 7.4, read next chapter where CognacBox is used (to be added when tested)

Basic overview and Vagrant configuration:

List of all available OS images for Vagrant is here:

Both Yii demo-applications already contain the Vagrantfile, but its setup is unclear to me - it is too PRO. So I wanted to publish my simplified version which uses OS image named scotch/box and you can use it also for non-yii PHP projects. (It has some advantages, the disadvantage is older PHP in the free version)

The Vagrantfile is stored in the root-folder of your demo-project. My Vagrantfile contains only following commands.

Vagrant.configure("2") do |config|
    config.vm.box = "scotch/box"
    config.vm.network "private_network", ip: "11.22.33.44"
    config.vm.hostname = "scotchbox"
    config.vm.synced_folder ".", "/var/www/public", :mount_options => ["dmode=777", "fmode=777"]
    config.vm.provision "shell", path: "./vagrant/vagrant.sh", privileged: false
end

# Virtual machine will be available on IP A.B.C.D (in our case 11.22.33.44, see above)
# Virtual can access your host machine on IP A.B.C.1 (this rule is given by Vagrant)

It requires file vagrant/vagrant.sh, because I wanted to enhance the server a bit. It contains following:


# Composer:
# (In case of composer errors, it can help to delete the vendor-folder and composer.lock file)
cd /var/www/public/
composer install

# You can automatically import your SQL (root/root, dbname scotchbox)
#mysql -u root -proot scotchbox < /var/www/public/vagrant/db.sql

# You can run migrations:
#php /var/www/public/protected/yiic.php migrate --interactive=0

# You can create folder and set 777 rights:
#mkdir /var/www/public/assets
#sudo chmod -R 777 /var/www/public/assets

# You can copy a file:
#cp /var/www/public/from.php /var/www/public/to.php

# Installing Xdebug v2 (Xdebug v3 has renamed config params!):
sudo apt-get update
sudo apt-get install php-xdebug

# Configuring Xdebug in php.ini:
# If things do not work, disable your firewall and restart IDE. It might help.
echo "" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "[XDebug]" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_enable=1" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_port=9000" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_autostart=1" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_log=/var/www/public/xdebug.log" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_connect_back=1" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.idekey=netbeans-xdebug" | sudo tee -a /etc/php/7.0/apache2/php.ini

# Important: Make sure that your IDE has identical settings: idekey and remote_port.
# NetBeans: Make sure your project is correctly setup. Right-click the project and select Properties / Run Cofigurations. "Project URL" and "Index file" must have correct values.

# Note:
# Use this if remote_connect_back does not work. 
# IP must correspond to the Vagrantfile, only the last number must be 1
#echo "xdebug.remote_handler=dbgp" | sudo tee -a /etc/php/7.0/apache2/php.ini
#echo "xdebug.remote_host=11.22.33.1" | sudo tee -a /etc/php/7.0/apache2/php.ini 

sudo service apache2 restart

... so create both files in your project ...

If you want to manually open php.ini and paste this text, you can copy it from here:

// sudo nano /etc/php/7.0/apache2/php.ini
// (Xdebug v3 has renamed config params!)

[XDebug]
xdebug.remote_enable=1
xdebug.remote_port=9000
xdebug.remote_autostart=1
xdebug.remote_log=/var/www/public/xdebug.log
xdebug.remote_connect_back=1
xdebug.idekey=netbeans-xdebug

// Important: Make sure that your IDE has identical settings: idekey and remote_port.
// NetBeans: Make sure your project is correctly setup. Right-click the project and select Properties / Run Cofigurations. "Project URL" and "Index file" must have correct values.

To debug in PhpStorm check this video.

To connect to MySQL via PhpStorm check this comment by MilanG

Installing and using Vagrant:

First install Vagrant and VirtualBox, please.

Note: Sadly, these days VirtualBox does not work on the ARM-based Macs with the M1 chip. Use Docker in that case.

Important: If command "vagrant ssh" wants a password, enter "vagrant".

Now just open your command line, navigate to your project and you can start:

  • "vagrant -v" should show you the version if things work.
  • "vagrant init" creates a new project (You won't need it now)
  • "vagrant up" runs the Vagrantfile and creates/starts the virtual

Once virtual is running, you can call also these:

  • "vagrant ssh" opens Linux shell - use password "vagrant" is you are prompted.
  • "vagrant halt" stops the virtual
  • "vagrant reload" restarts the virtual and does NOT run config.vm.provision OR STARTS EXISTING VAGRANT VIRTUAL - you do not have to call "vagrant up" whenever you reboot your PC
  • "vagrant reload --provision" restarts the virtual and runs config.vm.provision

In the Linux shell you can call any command you want.

  • To find what Linux version is installed: "cat /etc/os-release" or "lsb_release -a" or "hostnamectl"
  • To get PHP version call: "php -version"
  • If you are not allowed to run "mysql -v", you can run "mysql -u {username} -p" .. if you know the login
  • Current IP: hostname -I

In "scotch/box" I do not use PhpMyAdmin , but Adminer. It is one simple PHP script and it will run without any installations. Just copy the adminer.php script to your docroot and access it via browser. Use the same login as in configurafion of Yii. Server will be localhost.

Running Yii project in Docker (Update: xDebug added below!)

Note: I am showing the advanced application. Basic application will not be too different I think. Great Docker tutorial is here

Yii projects are already prepared for Docker. To start you only have to install Docker from www.docker.com and you can go on with this manual.

  • Download the application template and extract it to any folder
  • Open command line and navigate to the project folder
  • Run command docker-compose up -d
    • Argument -d will run docker on the background as a service
    • Advantage is that command line will not be blocked - you will be able to call more commands
  • Run command init to initialize the application
  • You can also call composer install using one of following commands:
    • docker-compose run --rm frontend composer install
    • docker-compose run --rm backend composer install

Note: init and composer can be called locally, not necessarily via Docker. They only add files to your folder.

Now you will be able to open URLs:

Open common/config/main-local.php and set following DB connection:

  • host=mysql !!
  • dbname=yii2advanced
  • username=yii2advanced
  • password=secret
  • Values are taken from docker-compose.yml

Run migrations using one of following commands:

  • docker-compose run --rm frontend php yii migrate
  • docker-compose run --rm backend php yii migrate

Now go to Frontend and click "signup" in the right upper corner

Second way is to directly modify table in DB:

  • Download adminer - It is a single-file DB client: www.adminer.org/en
  • Copy Adminer to frontend\web\adminer.php
  • Open Adminer using: http://localhost:20080/adminer.php
  • If your DB has no password, adminer fill refuse to work. You would have to "crack" it.
  • Use following login and go to DB yii2advanced:
  • server=mysql !!
  • username=yii2advanced
  • password=secret
  • Values are taken from docker-compose.yml
  • Set status=10 to your first user

Now you have your account and you can log in to Backend

Enabling xDebug in Docker, yii demo application

Just add section environment to docker-compose.yml like this:

services:

  frontend:
    build: frontend
    ports:
      - 20080:80
    volumes:
      # Re-use local composer cache via host-volume
      - ~/.composer-docker/cache:/root/.composer/cache:delegated
      # Mount source-code for development
      - ./:/app
    environment:
      PHP_ENABLE_XDEBUG: 1
      XDEBUG_CONFIG: "client_port=9000 start_with_request=yes idekey=netbeans-xdebug log_level=1 log=/app/xdebug.log discover_client_host=1"
      XDEBUG_MODE: "develop,debug"

This will allow you to see nicely formatted var_dump values and to debug your application in your IDE.

Note: You can/must specify the idekey and client_port based on your IDE settings. Plus your Yii project must be well configured in the IDE as well. In NetBeans make sure that "Project URL" and "index file" are correct in "Properties/Run Configuration" (right click the project)

Note 2: Please keep in mind that xDebug2 and xDebug3 have different settings. Details here.

I spent on this approximately 8 hours. Hopefully someone will enjoy it :-) Sadly, this configuration is not present in docker-compose.yml. It would be soooo handy.

Docker - Custom php.ini

Add into section "volumes" this line:

- ./myphp.ini:/usr/local/etc/php/conf.d/custom.ini

And create file myphp.ini the root of your Yii application. You can enter for example html_errors=on and html_errors=off to test if the file is loaded. Restart docker and check results using method phpinfo() in a PHP file.

How to enter Docker's bash (cli, command line)

Navigate in command line to the folder of your docker-project and run command:

  • docker ps
  • This will list all services you defined in docker-compose.yml

The last column of the list is NAMES. Pick one and copy its name. Then run command:

  • docker exec -it {NAME} /bin/bash
  • ... where {NAME} is your service name. For example:
  • docker exec -it yii-advanced_backend_1 /bin/bash

To findout what Linux is used, you can call cat /etc/os-release. (or check the Vagrant chapter for other commands)

If you want to locate the php.ini, type php --ini. Once you find it you can copy it to your yii-folder like this:

cp path/to/php.ini /app/myphp.ini

AdminLTE - overview & general research on the theme

AdminLTE is one of available admin themes. It currently has 2 versions:

  • AdminLTE v2 = based on Bootstrap 3 = great for Yii v2 application
  • AdminLTE v3 = based on Bootstrap 4 (it is easy to upgrade Yii2 from Bootstrap3 to Bootstrap4 *)

* Upgrading Yii2 from Bootstrap3 to Bootstrap4: https://www.youtube.com/watch?v=W1xxvngjep8

Documentation for AdminLTE <= 2.3, v2.4, v3.0 Note that some AdminLTE functionalities are only 3rd party dependencies. For example the map.

There are also many other admin themes:

There are also more Yii2 extensions for integration of AdminLTE into Yii project:

I picked AdminLTE v2 (because it uses the same Bootstrap as Yii2 demos) and I tested some extensions which should help with implementation.

But lets start with quick info about how to use AdminLTE v2 without extensions in Yii2 demo application.

Manual integration of v2.4 - Asset File creation

  • Open documentation and run composer or download all dependencies in ZIP.
  • Open preview page and copy whole HTML code to your text editor.
  • Delete those parts of BODY section which you do not need (at least the content of: section class="content")
  • Also delete all SCRIPT and LINK tags. We will add them using the AssetBundle later.

  • Open existing file views/layouts/main.php and copy important PHP calls to the new file. (Asset, beginPage, $content, Breadcrumbs etc)
  • Now your layout is complete, you can replace the original layout file.

We only need to create the Asset file to link all SCRIPTs and LINKs:

  • Copy file assets/AppAsset into assets/LteAsset and rename the class inside.
  • Copy all LINK- and SCRIPT- URLs to LteAsset.
  • Skip jQuery and Bootstrap, they are part of Yii. Example:
namespace app\assets;
use yii\web\AssetBundle;
class LteAsset extends AssetBundle
{
    public $sourcePath = '@vendor/almasaeed2010/adminlte/';
    public $jsOptions = ['position' => \yii\web\View::POS_HEAD];  // POS_END cause conflict with YiiAsset  
    public $css = [
        'bower_components/font-awesome/css/font-awesome.min.css',
        'https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic',
        // etc
    ];
    public $js = [
        'bower_components/jquery-ui/jquery-ui.min.js',
        // etc
    ];
    public $depends = [
        'yii\web\YiiAsset',
        'yii\bootstrap\BootstrapAsset',
    ];
}
  • Refresh your Yii page and check "developer tools" for network errors. Fix them.

This error can appear: "Headers already sent"

  • It means you forgot to copy some PHP code from the old layout file to the new one.

Now you are done, you can start using HTML and JS stuff from AdminLTE. So lets check extensions which will do it for us

Insolita extension

Works good for many UI items: Boxes, Tile, Callout, Alerts and Chatbox. You only have to prepare the main layout file and Asset bundle, see above. It hasn't been updated since 2018.

Check its web for my comment. I showed how to use many widgets.

Imperfections in the sources:

vendor\insolita\yii2-adminlte-widgets\LteConst.php

  • There is a typo: COLOR_LIGHT_BLUE should be 'lightblue', not 'light-blue'

vendor\insolita\yii2-adminlte-widgets\CollapseBox.php

  • Class in $collapseButtonTemplate should be "btn btn-box-tool", not "btn {btnType} btn-xs"
  • (it affects the expand/collapse button in expandable boxes)
  • $collapseButtonTemplate must be modified in order to enable removing Boxes from the screen. Namely data-widget and iconClass must be changed in method prepareBoxTools()

LteBox

  • Boxes can be hidden behind the "waiting icon" overlay. This is done using following HTML at the end of the box's div:
    <div class="overlay"><i class="fa fa-refresh fa-spin"></i></div>
    
  • This must be added manually or by modifying LteBox

Yiister

Its web explains everything. Very usefull: http://adminlte.yiister.ru You only need the Asset File from this article and then install Yiister. Sadly it hasn't been updated since 2015. Provides widgets for rendering Menu, GridView, Few boxes, Fleshalerts and Callouts. Plus Error page.

dmstr/yii2-adminlte-asset

Officially mentioned on AdminLTE web. Renders only Menu and Alert. Provides mainly the Asset file and Gii templates. Gii templates automatically fix the GridView design, but you can find below how to do it manually.

Other enhancements

AdminLTE is using font Source Sans Pro. If you want a different one, pick it on Google Fonts and modify the layout file like this:

<link href="https://fonts.googleapis.com/css2?family=Palanquin+Dark:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
 body {
    font-family: 'Palanquin Dark', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  } 
  
  h1,h2,h3,h4,h5,h6,
  .h1,.h2,.h3,.h4,.h5,.h6 {
    font-family: 'Palanquin Dark', sans-serif;
  }
</style>

To display GridView as it should be, wrap it in this HTML code:

<div class="box box-primary">
  <div class="box-header">
    <h3 class="box-title"><i class="fa fa-table"></i>&nbsp;Grid caption</h3>
  </div>
  <div class="box-body"

  ... grid view ...

  </div>
</div>

You can also change the glyphicon in web/css/site.css:

a.asc:after {
    content: "\e155";
}

a.desc:after {
    content: "\e156";
}

And this is basically it. Now we know how to use AdminLTE and fix the GridView. At least one extension will be needed to render widgets, see above.

Creating custom Widget

See official reading about Widgets or this explanation. I am presenting this example, but I added 3 rows. Both types of Widgets can be coded like this:

namespace app\components;
use yii\base\Widget;
use yii\helpers\Html;

class HelloWidget extends Widget{
 public $message;
 public function init(){
  parent::init();
  if($this->message===null){
   $this->message= 'Welcome User';
  }else{
   $this->message= 'Welcome '.$this->message;
  }
  // ob_start();
  // ob_implicit_flush(false);
 }
 public function run(){
  // $content = ob_get_clean();
  return Html::encode($this->message); // . $content;
 }
}

// This widget is called like this:
echo HelloWidget::widget(['message' => ' Yii2.0']);

// After uncommenting my 4 comments you can use this
HelloWidget::begin(['message' => ' Yii2.0']);
echo 'My content';
HelloWidget::end();

Tests - unit + functional + acceptance (opa) + coverage

It is easy to run tests as both demo-applications are ready. Use command line and navigate to your project. Then type:

php ./vendor/bin/codecept run

This will run Unit and Functional tests. They are defined in folder tests/unit and tests/functional. Functional tests run in a hidden browser and do not work with JavaScript I think. In order to test complex JavaScript, you need Acceptance Tests. How to run them is to be found in file README.md or in documentation in both demo applications. If you want to run these tests in your standard Chrome or Firefox browser, you will need Java JDK and file selenium-server*.jar. See links in README.md. Once you have the JAR file, place is to your project and run it:

java -jar selenium-server-4.0.0.jar standalone

Now you can rerun your tests. Make sure that you have working URL of your project in file acceptance.suite.yml, section WebDriver. For example http://localhost/yii-basic/web. It depends on your environment. Also specify browser. For me works well setting "browser: chrome". If you receive error "WebDriver is not installed", you need to call this composer command:

composer require codeception/module-webdriver --dev

PS: There is also this file ChromeDriver but I am not really sure if it is an alternative to "codeception/module-webdriver" or when to use it. I havent studied it yet.

If you want to see the code coverage, do what is described in the documentation (link above). Plus make sure that your PHP contains xDebug! And mind the difference in settings of xDebug2 and xDebug3! If xDebug is missing, you will receive error "No code coverage driver available".

Microsoft Access MDB

Under Linux I haven't suceeded, but when I install a web server on Windows (for example XAMPP Server) I am able to install "Microsoft Access Database Engine 2016 Redistributable" and use *.mdb file.

So first of all you should install the web server with PHP and you should know wheather you are installing 64 or 32bit versions. Probably 64. Then go to page Microsoft Access Database Engine 2016 Redistributable (or find newer if available) and install corresponding package (32 vs 64bit).

Note: If you already have MS Access installed in the identical bit-version, you might not need to install the engine.

Then you will be able to use following DSN string in DB connection. (The code belongs to file config/db.php):

<?php

$file = "C:\\xampp\\htdocs\\Database1.mdb";

return [
  'class' => 'yii\db\Connection',
	
  'dsn' => "odbc:DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=$file;Uid=;Pwd=;",
  'username' => '',
  'password' => '',
  'charset' => 'utf8',
	
  //'schemaMap' => [
  //  'odbc'=> [
  //    'class'=>'yii\db\pgsql\Schema',
  //    'defaultSchema' => 'public' //specify your schema here
  //  ]
  //], 

  // Schema cache options (for production environment)
  //'enableSchemaCache' => true,
  //'schemaCacheDuration' => 60,
  //'schemaCache' => 'cache',
];

Then use this to query a table:

$data = Yii::$app->db->createCommand("SELECT * FROM TableX")->queryAll();
var_dump($data);

Note: If you already have MS Access installed in different bit-version then your PHP, you will not be able to install the engine in the correct bit-version. You must uninstall MS Access in that case.

Note2: If you do not know what your MDB file contains, Google Docs recommended me MDB, ACCDB Viewer and Reader and it worked.

Note3: There are preinstalled applications in Windows 10 named:

  • "ODBC Data Sources 32-bit"
  • "ODBC Data Sources 64-bit"
  • (Just hit the Win-key and type "ODBC")

Open the one you need, go to tab "System DSN" and click "Add". You will see what drivers are available - only these drivers can be used in the DSN String!!

If only "SQL Server" is present, then you need to install the Access Engine (or MS Access) with drivers for your platform. You need driver named cca "Microsoft Access Driver (*.mdb, *.accdb)"

In my case the Engine added following 64bit drivers:

  • Microsoft Access dBASE Driver (*.dbf, *.ndx, *.mdx)
  • Microsoft Access Driver (*.mdb, *.accdb)
  • Microsoft Access Text Driver (*.txt, *.csv)
  • Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)

And how about Linux ?

You need the MS Access Drivers as well, but Microsoft does not provide them. There are some 3rd party MdbTools or EasySoft, but their are either not-perfect or expensive. Plus there is Unix ODBC.

For Java there are Java JDBC, Jackcess and Ucanaccess.

And how about Docker ? As far as I know you cannot run Windows images under Linux so you will not be able to use the ODBC-advantage of Windows in this case. You can use Linux images under Windows, but I think there is no way how to access the ODBC drivers from virtual Linux. You would have to try it, I haven't tested it yet.

Migration batch insert csv

If you want to import CSV into your DB in Yii2 migrations, you can create this "migration base class" and use it as a parent of your actual migration. Then you can use method batchInsertCsv().

<?php

namespace app\components;

use yii\db\Migration;

class BaseMigration extends Migration
{
    /**
     * @param $filename Example: DIR_ROOT . DIRECTORY_SEPARATOR . "file.csv"
     * @param $table The target table name
     * @param $csvToSqlColMapping [csvColName => sqlColName] (if $containsHeaderRow = true) or [csvColIndex => sqlColName] (if $containsHeaderRow = false)
     * @param bool $containsHeaderRow If the header with CSV col names is present
     * @param int $batchSize How many rows will be inserted in each batch
     * @throws Exception
     */
    public function batchInsertCsv($filename, $table, $csvToSqlColMapping, $containsHeaderRow = false, $batchSize = 10000, $separator = ';')
    {
        if (!file_exists($filename)) {
            throw new \Exception("File " . $filename . " not found");
        }

        // If you see number 1 in first inserted row and column, most likely BOM causes this.
        // Some Textfiles begin with 239 187 191 (EF BB BF in hex)
        // bite order mark https://en.wikipedia.org/wiki/Byte_order_mark
        // Let's trim it on the first row.
        $bom = pack('H*', 'EFBBBF');

        $handle = fopen($filename, "r");
        $lineNumber = 1;
        $header = [];
        $rows = [];
        $sqlColNames = array_values($csvToSqlColMapping);
        $batch = 0;

        if ($containsHeaderRow) {
            if (($raw_string = fgets($handle)) !== false) {
                $header = str_getcsv(trim($raw_string, $bom), $separator);
            }
        }

        // Iterate over every line of the file
        while (($raw_string = fgets($handle)) !== false) {
            $dataArray = str_getcsv(trim($raw_string, $bom), $separator);

            if ($containsHeaderRow) {
                $dataArray = array_combine($header, $dataArray);
            }

            $tmp = [];
            foreach ($csvToSqlColMapping as $csvCol => $sqlCol) {
                $tmp[] = trim($dataArray[$csvCol]);
            }
            $rows[] = $tmp;

            $lineNumber++;
            $batch++;

            if ($batch >= $batchSize) {
                $this->batchInsert($table, $sqlColNames, $rows);
                $rows = [];
                $batch = 0;
            }
        }
        fclose($handle);

        $this->batchInsert($table, $sqlColNames, $rows);
    }
}
]]>
0
[wiki] How to redirect all emails to one inbox on Yii2 applications Sun, 03 Dec 2023 17:03:34 +0000 https://www.yiiframework.com/wiki/2566/how-to-redirect-all-emails-to-one-inbox-on-yii2-applications https://www.yiiframework.com/wiki/2566/how-to-redirect-all-emails-to-one-inbox-on-yii2-applications glpzzz glpzzz

\yii\mail\BaseMailer::useFileTransport is a great tool. If you activate it, all emails sent trough this mailer will be saved (by default) on @runtime/mail instead of being sent, allowing the devs to inspect thre result.

But what happens if we want to actually receive the emails on our inboxes. When all emails are suppose to go to one account, there is no problem: setup it as a param and the modify it in the params-local.php (assuming advaced application template).

The big issue arises when the app is supposed to send emails to different accounts and make use of replyTo, cc and bcc fields. It's almost impossible try to solve it with previous approach and without using a lot of if(YII_DEBUG).

Well, next there is a solution:

'useFileTransport' => true,
'fileTransportCallback' => function (\yii\mail\MailerInterface $mailer, \yii\mail\MessageInterface $message) {
    $message->attachContent(json_encode([
            'to' => $message->getTo(),
            'cc' => $message->getCc(),
            'bcc' => $message->getBcc(),
            'replyTo' => $message->getReplyTo(),
        ]), ['fileName' => 'metadata.json', 'contentType' => 'application/json'])
        ->setTo('debug@mydomain.com') // account to receive all the emails
        ->setCc(null)
        ->setBcc(null)
        ->setReplyTo(null);

    $mailer->useFileTransport = false;
    $mailer->send($message);
    $mailer->useFileTransport = true;

    return $mailer->generateMessageFileName();
}

How it works? fileTransportCallback is the callback to specify the filename that should be used to create the saved email on @runtime/mail. It "intercepts" the send email process, so we can use it for our porpuses.

  1. Attach a json file with the real recipients information so we can review it
  2. Set the recipient (TO) as the email address where we want to receive all the emails.
  3. Set the others recipients fields as null
  4. Deactivate useFileTransport
  5. Send the email
  6. Activate useFileTransport
  7. Return the defaut file name (datetime of the operation)

This way we both receive all the emails on the specified account and get them stored on @runtime/mail.

Pretty simple helper to review emails on Yii2 applications.

Originally posted on: https://glpzzz.github.io/2020/10/02/yii2-redirect-all-emails.html

]]>
0
[wiki] Api of Multiple File Uploading in Yii2 Tue, 05 Jul 2022 03:01:39 +0000 https://www.yiiframework.com/wiki/2565/api-of-multiple-file-uploading-in-yii2 https://www.yiiframework.com/wiki/2565/api-of-multiple-file-uploading-in-yii2 fezzymalek fezzymalek

After getting lot's of error and don't know how to perform multiple images api in yii2 finally I get it today

This is my question I asked on forum and it works for me https://forum.yiiframework.com/t/multiple-file-uploading-api-in-yii2/130519

Implement this code in model for Multiple File Uploading

public function rules()
    {
        return [
            [['post_id', 'media'], 'required'],
            [['post_id'], 'integer'],
            [['media'], 'file', 'maxFiles' => 10],//here is my file field
            [['created_at'], 'string', 'max' => 25],
            [['post_id'], 'exist', 'skipOnError' => true, 'targetClass' => Post::className(), 'targetAttribute' => ['post_id' => 'id']],
        ];
    }
    

You can add extension or any skiponempty method also in model.

And this is my controller action where I performed multiple file uploading code.

public function actionMultiple(){
        $model = new Media;
        $model->post_id = '2';
        if (Yii::$app->request->ispost) {
            $model->media = UploadedFile::getInstances($model, 'media');
            if ($model->media) {
                foreach ($model->media as $value) {
                    $model = new Media;
                    $model->post_id = '2';
                    $BasePath = Yii::$app->basePath.'/../images/post_images';
                    $filename = time().'-'.$value->baseName.'.'.$value->extension;
                    $model->media = $filename;
                    if ($model->save()) {
                        $value->saveAs($BasePath.$filename);
                    }
                }
                return array('status' => true, 'message' => 'Image Saved'); 
            }
        }
        return array('status' => true, 'data' => $model);
    }

If any query or question I will respond.

]]>
0
[wiki] How to email error logs to developer on Yii2 apps Sun, 03 Dec 2023 17:03:34 +0000 https://www.yiiframework.com/wiki/2564/how-to-email-error-logs-to-developer-on-yii2-apps https://www.yiiframework.com/wiki/2564/how-to-email-error-logs-to-developer-on-yii2-apps glpzzz glpzzz

Logging is a very important feature of the application. It let's you know what is happening in every moment. By default, Yii2 basic and advanced application have just a \yii\log\FileTarget target configured.

To receive emails with messages from the app, setup the log component to email (or Telegram, or slack) transport instead (or besides) of file transport:

'components' => [
    // ...
    'log' => [
         'targets' => [
             [
                 'class' => 'yii\log\EmailTarget',
                 'mailer' => 'mailer',
                 'levels' => ['error', 'warning'],
                 'message' => [
                     'from' => ['log@example.com'],
                     'to' => ['developer1@example.com', 'developer2@example.com'],
                     'subject' => 'Log message',
                 ],
             ],
         ],
    ],
    // ...
],

The \yii\log\EmailTarget component is another way to log messages, in this case emailing them via the mailer component of the application as specified on the mailer attribute of EmailTarget configuration. Note that you can also specify messages properties and which levels of messages should be the sent trough this target.

If you want to receive messages via other platforms besides email, there are other components that represents log targets:

Or you can implement your own by subclassing \yii\log\Target

]]>
0
[wiki] How to add Schema.org markup to Yii2 pages Fri, 11 Sep 2020 22:09:55 +0000 https://www.yiiframework.com/wiki/2560/how-to-add-schema-org-markup-to-yii2-pages https://www.yiiframework.com/wiki/2560/how-to-add-schema-org-markup-to-yii2-pages glpzzz glpzzz

https://schema.org is a markup system that allows to embed structured data on their web pages for use by search engines and other applications. Let's see how to add Schema.org to our pages on Yii2 based websites using JSON-LD.

Basically what we need is to embed something like this in our pages:

<script type="application/ld+json">
{ 
  "@context": "http://schema.org/",
  "@type": "Movie",
  "name": "Avatar",
  "director": 
    { 
       "@type": "Person",
       "name": "James Cameron",
       "birthDate": "1954-08-16"
    },
  "genre": "Science fiction",
  "trailer": "../movies/avatar-theatrical-trailer.html" 
}
</script>

But we don't like to write scripts like this on Yii2, so let's try to do it in other, more PHP, way.

In the layout we can define some general markup for our website, so we add the following snippet at the beginning of the@app/views/layouts/main.php file:

<?= \yii\helpers\Html::script(isset($this->params['schema'])
    ? $this->params['schema']
    : \yii\helpers\Json::encode([
        '@context' => 'https://schema.org',
        '@type' => 'WebSite',
        'name' => Yii::$app->name,
        'image' => $this->image,
        'url' => Yi::$app->homeUrl,
        'descriptions' => $this->description,
        'author' => [
            '@type' => 'Organization',
            'name' => Yii::$app->name,
            'url' => 'https://www.hogarencuba.com',
            'telephone' => '+5352381595',
        ]
    ]), [
    'type' => 'application/ld+json',
]) ?>

Here we are using the Html::script($content, $options) to include the script with the necessary type option, and Json::encode($value, $options) to generate the JSON. Also we use a page parameter named schema to allow overrides on the markup from other pages. For example, in @app/views/real-estate/view.php we are using:

$this->params['schema'] = \yii\helpers\Json::encode([
    '@context' => 'https://schema.org',
    '@type' => 'Product',
    'name' => $model->title,
    'description' => $model->description,
    'image' => array_map(function ($item) {
        return $item->url;
    }, $model->images),
    'category' => $model->type->description_es,
    'productID' => $model->code,
    'identifier' => $model->code,
    'sku' => $model->code,
    'url' => \yii\helpers\Url::current(),
    'brand' => [
        '@type' => 'Organization',
        'name' => Yii::$app->name,
        'url' => 'https://www.hogarencuba.com',
        'telephone' => '+5352381595',
    ],
    'offers' => [
        '@type' => 'Offer',
        'availability' => 'InStock',
        'url' => \yii\helpers\Url::current(),
        'priceCurrency' => 'CUC',
        'price' => $model->price,
        'priceValidUntil' => date('Y-m-d', strtotime(date("Y-m-d", time()) . " + 365 day")),
        'itemCondition' => 'https://schema.org/UsedCondition',
        'sku' => $model->code,
        'identifier' => $model->code,
        'image' => $model->images[0],
        'category' => $model->type->description_es,
        'offeredBy' => [
            '@type' => 'Organization',
            'name' => Yii::$app->name,
            'url' => 'https://www.hogarencuba.com',
            'telephone' => '+5352381595',
        ]
    ]
]);

Here we redefine the schema for this page with more complex markup: a product with an offer.

This way all the pages on our website will have a schema.org markup defined: in the layout we have a default and in other pages we can redefine setting the value on $this->params['schema'].

]]>
0
[wiki] How to add Open Graph and Twitter Card tags to Yii2 website. Sun, 03 Dec 2023 17:03:34 +0000 https://www.yiiframework.com/wiki/2559/how-to-add-open-graph-and-twitter-card-tags-to-yii2-website https://www.yiiframework.com/wiki/2559/how-to-add-open-graph-and-twitter-card-tags-to-yii2-website glpzzz glpzzz

OpenGraph and Twitter Cards are two metadata sets that allow to describe web pages and make it more understandable for Facebook and Twitter respectively.

There a lot of meta tags to add to a simple webpage, so let's use TaggedView

This component overrides the yii\web\View adding more attributes to it, allowing to set the values on every view. Usually we setup page title with

$this->title = $model->title;

Now, with TaggedView we are able to set:

$this->title = $model->title;
$this->description = $model->abstract;
$this->image = $model->image;
$this->keywords = ['foo', 'bar'];

And this will generate the proper OpenGraph, Twitter Card and HTML meta description tags for this page.

Also, we can define default values for every tag in the component configuration that will be available for every page and just will be overriden if redefined as in previous example.

'components' => [
    //...
    'view' => [
        'class' => 'daxslab\taggedview\View',
        'site_name' => '',
        'author' => '',
        'locale' => '',
        'generator' => '',
        'updated_time' => '',
    ],
    //...
]

Some of this properties have default values assigned, like site_name that gets Yii::$app->name by default.

Result of usage on a website:

<title>¿Deseas comprar o vender una casa en Cuba? | HogarEnCuba, para comprar y vender casas en Cuba</title>
<meta name="author" content="Daxslab (https://www.daxslab.com)">
<meta name="description" content="Hay 580 casas...">
<meta name="generator" content="Yii2 PHP Framework (http://www.yiiframework.com)">
<meta name="keywords" content="HogarEnCuba, ...">
<meta name="robots" content="follow">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:description" content="Hay 580 casas...">
<meta name="twitter:image" content="https://www.hogarencuba.com/images/main-identifier_es.png">
<meta name="twitter:site" content="HogarEnCuba">
<meta name="twitter:title" content="¿Deseas comprar o vender una casa en Cuba?">
<meta name="twitter:type" content="website">
<meta name="twitter:url" content="https://www.hogarencuba.com/">
<meta property="og:description" content="Hay 580 casas...">
<meta property="og:image" content="https://www.hogarencuba.com/images/main-identifier_es.png">
<meta property="og:locale" content="es">
<meta property="og:site_name" content="HogarEnCuba">
<meta property="og:title" content="¿Deseas comprar o vender una casa en Cuba?">
<meta property="og:type" content="website">
<meta property="og:updated_time" content="10 sept. 2020 9:43:00">
]]>
0
[wiki] Yii v2 snippet guide II Thu, 11 Nov 2021 13:47:12 +0000 https://www.yiiframework.com/wiki/2558/yii-v2-snippet-guide-ii https://www.yiiframework.com/wiki/2558/yii-v2-snippet-guide-ii rackycz rackycz
  1. My articles
  2. Connection to MSSQL
  3. Using MSSQL database as the 2nd DB in the Yii2 project
  4. Creating models in Gii for remote MSSQL tables
  5. PhpExcel/PhpSpreadsheet in Yii 2 and sending binary content to the browser
  6. PDF - UTF + 1D & 2D Barcodes - TCPDF
  7. Custom formatter - asDecimalOrInteger
  8. Displaying SUM of child models in a GridView with parent models
  9. Sort and search by related column
  10. Sending binary data as a file to browser - decoded base64

My articles

Articles are separated into more files as there is the max lenght for each file on wiki.

Connection to MSSQL

You will need MSSQL drivers in PHP. Programatically you can list them or test their presence like this:

var_dump(\PDO::getAvailableDrivers());

if (in_array('sqlsrv', \PDO::getAvailableDrivers())) {
  // ... MsSQL driver is available, do something
}

Based on your system you have to download different driver. The differences are x64 vs x86 and ThreadSafe vs nonThreadSafe. In Windows I always use ThreadSafe. Explanation.

Newest PHP drivers are here.

  • Drivers v5.8 = PHP 7.2 - 7.4

Older PHP drivers here.

  • Drivers v4.0 = PHP 7.0 - 7.1
  • Drivers v3.2 = PHP 5.x

Once drivers are downloaded and extracted, pick one DLL file and place it into folder "php/ext". On Windows it might be for example here: "C:\xampp\php\ext"

Note: In some situations you could also need these OBDC drivers, but I am not sure when:

Now file php.ini must be modified. On Windows it might be placed here: "C:\xampp\php\php.ini". Open it and search for rows starting with word "extension" and paste there cca this:

extension={filename.dll}
// Example:
extension=php_pdo_sqlsrv_74_ts_x64.dll

Now restart Apache and visit phpinfo() web page. You should see section "pdo_sqlsrv". If you are using XAMPP, it might be on this URL: http://localhost/dashboard/phpinfo.php.

Then just add connection to your MSSQL DB in Yii2 config. In my case the database was remote so I needed to create 2nd DB connection. Read next chapter how to do it.

Using MSSQL database as the 2nd DB in the Yii2 project

Adding 2nd database is done like this in yii-config:

'db' => $db, // the original DB
'db2'=>[
  'class' => 'yii\db\Connection',
  'driverName' => 'sqlsrv',
  // I was not able to specify database like this: 
  // 'dsn' => 'sqlsrv:Server={serverName};Database={dbName}',
  'dsn' => 'sqlsrv:Server={serverName}', 
  'username' => '{username}',
  'password' => '{pwd}',
  'charset' => 'utf8',
],

That's it. Now you can test your DB like this:

$result = Yii::$app->db2->createCommand('SELECT * FROM {tblname}')->queryAll();
var_dump($result);

Note that in MSSQL you can have longer table names. Example: CATEGORY.SCHEMA.TBL_NAME

And your first test-model can look like this (file MyMsModel.php):

namespace app\models;
use Yii;
use yii\helpers\ArrayHelper;
class MyMsModel extends \yii\db\ActiveRecord
{
  public static function getDb()
  {
    return \Yii::$app->db2; // or Yii::$app->get('db2');
  }
  public static function tableName()
  {
    return 'CATEGORY.SCHEMA.TBL_NAME'; // or SCHEMA.TBL_NAME
  }
}

Usage:

$result = MyMsModel::find()->limit(2)->all();
var_dump($result);

Creating models in Gii for remote MSSQL tables

Once you have added the 2nd database (read above) go to the Model Generator in Gii. Change there the DB connection to whatever you named the connection in yii-config (in the example above it was "db2") and set tablename in format: SCHEMA.TBL_NAME. If MSSQL server has more databases, one of them is set to be the main DB. This will be used I think. I haven't succeeded to change the DB. DB can be set in the DSN string, but it had no effect in my case.

PhpExcel/PhpSpreadsheet in Yii 2 and sending binary content to the browser

In previous chapters I showed how to use PhpExcel in Yii 1. Now I needed it also in Yii 2 and it was extremely easy.

Note: PhpExcel is deprecated and was replaced with PhpSpreadsheet.

// 1) Command line:
// This downloads everything to folder "vendor"
composer require phpoffice/phpspreadsheet --prefer-source
// --prefer-source ... also documentation and samples are downloaded 
// ... adds cca 40MB and 1400 files 
// ... only for devel system

// 2) PHP:
// Now you can directly use the package without any configuration:
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;

$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();

// Uncomment following rows if you want to set col width:
//$sheet->getColumnDimension('A')->setAutoSize(false);
//$sheet->getColumnDimension('A')->setWidth("50");

$sheet->setCellValue('A1', 'Hello World !');

$writer = new Xlsx($spreadsheet);

// You can save the file on the server:
// $writer->save('hello_world.xlsx'); 

// Or you can send the file directly to the browser so user can download it:
// header('Content-Type: application/vnd.ms-excel'); // This is probably for older XLS files.
header('Content-Type: application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); // This is for XLSX files (they are basically zip files).
header('Content-Disposition: attachment;filename="filename.xlsx"');
header('Cache-Control: max-age=0');
$writer->save('php://output');
exit();

Thanks to DbCreator for the idea how to send XLSX to browser. Nevertheless exit() or die() should not be called. Read the link.

Following is my idea which originates from method renderPhpFile() from Yii2:

ob_start();
ob_implicit_flush(false);
$writer->save('php://output');
$file = ob_get_clean();

return \Yii::$app->response->sendContentAsFile($file, 'file.xlsx',[
  'mimeType' => 'application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  'inline' => false
]);

This also worked for me:

$tmpFileName = uniqid('file_').'.xlsx';
$writer->save($tmpFileName);    
header('Content-Type: application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); 
header('Content-Disposition: attachment;filename="filename.xlsx"');
header('Cache-Control: max-age=0');
echo file_get_contents($tmpFileName);
unlink($tmpFileName);
exit();

Note: But exit() or die() should not be called. Read the "DbCreator" link above.

PDF - UTF + 1D & 2D Barcodes - TCPDF

See part I of this guide for other PDF creators:

TCPDF was created in 2002 (I think) and these days (year 2020) is being rewritten into a modern PHP application. I will describe both, but lets begin with the older version.

Older version of TCPDF

Download it from GitHub and save it into folder

{projectPath}/_tcpdf

Into web/index.php add this:

require_once('../_tcpdf/tcpdf.php');

Now you can use any Example to test TCPDF. For example: https://tcpdf.org/examples/example_001/

Note: You have to call constructor with backslash:

$pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);

Note: Texts are printed using more methods - see file tcpdf.php for details:

  • writeHTMLCell()
  • Multicell()
  • writeHTML()
  • Write()
  • Cell()
  • Text()

Note: Store your files in UTF8 no BOM format so diacritics is correct in PDF.

Importing new TTF fonts is done like this:

// this command creates filed in folder _tcpdf\fonts. Use the filename as the fontname in other commands.
$fontname = \TCPDF_FONTS::addTTFfont("path to TTF file", 'TrueTypeUnicode', '', 96);

Now you can use it in PHP like this:

$pdf->SetFont($fontname, '', 24, '', true);

Or in HTML:

<font size="9" face="fontName" style="color: rgb(128, 128, 128);">ABC</font>

Rendering is done like this:

$pdf->writeHTML($html);

Note: When printing pageNr and totalPageCount to the footer, writeHTML() was not able to correctly interpret methods getAliasNumPage() and getAliasNbPages() as shown in Example 3. I had to use rendering method Text() and position the numbers correctly like this:

$this->writeHTML($footerHtmlTable);
$this->SetTextColor('128'); // I have gray pageNr
$this->Text(185, 279, 'Page ' . $this->getAliasNumPage() . '/' . $this->getAliasNbPages());
$this->SetTextColor('0'); // returning black color

New version of TCPDF

... to be finished ...

Custom formatter - asDecimalOrInteger

If I generate a PDF-invoice it contains many numbers and it is nice to print them as integers when decimals are not needed. For example number 24 looks better and saves space compared to 24.00. So I created such a formatter. Original inspiration and how-to was found here:

My formatter looks like this:

<?php

namespace app\myHelpers;

class MyFormatter extends \yii\i18n\Formatter {

  public function asDecimalOrInteger($value) {
    $intStr = (string) (int) $value; // 24.56 => "24" or 24 => "24"
    if ($intStr === (string) $value) {
      // If input was integer, we are comparing strings "24" and "24"
      return $this->asInteger($value);
    }
    if (( $intStr . '.00' === (string) $value)) {
      // If the input was decimal, but decimals were all zeros, it is an integer.
      return $this->asInteger($value);
    }
    // All other situations
    $decimal = $this->asDecimal($value);
    
    // Here I trim also the trailing zero.
    // Disadvantage is that String is returned, but in PDF it is not important
    return rtrim((string)$decimal, "0"); 
  }

}

Usage is simple. Read the link above and give like to karpy47 or see below:

// file config/web.php
'components' => [
    'formatter' => [
        'class' => 'app\myHelpers\MyFormatter',
   ],
],

There is only one formatter in the whole of Yii and you can extend it = you can add more methods and the rest of the formatter will remain so you can use all other methods as mentioned in documentation.

Displaying SUM of child models in a GridView with parent models

... can be easily done by adding a MySQL VIEW into your DB, creating a model for it and using it in the "ParentSearch" model as the base class.

Let's show it on list of invoices and their items. Invoices are in table "invoice" (model Invoice) and their items in "invoice_item" (model InvoiceItem). Now we need to join them and sort and filter them by SUM of prices (amounts). To avoid calculations in PHP, DB can do it for us if we prepare a MySQL VIEW:

CREATE VIEW v_invoice AS
SELECT invoice.*, 
SUM(invoice_item.units * invoice_item.price_per_unit) as amount,
COUNT(invoice_item.id) as items
FROM invoice 
LEFT JOIN invoice_item 
ON (invoice.id = invoice_item.id_invoice)
GROUP BY invoice.id

Note: Here you can read why it is better not to use COUNT(*) in LEFT JOIN:

This will technically clone the original table "invoice" into "v_invoice" and will append 2 calculated columns: "amount" + "items". Now you can easily use this VIEW as a TABLE (for reading only) and display it in a GridView. If you already have a GridView for table "invoice" the change is just tiny. Create this model:

<?php
namespace app\models;
class v_Invoice extends Invoice
{
    public static function primaryKey()
    {
        // here is specified which column(s) create the fictive primary key in the mysql-view
        return ['id']; 
    }
    public static function tableName()
    {
        return 'v_invoice';
    }
}

.. and in model InvoiceSearch replace word Invoice with v_Invoice (on 2 places I guess) plus add rules for those new columns. Example:

public function rules()
{
  return [
    // ...
    [['amount'], 'number'], // decimal
    [['items'], 'integer'],
  ];
}

Into method search() add condition if you want to filter by amount or items:

if (trim($this->amount)!=='') {
  $query->andFilterWhere([
    'amount' => $this->amount
  ]);
}

In the GridView you can now use the columns "amount" and "items" as native columns. Filtering and sorting will work.

Danger: Read below how to search and sort by related columns. This might stop working if you want to join your MySQL with another table.

I believe this approach is the simplest to reach the goal. The advantage is that the MySQL VIEW is only used when search() method is called - it means in the list of invoices. Other parts of the web are not influenced because they use the original Invoice model. But if you need some special method from the Invoice model, you have it also in v_Invoice. If data is saved or changed, you must always modify the original table "invoice".

Sort and search by related column

Lets say you have table of invoices and table of companies. They have relation and you want to display list of Invoices plus on each row the corresponding company name. You want to filter and sort by this column.

Your GridView:

<?= GridView::widget([
// ...
  'columns' => [
    // ...
    [
      'attribute'=>'company_name',
      'value'=>'companyRelation.name',
    ],

Your InvoiceSearch model:

class InvoiceSearch extends Invoice
{
  public $company_name;
  
  // ...
  
  public function rules() {
    return [
      // ...
      [['company_name'], 'safe'],
    ];
  }             

  // ...
  
  public function search($params) {
    // ...

    // You must use joinWith() in order to have both tables in one JOIN - then you can call WHERE and ORDER BY on the 2nd table. 
    // Explanation here:
    // https://stackoverflow.com/questions/25600048/what-is-the-difference-between-with-and-joinwith-in-yii2-and-when-to-use-them
    
    $query = Invoice::find()->joinWith('companyRelation');

    // Appending new sortable column:
    $sort = $dataProvider->getSort(); 
    $sort->attributes['company_name'] = [
      'asc' => ['table.column' => SORT_ASC],
      'desc' => ['table.column' => SORT_DESC],
      'label' => 'Some label',
      'default' => SORT_ASC            
    ];

    // ...
 
    if (trim($this->company_name)!=='') {
      $query->andFilterWhere(['like', 'table.column', $this->company_name]);
    }
  }

Sending binary data as a file to browser - decoded base64

In my tutorial for Yii v1 I presented following way how to send headers manually and then call exit(). But calling exit() or die() is not a good idea so I discovered a better way in Yii v2. See chapter Secured (secret) file download

Motivation: Sometimes you receive a PDF file encoded into a string using base64. For example a label with barcodes from FedEx, DPD or other delivery companies and your task is to show the label to users.

For me workes this algorithm:

$pdfBase64 = 'JVBERi0xLjQ ... Y0CiUlRU9GCg==';

// First I create a fictive stream in a temporary file
// Read more about PHP wrappers: 
// https://www.php.net/manual/en/wrappers.php.php 
$stream = fopen('php://temp','r+');

// Decoded base64 is written into the stream
fwrite($stream, base64_decode($pdfBase64));

// And the stream is rewound back to the start so others can read it
rewind($stream);

// This row sets "Content-Type" header to none. Below I set it manually do application/pdf.
Yii::$app->response->format = Yii::$app->response::FORMAT_RAW;
Yii::$app->response->headers->set('Content-Type', 'application/pdf');
      
// This row will download the file. If you do not use the line, the file will be displayed in the browser.
// Details here:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Downloads
// Yii::$app->response->headers->set('Content-Disposition','attachment; filename="hello.pdf"'); 
    
// Here is used the temporary stream
Yii::$app->response->stream = $stream;

// You can call following line, but you don't have to. Method send() is called automatically when current action ends:
// Details here:
// https://www.yiiframework.com/doc/api/2.0/yii-web-response#sendContentAsFile()-detail
// return Yii::$app->response->send(); 

Note: You can add more headers if you need. Check my previous article (linked above).

]]>
0
[wiki] Start using Yii2 in Raspberry Pi3 (RPI3) via Pantahub Tue, 22 Dec 2020 14:57:34 +0000 https://www.yiiframework.com/wiki/2557/start-using-yii2-in-raspberry-pi3-rpi3-via-pantahub https://www.yiiframework.com/wiki/2557/start-using-yii2-in-raspberry-pi3-rpi3-via-pantahub sirin_ibin sirin_ibin
  1. Make your RPI3 device ready to deploy Yii2 by following 6 Steps
  2. Deploy your Yii2 app to the device by following 5 Steps

Note:Pantahub is the only place where Linux firmware can be shared and deployed for any device, You can signup @pantahub here:http://www.pantahub.com

Make your RPI3 device ready to deploy Yii2 by following 6 Steps

Step 1: Burn the RPI3 initial stable image into your sd card.
a) Download RPI3 image

Click to download: https://pantavisor-ci.s3.amazonaws.com/pv-initial-devices/tags/012-rc2/162943661/rpi3_initial_stable.img.xz

b) unxz the device image

Run $ unxz rpi3_initial_stable.img.xz

c) Burn image into sd card using Raspberry Pi Imager 1.2

Step 2: Boot your RPI3
a) Insert your sd card and supply the power

Step 3: Singup @pantahub here http://www.pantahub.com
Step 4: Download & Install a CLI tool "pvr"

Note: pvr is a CLI tool which can be used to interact with your device through pantahub platform.

Note: Using pvr you can share your firmware and projects as simple as with a git tree.

Note: Move the pvr binary to your bin folder after download.

Linux(AMD64): Download

Linux(ARM32v6): Download

Darwin(AMD64): Download

pvr clone; pvr commit; pvr post

Install from github source code: $ go get gitlab.com/pantacor/pvr $ go build -o ~/bin/pvr gitlab.com/pantacor/pvr

Note: You need "GOLANG" to be installed in your system for building pvr from github source code.

Step 5: Detect & Claim your device
a) Connect a LAN cable between your RPI3 & computer/Router.

b) Open your terminal & run $ pvr scan

c) Claim your device

$ pvr claim -c merely-regular-gorilla https://api.pantahub.com:443/devices/5f1b9c44e193a Watch now on Amazon Prime Video 5000afa9901

d) Log into Panthub.com and check whether the newly claimed device appeared in the dashboard or not.

Step 6: Clone the device to your computer using the Clone URL of your device

$ pvr clone https://pvr.pantahub.com/sirinibin/presently_learning_pelican/0 presently_learning_pelican

Now your device is ready to deploy your Yii2 app

Deploy your Yii2 app to the device by following 5 Steps

Step 1: Move to device root dir
 `$ cd presently_learning_pelican`
Step 2: Add a new app "yii2" into the device

>sirinibin/yii2-basic-arm32v7:latest is a Docker Watch now on Amazon Prime Video image made for the devices with ARM32 architecture >> You can customise the docker image for your custom Yii2 app.

$ pvr app add yii2 --from=sirinibin/yii2-basic-arm32v7:latest

Step 3: Deploy the changes to the device

$ pvr add . $ pvr commit $ pvr post

Step 4: Check the device status changes in Pantahub.com dashboard & wait for the status to become "DONE"

Status 1:

Status 2:

Status 3:

Status 4:

Step 5: Verify the "yii2" app deployment

Access the device IP: http://10.42.0.231/myapp1/web/ in your web browser.

You are done!

]]>
0
[wiki] Yii2 - Upgrading to Bootstrap 4 Fri, 20 Mar 2020 12:18:55 +0000 https://www.yiiframework.com/wiki/2556/yii2-upgrading-to-bootstrap-4 https://www.yiiframework.com/wiki/2556/yii2-upgrading-to-bootstrap-4 RichardPillay RichardPillay

Yii2 - Converting from Bootstrap3 to Bootstrap4

This article has been written because while conversion is a largely pain-free process, there are some minor issues. These are not difficult to solve, but it is not immediately obvious where the problem lies.

1 - Install Bootstrap4 My preference is to simply use composer. Change composer.json in the root of your project:

  • find the line that includes Bootstrap3.
  • Copy the line, and change the new line:

    • change bootstrap to bootstrap4
    • Now head over to https://github.com/yiisoft/ - the Yii2 repository on Github
    • Change the version string to that version number, and also change the ~ to ^
    • After this, you should have something like this below, maybe with higher version numbers:

      "yiisoft/yii2-bootstrap" : "~2.0.6", "yiisoft/yii2-bootstrap4" : "^2.0.8",

  • Save the file, then run 'composer update'
  • Use your IDE, text editor or whatever other means you have at your disposal to find all occurrences where bootstrap is used and change it bootstrap4. My suggestion is to search for the string yii\bootstrap\ and change it to yii\bootstrap4\. However, be careful - your IDE may require the backslash to be escaped. For example, Eclipse will find all files with the string easily, but the search string must have double-backslashes, while the replacement string must be left as single ones.
  • Now run all your tests and fix the problems that have arisen. Most of the failures will come from the fact that the Navbar no longer works, most likely. It's still there, just being rendered differently, and in my case it was invisible. This is because Bootstrap4 has changed some elements of the navbar. In my case, 14 tests failed - many of which failed due to the use of navbar content in some manner, such as looking for the Login link to infer whether the user is logged in or not.
    • You're not going to fix these issues without understanding them, so take a look at https://getbootstrap.com/docs/4.0/migration/. In particular, look at what has changed regarding the Navbars. The most meaningful is that Bootstrap4 no longer specifies a background, where Bootstrap3 did.
    • Open up frontend/viewslayouts/main.php in your editor, then look at your site in the browser. In my case no navbar, except for the Brand, and that is because I changed this from what was delivered as part of the Yii2 template, and it included a background. Since the rest of it was standard, there was nothing else - no ribbon strip, and no menu entries, although mousing into the area would show the links were there and could be clicked.
      • Find the line that starts with NavBar::begin and look at it's class options. In my case they were the original: 'class' => 'navbar-inverse navbar-fixed-top'
        • As I said before, no background is included, so add one: bg-dark - and check again. Now there's the ribbon back again, but no menu items.
        • Right-click on the ribbon and select "Inspect Element", or do whatever you have to do in your browser to inspect the page source. Looking at that starts to give you clues over what the navbar is doing. Looking at that, and referring back to the Bootstrap4 migration guide, I had the impression that neither navbar-inverse nor navbar-fixed-top were doing anything. So I removed those, and when refreshing the page, confirmed there were no changes.
        • More reading on the Bootstrap website gave me the bg-dark I mentioned earlier, and for the text, navbar-light or navbar-dark produced results.
        • now I had no menu items, but I did have a button that expanded the menu. Inspecting it's properties told me it was 'navbar-toggler', and the Bootstrap website told me it new to Bootstrap4, while it and was collapsed by default, 'navbar-expand' would expand it by default. That's cool - I reckon I'm going to add a setting for logged-in users that let them choose which they prefer. In the end, I opted for navbar-expand-md, which keeps it expanded unless the screen width is tight.

At the end of all this, I had the class line changed to something which gave me a navbar very similar to the original:

        //Bootstrap3: 'class' => 'navbar-inverse navbar-fixed-top',
        //Changed for Bootstrap4: 
        'class' => 'navbar navbar-expand-md navbar-light bg-dark',


Breadcrumbs

Note - March 2020: This entire section on Breadcrumbs may no longer be an issue. While I've left the section in as a tutorial, before making any changes read what Davide has to say in the user comments.

So, that fixed my navbar. Next, I noticed that the breadcrumbs were not quite right - the slash separating the path elements was no longer there. Preparing for a lot of debugging, I went to the Bootstrap site to look for a little inspiration. I didn't need to look any further - Bootstrap 4 requires each Breadcrumb element to have a class of "breadcrumb-item". After I spent a little time looking at vendors/yiisoft/yii2/widgets/Breadcrumbs.php to get some understanding of the issue, I discovered all that's needed is to change the itemTemplate and activeItemTemplate. Of course, since these are part of the Yii2 framework, you don't want to change that file, otherwise, it will probably get updated at some stage, and all your changes would be lost. Since both of these attributes are public, you can change them from outside the class, and the easiest place to do this is in frontend/views/main.php: `html

<div class="container">
    <?= Breadcrumbs::widget([
        'itemTemplate' => "\n\t<li class=\"breadcrumb-item\"><i>{link}</i></li>\n", // template for all links
        'activeItemTemplate' => "\t<li class=\"breadcrumb-item active\">{link}</li>\n", // template for the active link
        'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
    ]) ?>
    <?= Alert::widget() ?>
    <?= $content ?>
</div>```


Data Grid ActionColumn One of my pages was a data grid generated for me by gii. On each row it has a set of buttons that you can click to view, edit or delete the row. Under Bootstrap 4, the ActionColumn disappeared. Viewing the page source showed me it was there, but I couldn't see it or click on it. Going to the migration guide, it turns out that Bootstrap 3 includes icons, but Bootstrap 4 doesn't. I got a lot of help from a question asked in the Yii2forum.. In the end, my solution was to get a local copy of FontAwesome 5 by including the line "fortawesome/font-awesome": "^5.12.1" in the require section of composer.json, and then choosing the icons that I wanted. I spent a lot of time figuring out how to do this, but when I was done, it seemed almost anti-climactic in it's simplicity. This is what I did in my data form:

            ['class' => 'yii\grid\ActionColumn',
                'buttons' => [
                    'update' =>  function($url,$model) {
                        return Html::a('<i class="fas fa-edit"></i>', $url, [
                            'title' => Yii::t('app', 'update')
                        ]);
                    },
                    'view' =>  function($url,$model) {
                        return Html::a('<i class="fas fa-eye"></i>', $url, [
                            'title' => Yii::t('app', 'view')
                        ]);
                    },
                    'delete' => function($url,$model) {
                        return Html::a('<i class="fas fa-trash"></i>', $url, [
                            'title' => Yii::t('app', 'delete')
                        ]);
                    }
                 ]
            ],


Functional Tests

Not seeing anything more visually, at least nothing that was obvious, I now ran the suite of tests. These were all previously passing, but now several of them failed. One of them was the Contact Form, so I ran that one separately and the tests informed me they were failing because they couldn't see the error message:

1) ContactCest: Check contact submit no data
 Test  ../frontend/tests/functional/ContactCest.php:checkContactSubmitNoData
 
 Step  See "Name cannot be blank",".help-block"
 
 Fail  Element located either by name, CSS or XPath element with '.help-block' was not found.


I, on the other hand, could see the error messages on the form, so I used the browser's page source and discovered that the css class was no longer "help-block", it had changed to "invalid-feedback". Easy enough - in frontend/tests/_support/FunctionalTester.php, I changed the expected css class:

public function seeValidationError($message)
{
    $this->see($message, '.invalid-feedback');
}

Of course, this little excerpt is just an example. I found the same thing had to be done in several locations, but all were easily found and resolved.


After this, running my tests pointed me to no other problems, but I don't expect that to mean there aren't any other problems. While everything seems to be working so far, I expect there are more issues hiding in the woodwork. Somehow, those problems don't seem quite so insurmountable anymore.

]]>
0
[wiki] UUID instead of an auto-increment integer for ID with Active Record Wed, 22 Apr 2020 13:09:03 +0000 https://www.yiiframework.com/wiki/2555/uuid-instead-of-an-auto-increment-integer-for-id-with-active-record https://www.yiiframework.com/wiki/2555/uuid-instead-of-an-auto-increment-integer-for-id-with-active-record grigori grigori

I have a dream ... I am happy to join with you today in what will go down in history as the greatest demonstration of bad design of Active Record.

I have an API. It's built with a RESTful extension over Active Record, and some endpoints provide PUT methods to upload files. By a REST design we create an entity with POST /video first, and then upload a video file with PUT /video/{id}/data.

How do we get the {id}? The essential solutuion is UUID generated by a client. It allows API application to be stateless and scale it, use master-master replication for databases and feel yourself a modern guy. If you have Postgres — lucky you, feel free to use the built-in UUID data type and close this article. With MySQL the essential solution is insert into users values(unhex(replace(uuid(),'-',''))... MySQL team recommends updating our INSERT queries. With Active Record it is not really possible. For fetching UUIDs it recommends adding a virtual column — this can be used.

If you design the application from ground up, you can use defferent fields for a binary and text representation of UUID, and reference them in different parts of an application, but I am bound to the legacy code.

Adding getId()/setId() won't help - data comes from a client in JSON and fills the model object with a setAttributes() call avoiding generic magic methods.

Here's the hack:

Step 1. Add a private $idText property

use yii\db\ActiveRecord;
class Video extends ActiveRecord
{
    private $idText;

Step 2. Add two validators and a filter

//check if value is a valid UUID
['id','match', 'pattern'=>'/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i'],
// convert UUID from text value to binary and store the text value in a private variable,
// this is a workaround for lack of mapping in active record
['id','filter','skipOnError' => true, 'filter' => function($uuid) {
    $this->idText = $uuid;
    return pack("H*", str_replace('-', '', $uuid));
}],
//now let's check if ID is taken
['id','unique','filter' => function(\yii\db\Query $q) {
    $q->where(['id' => $this->getAttribute('id')]);
}],

First rule is a validator for an input. Second rule is a filter preparing UUID to be written in a binary format and keeping the text form for output. Third one is a validator running a query over the binary value generated by a filter.

Note: I wrote $this->getAttribute('id'), $this->id returns a text form.

We can write a query to validate data, not to save it.

Step 3. Add getters

public function __get($name)
{
    return ($name === 'id') ? $this->getId() : parent::__get($name);
}

/**
 * Return UUID in a textual representation
 */
public function getId(): string
{
    if ($this->idText === NULL && $this->getIsNewRecord()){
        //the filter did not convert ID to binary yet, return the data from input
        return strtoupper($this->getAttribute('id'));
    }
    //ID is converted
    return strtoupper($this->idText ?? $this->getAttribute('id_text'));
}

When we call the $model->id property we need the getId() executed. But Active Record base class overrides Yii compoent default behavior and does not call a getter method of an object if a property is a field in a table. So I override the magic getter. From the other hand, a regexp valiator I wrote calls $model->id, triggering the getter before the UUID is saved to the private property. I check if the object is newly created to serve the text value for validator.

Note the strtoupper() call: client may send UUID in both upper and low cases, but after unpacking from binary we will have a value in upper case. I received different string values before storing data to DB and after fetching it. Convert the textual UUID value to an upper or lower case everywhere to avoid problems.

It looks weird to mutate data in a validator, but I found this is the best way. I belive I shouldn't use beforeSave() callback to set the binary value for generating SQL, and return the text value back in afterSave() - supporting this code would be a classic hell like #define true false;.

Step 4. Define the mapping for output

public function fields()
{
    $fields = parent::fields();
    $fields['id'] =function(){return $this->getId();};
    return $fields;
}

This method is used by RESTful serializers to format data when you access your API with GET /video requests.

So, now you can go the generic MySQL way

Step 5. add a virtual column

ALTER TABLE t1 ADD id_text varchar(36) generated always as
 (insert(
    insert(
      insert(
        insert(hex(id_bin),9,0,'-'),
        14,0,'-'),
      19,0,'-'),
    24,0,'-')
 ) virtual;

Step 5. Use Object Relation Mapping in Yii 3 when it's available and write mapping instead of these hacks.

P.S. A couple of helper functions.

declare(strict_types=1);

namespace common\helpers;


class UUIDHelper
{
    const UUID_REGEXP = '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';

    public static function string2bin(string $uuid): string
    {
        return pack("H*", str_replace('-', '', $uuid));
    }

    public static function bin2string(string $binary): string
    {
        return strtolower(join("-", unpack("H8time_low/H4time_mid/H4time_hi/H4clock_seq_hi/H12clock_seq_low", $binary)));
    }

    public static function isUUID(string $uuid): bool
    {
        return (bool)preg_match(self::UUID_REGEXP,$uuid);
    }
}
]]>
0
[wiki] Yii v2 snippet guide Tue, 31 Oct 2023 08:29:37 +0000 https://www.yiiframework.com/wiki/2552/yii-v2-snippet-guide https://www.yiiframework.com/wiki/2552/yii-v2-snippet-guide rackycz rackycz
  1. My articles
  2. Intro
  3. Prerequisities
  4. Yii demo app + GitLab
  5. Automatical copying from GitLab to FTP
  6. User management + DB creation + login via DB
  7. i18n translations
  8. Switching languages + session + lang-dropdown in the top menu
  9. Formatting values based on your Locale
  10. Simple access rights
  11. Nice URLs
  12. How to redirect web to subfolder /web
  13. Auto redirection from login to desired URL
  14. What to change when exporting to the Internet
  15. Saving contact inqueries into DB
  16. Tests - unit + opa
  17. Adding a google-like calendar
  18. Scenarios - UNKNOWN SCENARIO EXCEPTION
  19. Richtext / wysiwyg HTML editor - Summernote
  20. SEO optimization
  21. Other useful links
  22. jQuery + draggable/droppable on mobile devices (Android)
  23. Enhancing Gii
  24. Webproject outsite docroot (htdocs) folder (Windows)
  25. Modal window + ajax
  26. Simple Bootstrap themes
  27. Yii2 + Composer
  28. Favicon
  29. GridView + DatePicker in filter + filter reset
  30. Drop down list for foreign-key column
  31. GridView - Variable page size
  32. Creating your new helper class
  33. Form-grid renderer
  34. Netbeans + Xdebug
  35. PDF - no UTF, only English chars - FPDF
  36. PDF - UTF, all chars - tFPDF
  37. PDF - 1D & 2D Barcodes - TCPDF
  38. Export (not only GridView) to CSV in UTF-8 without extensions
  39. Next chapters had to be moved to a new article!

My articles

Articles are separated into more files as there is the max lenght for each file on wiki.

Intro

Hi all!

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.

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

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 or command "ssh-keygen" in Windows10. 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.

Note: Here you can add the public SSH key to GitLab. Private key must be named "id_rsa" and stored in Win10 on path C:\Users\{username}\.ssh\id_rsa

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.

You need to create file .gitlab-ci.yml in the root of your repository with following content. It will fire a Pipeline job on commit using "LFTP client" automatically. If you want to do it manually, add "when:manual", see below.

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 --exclude .gitlab-ci.yml" 
  only:
    - master
  when: manual

I just added some exclusions (see the code) and will probably add --delete in the future. Read linked webs.

Important info: Your FTP server might block foreign IPs. If this happens, your transfer will fail with error 530. You must findout GitLab's IPs and whitelist them. [This link]( https://docs.gitlab.com/ee/user/gitlab_com/#ip-range) might help.

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

Formatting values based on your Locale

Go to config\web.php and add following values:

$config = [
  // ..
 'language' => 'cs-CZ', 
 // \Yii::$app->language: 
 // https://www.yiiframework.com/doc/api/2.0/yii-base-application#$language-detail
//..
 'components' => [
  'formatter' => [
   //'locale' => 'cs_CZ', 
   // Only effective when the "PHP intl extension" is installed else "language" above is used: 
   // https://www.php.net/manual/en/book.intl.php

   //'language' => 'cs-CZ', 
   // If not set, "locale" above will be used:
   // https://www.yiiframework.com/doc/api/2.0/yii-i18n-formatter#$language-detail
      
   // Following values might be usefull for your situation:
   'booleanFormat' => ['Ne', 'Ano'],
   'dateFormat' => 'yyyy-mm-dd', // or 'php:Y-m-d'
   'datetimeFormat' => 'yyyy-mm-dd HH:mm:ss', // or 'php:Y-m-d H:i:s'
   'decimalSeparator' => ',',
   'defaultTimeZone' => 'Europe/Prague',
   'thousandSeparator' => ' ',
   'timeFormat' => 'php:H:i:s', //  or HH:mm:ss
   'currencyCode' => 'CZK',
  ],

In GridView and DetailView you can then use following and your settings from above will be used:

'columns' => [
 [
  'attribute' => 'colName',
  'label' => 'Value',
  'format'=>['decimal',2]
 ],
 [
   'label' => 'Value', 
   'value'=> function ($model) { return \Yii::$app->formatter->asDecimal($model->myCol, 2) . ' EUR' ; } ],
 ]
 // ...
]

PS: I do not use currency formatter as it always puts the currency name before the number. For example USD 123. But in my country we use format: 123 CZK.

More links on this topic:

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

... see next chapters ...

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"></span> ', 
			  ['user/update', 'id' =>  $model->id], 
			  ['class' => 'openInMyModal', 'onclick'=>'return false;', 'data-myModalTitle'=>'']
		    );
          },
        ],
        '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="myModalTitle">Title</span>',
  'id' => 'myModalDialog',
  'size' => 'modal-lg',
  'clientOptions' => [
      // https://getbootstrap.com/docs/3.3/javascript/#modals-options
      'keyboard' => false, // ESC key won't close the modal
      'backdrop' => 'static', // clicking outside the modal will not close it
      ],
]);
echo "<div id='myModalContent'></div>";
yii\bootstrap\Modal::end();

$this->registerJs(
 "// If you use $(document).on, it will handle also events on elements rendered by AJAX.
   $(document).on('click','a.openInMyModal',function(e){  
  // And if you use $('a.openInMyModal'), it will work only on standard elements
  // $('a.openInMyModal').click(function(e){  
  
  // Prevents the browsers default behaviour (such as opening a link)
  // ... but does not stop the event from bubbling up the DOM
  e.preventDefault(); 
  
  // Prevents the event from bubbling up the DOM
  // ... but does not stop the browsers default behaviour
  // e.stopPropagation(); 
  
  // Prevents other listeners of the same event from being called
  // e.stopImmediatePropagation(); 
  
  // Good idea is to set onclick='return false;' to the link if it is in a modal window
  
  let title = $(this).attr('data-myModalTitle');
  if (title==undefined) { title = ''; }
  
  $('#myModalDialog #myModalTitle').text(title);
  $('#myModalDialog').find('#myModalContent').html('');
  $('#myModalDialog').modal('show')
    .find('#myModalContent')
    .load($(this).attr('href'));
  return false;
  });",
  yii\web\View::POS_READY,
  'myModalHandler'
);
?>

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'});"
        . "$('#myModalDialog').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'
  ],
        
  // ...
        

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.

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.

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

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

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

Creating your new helper class

Sometimes you need a static class that will do things for you. This is what helpers do.

I work with the Basic example so I do things like this:

  • Create folder named "myHelpers" next to the folder "controllers"
  • Place there your class and do not forget about the "namespace":
<?php
namespace myHelpers;
class MyClassName { /* ... */ }
  • Now open file index.php and add following row:
require __DIR__ . '/../myHelpers/MyClassName.php';
  • If you want to use the class, do not forget to include the file:
use myHelpers\MyClassName;
// ...
echo MyClassName::myMethod(123);

Form-grid renderer

If you want your form to be rendered in a grid, you can use your custom new helper to help you. How to create helpers is mentioned right above. The helper then looks like this:

<?php
namespace myHelpers;

class GridFormRenderer {
  
  // https://www.w3schools.com/bootstrap/bootstrap_grid_system.asp
  // Bootstrap works with 12-column layouts so max span is 12.
  public static function renderGridForm($gridForm, $colSize = 'md', $nullReplacement = '&nbsp;', $maxBoootstrapColSpan = 12) {
    $result = '';
    foreach ($gridForm as $row) {
      if (is_null($row)) {
        $colSpan = $maxBoootstrapColSpan;
        $result .= '<div class="row">' . '<div class="col-' . $colSize . '-' . $colSpan . '">' . $nullReplacement . '</div></div>';
        continue;
      }
      $colSpan = floor($maxBoootstrapColSpan / count($row));
      $result .= '<div class="row">';
      foreach ($row as $col) {
        if (is_null($col)) {
          $col = $nullReplacement;
        }
        $result .= '<div class="col-' . $colSize . '-' . $colSpan . '">' . $col . '</div>';
      }
      $result .= '</div>';
    }
    return $result;
  }
}

And is used like this in any view:

use myHelpers\GridFormRenderer;
// ...

$form = ActiveForm::begin();

$username = $form->field($model, 'username')->textInput(['maxlength' => true]);
$password_new = $form->field($model, 'password_new')->passwordInput(['maxlength' => true]);
$password_new_repeat = $form->field($model, 'password_new_repeat')->passwordInput(['maxlength' => true]);
$email = $form->field($model, 'email')->textInput(['maxlength' => true]);

$gridForm = [
  [$username, null, $email], // null = empty cell
  null, // null = empty row
  [$password_new, $password_new_repeat],
  ];

echo GridFormRenderer::renderGridForm($gridForm);

ActiveForm::end();
// ...

The result is that your form has 3 rows, the middle one is empty. In the first row there are 3 cells (username, nothing, email) and in the last row there is 2x password.

You do not have to write any HTML, you only arrange inputs into any number of rows and columns (using the array $gridForm) and things just happen automagically.

Netbeans + Xdebug

Note: I am using Windows 10 + XAMPP

I had to follow 2 manuals:

The result in C:\xampp\php\php.ini was:

[XDebug]
zend_extension = c:\xampp\php\ext\php_xdebug.dll
xdebug.remote_enable = on
xdebug.idekey = netbeans-xdebug
xdebug.remote_host = localhost
xdebug.remote_port = 9000
xdebug.remote_autostart=on
xdebug.var_display_max_depth=5

The last row changes behaviour of var_dump() when xdebug is installed. It does not output whole arrays, but max 3 levels. Read here or here.

Quotes were not important. I didnt even need to download current version of xdebug, it was already in folder C:\xampp\php\ext.

Important also is to righ-click your project, select Properties, then menu "Run configurations" and set correct path to your index.php. Best is to use the button "Browse"

Then you just add a breakpoint, click the debug-play button in NetBeans and refresh your browser. Netbeans will stop the code for you.

PDF - no UTF, only English chars - FPDF

For creating PDFs can be used FPDF library. It is extremely simple to make it run. Just download it and then use it as a helper - I described how this is done above. Do not forget to add namespace to FPDF.php.

You will only need FPDF.php and folder font. Then in your controller just do this:

use myHelpers\FPDF;
// ...
$pdf = new FPDF();
$pdf->AddPage();
$pdf->SetFont('Arial','B',16);
$pdf->Cell(40,10,'Hello World!');
$pdf->Output('D', 'hello.pdf');

Note: I renamed original file fpdf.php to FPDF.php

The only disadvantage is that UTF cannot be used and conversion to older encodings is required. For Czech Republic all texts must be converted like this:

private function convertUtf8ToWin1250($value) {
  $value = trim($value);
  if (strlen($value)==0) {
    // Warning:
    // Method strlen() returns number of bytes, not necessiraly number of characters.
    // Usually it is the same, but not always.
    // see also mb_strlen()
    return '';
  }
  return iconv("UTF-8", "WINDOWS-1250//IGNORE", $value );
}

A discussion is available here.

PDF - UTF, all chars - tFPDF

When you need non-English characters, tFPDF should be used. It is the same as FPDF so FPDF documentation and manual can be used. It only modifies character-handling.

Just download it and then use it as a helper - I described how this is done above.

Summary:

  • Download tFPDF and unpack it.
  • use file tfpdf.php and folder font .. it contains file ttfonts.php !!
  • Into both mentioned php files add the namespace you are using for your helpers.
  • Do other modifications needed to use it as a Helper. Explained above.

tFPDF example:

$pdf = new tFPDF();

$pdf->AddFont('DejaVu','','DejaVuSansCondensed.ttf',true);
$pdf->AddFont('DejaVu','B','DejaVuSansCondensed-Bold.ttf',true);
$pdf->SetFont('DejaVu','',10);

$pageWidth = 210;
$pageMargin = 10;
$maxContentW = $pageWidth - 2*$pageMargin; // = max width of an element

$pdf->SetAutoPageBreak(true, 0);
$pdf->SetMargins($pageMargin, $pageMargin, $pageMargin);
$pdf->SetAutoPageBreak(true, $pageMargin);

// Settings for tables:
$pdf->SetLineWidth(0.2);
$pdf->SetDrawColor(0, 0, 0);

$pdf->AddPage();
/ $pdf->SetFontSize(8);

$displayBorders = 1;
$valueAlign = "L";
$labelAlign = "L";

$usedHeight = 0;

// Logo on the 1st line
$pdf->SetY($pageMargin);
$pdf->SetX($pageMargin);
$logoPath = '../tesla.png';
$imgWidth = 10;
$headerHeight = 10;
$pdf->Image($logoPath, null, null, $imgWidth, $headerHeight);

$pdf->SetY($pageMargin);
$pdf->SetX($pageMargin+$imgWidth);
$pdf->Cell($maxContentW-$imgWidth, $headerHeight, 'Non English chars: ěščřžýáíéúů', $displayBorders, 0, 'C', false);

$usedHeight+= $headerHeight;
$usedHeight+=10;
        
$pdf->SetY($pageMargin);
$pdf->SetX($pageMargin+$imgWidth);
$pdf->Cell($maxContentW-$imgWidth, 10, 'Non English chars: ěščřžýáíéúů', $displayBorders, 0, 'C', false);

$pdf->SetY($pageMargin + $usedHeight);
$pdf->SetX($pageMargin);
$pdf->Cell(30, 10, 'Customer number:', $displayBorders, 0, 'R', false);

$pdf->SetFont('DejaVu','B',14);

$pdf->SetY($pageMargin + $usedHeight);
$pdf->SetX($pageMargin + 30);
$pdf->Cell(20, 10, 'ABC123', $displayBorders, 0, 'L', false);

$pdf->Output('D', 'hello.pdf');

Note to tFPFD: Once you use it, it creates a few PHP and DAT files in folder unifont. Delete them before uploading to the internet. They will contain hardcoded paths to fonts and must be recreated.

PDF - 1D & 2D Barcodes - TCPDF

See part II of this guide:

Export (not only GridView) to CSV in UTF-8 without extensions

I will describe how to easily export GridView into CSV so that filers and sorting is kept. I do not use any extentions which are so famous today. Note that GridView is not needed, I just want to show the most complicated situation.

Let's say you have page on URL user/index and it contains GridView where you can list and filter users.

Note: In class yii\data\Sort, in method getAttributeOrders(), is the sorting parameter taken from Yii::$app->getRequest() so the name of the sorted column must be in the URL you are using at the moment. This is why sorting might not work if you want to run UserSearch->search() manually without any GET parameters available in Yii::$app->request->queryParams.

The basic method for exporting DataProvider is here:

public function exportDataProviderToCsv($dataProvider) {

  // Setting infinite number of rows per page to receive all pages at once
  $dataProvider->pagination->pageSize = -1;

  // All text-rows will be placed in this array. 
  // We will later use implode() to insert separators and join everything into 1 large string
  $rows = [];

  // UTF-8 header = chr(0xEF) . chr(0xBB) . chr(0xBF)
  // Plus column names in format: 
  // ID;Username;Email etc based on your column names
  $rows [] = chr(0xEF) . chr(0xBB) . chr(0xBF) . User::getCsvHeader();

  foreach ($dataProvider->models as $m) {
    // Method getCsvRow() returns CSV row with values. Example:
    // 1;petergreen;peter.green@gmail.com ...
    $row = trim($m->getCsvRow());
    if ($row!='') {
      $rows[] = $row;  
    }
  }

  // Here we use implode("\n",$rows) to create large string with rows separated by new lines. 
  // Double quotes must be used around \n !
  $csv = implode("\n", $rows);

  $currentDate = date('Y-m-d_H-i-s');

  return \Yii::$app->response->sendContentAsFile($csv, 'users_' . $currentDate . '.csv', [
    'mimeType' => 'application/csv',
    'inline' => false
  ]);
}

If you want to use it to export data from your GridView, modify your action like this:

public function actionIndex($exportToCsv=false) {

  // These 2 rows already existed
  $searchModel = new UserSearch();
  $dataProvider = $searchModel->search(Yii::$app->request->queryParams)
        
  if ($exportToCsv) {
    $this->exportDataProviderToCsv($dataProvider);  
    return;       
  }
  // ...
}

And right above your GridView place this link:

<?php 
  // Pjax::begin(); // If you are using Pjax for GridView, it must start before following buttons.
?>

<div style="display:flex;flex-direction:row;">
  <?= Html::a('+ Create new record', ['create'], ['class' => 'btn btn-success']) ?>
  &nbsp;
  <div class="btn-group">
    <button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
      Export to CSV&nbsp;<span class="caret"></span>
    </button>
    <ul class="dropdown-menu">
      <li><?php
          echo Html::a('Ignore filters and sorting', ['index', 'exportToCsv' => 1], ['target' => '_blank', 'class' => 'text-left', 'data-pjax'=>'0']);
          // 'data-pjax'=>'0' is necessaary to avoid PJAX. 
          // Now we need to open the link in a new tab, not to resolve it as an ajax request.
          ?></li>
      <li><?php
          $csvUrl = \yii\helpers\Url::current(['exportToCsv' => 1]);
          echo Html::a('Preserve filters and sorting', $csvUrl, ['target' => '_blank', 'class' => 'text-left', 'data-pjax'=>'0']);
          // 'data-pjax'=>'0' is necessaary to avoid PJAX. 
          // Now we need to open the link in a new tab, not to resolve it as an ajax request.
          ?></li>
    </ul>
  </div>
</div>

<php
// Here goes the rest ... 
// echo GridView::widget([
// ...
?>

In my code above there were used 2 methods in the model which export things to the CSV format. My implementatino is here:

public static function getCsvHeader() {
  $result = [];
  $result[] = "ID";
  $result[] = "Username";
  $result[] = "Email";
  // ...
  return implode(";", $result);
}
public function getCsvRow() {
  $result = [];
  $result[] = $this->id;
  $result[] = $this->username;
  $result[] = $this->email;
  // ...
  return implode(";", $result);
}

Next chapters had to be moved to a new article!

]]>
0
[wiki] Change default date format in Oracle Fri, 20 Sep 2019 06:15:43 +0000 https://www.yiiframework.com/wiki/2551/change-default-date-format-in-oracle https://www.yiiframework.com/wiki/2551/change-default-date-format-in-oracle lenovo7 lenovo7

Default date format in Oracle is DD-MON-RR (25-JAN-18). With that output, we can't using date formatting.

Too solve this issue, we must change date format oracle like date commonly using

ALTER SESSION SET NLS_DATE_FORMAT = ...

Add this script inside your database connection file

<?php

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'oci:host=127.0.0.1:1521/XE',
    'username' => 'your_username',
    'password' => 'your_password',
    'charset' => 'utf8',

    // Schema cache options (for production environment)
    //'enableSchemaCache' => true,
    //'schemaCacheDuration' => 60,
    //'schemaCache' => 'cache',

    'on afterOpen' => function($event) {
        // $event->sender refers to the DB connection
        $event->sender->createCommand("ALTER SESSION SET NLS_DATE_FORMAT='DD-MM-YYYY hh24:mi:ss'")->execute();
    }    
];
]]>
0