Live News for Yii Framework News, fresh extensions and wiki articles about Yii framework. Fri, 19 Apr 2024 11:44:45 +0000 Zend_Feed_Writer 2 (http://framework.zend.com) https://www.yiiframework.com/ [news] Yii Validator 1.3 Fri, 19 Apr 2024 11:44:44 +0000 https://www.yiiframework.com/news/628/yii-validator-1-3 https://www.yiiframework.com/news/628/yii-validator-1-3 vjik vjik

Yii Validator package was updated with the following enhancements and new features:

  • added methods addErrorWithFormatOnly() and addErrorWithoutPostProcessing() to Result object;
  • added Image validation rule;
  • added Date, DateTime and Time validation rules;
  • clarified psalm types in Result.
]]>
0
[news] Yii Arrays 3.1 Thu, 04 Apr 2024 11:09:36 +0000 https://www.yiiframework.com/news/627/yii-arrays-3-1 https://www.yiiframework.com/news/627/yii-arrays-3-1 vjik vjik

Minor version of Yii Arrays package is released.

  • Added ArrayHelper::parametrizedMerge() method that allows to merge two or more arrays recursively with specified depth.
  • Added ArrayrHelper::renameKey() method.
  • Removed null from return type of ArrayHelper::getObjectVars() method.
  • Minor ArrayableTrait refactoring.
]]>
0
[news] Yii Hydrator 1.2 Fri, 19 Apr 2024 11:44:44 +0000 https://www.yiiframework.com/news/626/yii-hydrator-1-2 https://www.yiiframework.com/news/626/yii-hydrator-1-2 vjik vjik

Minor version of Yii Hydrator package is released.

  • Added ToDateTime parameter attribute.
  • Added Trim, LeftTrim and RightTrim parameter attributes.
  • Raised the minimum version of PHP to 8.1.
]]>
0
[extension] asminog/yii2-proxy Fri, 19 Apr 2024 11:44:44 +0000 https://www.yiiframework.com/extension/asminog/yii2-proxy https://www.yiiframework.com/extension/asminog/yii2-proxy asminog asminog

HTTP Proxy Extension for Yii 2

  1. Installation
  2. Usage

This is a simple proxy for Yii2 framework. This extension provides the HTTP proxy action for the Yii framework 2.0.

For license information check the LICENSE-file.

Build Status Build Status

GitHub repo file count GitHub code size in bytes

Installation

composer require asminog/yii2-proxy

Usage

use asminog\proxy\ProxyAction;

class SiteController extends Controller
{
    public function actions()
    {
        return [
            'proxy' => [
                'class' => ProxyAction::class,
                // 'accessToken' => 'your-access-token', // - set access token for secure requests
                // 'throw404Exception' => true, // - show 404 error if access token is not valid or request url is not valid
                // 'proxyHeaders' => ['User-Agent', 'Content-Type'], // - set headers for proxy request
                // 'proxyCookies' => ['cookie1', 'cookie2'], // - set cookies for proxy request
            ],
        ];
    }
}
]]>
0
[news] Sphinx extension 2.0.16 released Fri, 19 Apr 2024 11:44:44 +0000 https://www.yiiframework.com/news/625/sphinx-extension-2-0-16-released https://www.yiiframework.com/news/625/sphinx-extension-2-0-16-released samdark samdark

We are very pleased to announce the release of Sphinx extension version 2.0.16. This release adds UINT_SET attribute type support and ensures compatibility with Sphinx 3 by running tests against it.

See the CHANGELOG for details.

]]>
0
[news] Yii Cycle 1.0 Thu, 21 Mar 2024 08:56:15 +0000 https://www.yiiframework.com/news/624/yii-cycle-1-0 https://www.yiiframework.com/news/624/yii-cycle-1-0 arogachev arogachev

One more package has been released. This time it's a wrapper around Cycle ORM for Yii ecosystem. Cycle is known for support both classic and daemonized PHP applications based for example on RoadRunner.

The package offers a set of console commands and tools for convinient work with DB migrations and schema.

Right before the release the decision was taken to move the part related with Yii Data adaptation to a separate package - Yii Data Cycle, similar to Yii Data DB. All of them will be released too soon enough. First Yii Data 2.0, then first releases of adapter packages will follow.

Just a reminder, using Yii Cycle in demo doesn't mean it's a recommended tool / default package for working with DB. You can also choose Yii DB for example.

Code coverage with tests, mutational index and static analysis level were increased to their maximum.

]]>
0
[news] Yii DB and drivers minor releases Thu, 21 Mar 2024 07:41:37 +0000 https://www.yiiframework.com/news/623/yii-db-and-drivers-minor-releases https://www.yiiframework.com/news/623/yii-db-and-drivers-minor-releases vjik vjik

Yii Database package was tagged along with its drivers.

Yii Database 1.3

  • Deprecate unnecessary argument $rawSql of AbstractCommand::internalExecute()
  • Specify result type of QueryInterface::all(), CommandInterface::queryAll() and DbArrayHelper::populate() methods to array[]
  • Specify populate closure type in BatchQueryResultInterface
  • Skip calling CommandInterface::getRawSql() if no logger or profiler is set
  • Specify result type of ConstraintSchemaInterface::getTableIndexes() method to IndexConstraint[]
  • Remove unused code in AbstractSchema::getTableIndexes()
  • Refactor AbstractCommand::getRawSql()
  • Refactor AbstractSchema::getDataType()
  • Remove unnecessary type casting to array in AbstractDMLQueryBuilder::getTableUniqueColumnNames()
  • Add message type to log context
  • Allow to use DMLQueryBuilderInterface::batchInsert() method with empty columns
  • Deprecate AbstractSchema::normalizeRowKeyCase() method
  • Deprecate SchemaInterface::getRawTableName() and add Quoter::getRawTableName() method
  • Deprecate SchemaInterface::isReadQuery() and add DbStringHelper::isReadQuery() method
  • Remove unnecessary symbol \\ from rtrim() function inside DbStringHelper::baseName() method
  • Minor refactoring of SchemaCache, AbstractPdoCommand and AbstractDDLQueryBuilder
  • Add psalm type for parameters to bind to the SQL statement
  • Add more specific psalm type for QueryFunctionsInterface::count() result
  • Fix Query::count() when it returns an incorrect value if the result is greater than PHP_INT_MAX
  • Fix bug of AbstractCommand::getRawSql() when a param value is Stringable object
  • Fix casting integer to string in AbstractCommand::getRawSql()
  • Fix bug with Quoter::$tablePrefix when change AbstractConnection::$tablePrefix property

Yii DB PostgreSQL Driver 1.3

  • Support structured type
  • Change property Schema::$typeMap to constant Schema::TYPE_MAP
  • Create instance of ArrayParser directly
  • Resolve deprecated methods
  • Minor DDLQueryBuilder refactoring
  • Support table view constraints
  • Exclude from index column names fields specified in INCLUDE clause

Yii DB MySQL Driver 1.2

  • Change property Schema::$typeMap to constant Schema::TYPE_MAP
  • Resolve deprecated methods
  • Minor refactoring of DDLQueryBuilder and Schema
  • Fix Command::insertWithReturningPks() method for empty values

Yii DB SQLite Driver 1.2

  • Remove unused code in Command class
  • Change property Schema::$typeMap to constant Schema::TYPE_MAP
  • Remove unnecessary check for array type in Schema::loadTableIndexes()
  • Resolve deprecated methods
  • Minor refactoring of DDLQueryBuilder and Schema

Yii DB MSSQL Server Driver 1.2

  • Change property Schema::$typeMap to constant Schema::TYPE_MAP
  • Resolve deprecated methods
  • Minor refactoring of Command and Quoter
  • Fix DMLQueryBuilder::insertWithReturningPks() and Command::insertWithReturningPks() methods

Yii DB Oracle Driver 1.3

  • Change property Schema::$typeMap to constant Schema::TYPE_MAP
  • Allow to use DMLQueryBuilderInterface::batchInsert() method with empty columns
  • Resolve deprecated methods
  • Fix execution Query without table(s) to select from
  • Fix Command::insertWithReturningPks() method for table without primary keys
  • Fix, table sequence name should be null if sequence name not found
]]>
0
[news] Yii Router FastRoute 3.1 Thu, 04 Apr 2024 08:11:58 +0000 https://www.yiiframework.com/news/622/yii-router-fastroute-3-1 https://www.yiiframework.com/news/622/yii-router-fastroute-3-1 vjik vjik

Minor verison of Yii Router FastRoute package was released.

  • added optional default host and scheme to UrlGenerator;
  • added support for psr/http-message version ^2.0.
]]>
0
[news] Yii Definitons 3.3 Thu, 04 Apr 2024 08:11:36 +0000 https://www.yiiframework.com/news/621/yii-definitons-3-3 https://www.yiiframework.com/news/621/yii-definitons-3-3 vjik vjik

Yii Definitions package was updated with the following enhancements and fixes:

  • allowed multiple method call in array definition;
  • fixed crash when intersection types are used.
]]>
0
[extension] sandritsch91/yii2-widget-flatpickr Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/sandritsch91/yii2-widget-flatpickr https://www.yiiframework.com/extension/sandritsch91/yii2-widget-flatpickr Sandritsch91 Sandritsch91

yii2-flatpickr

A flatpickr widget for Yii2

]]>
0
[extension] sandritsch91/yii2-widget-form-wizard Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/sandritsch91/yii2-widget-form-wizard https://www.yiiframework.com/extension/sandritsch91/yii2-widget-form-wizard Sandritsch91 Sandritsch91

yii2-form-wizard

  1. Features
  2. Installation
  3. Usage
  4. Contributing

A Yii2 form-wizard widget for bootstrap 5

Alt preview

Features

  • Bootstrap 5
  • Client side validation, with the option to validate each step separately

Installation

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist sandritsch91/yii2-form-wizard

or add

"sandritsch91/yii2-form-wizard": "*"

to the require section of your composer.json

Usage

use sandritsch91\yii2-form-wizard\FormWizard;

echo FormWizard::widget([
    // required
    'model' => $model,                                                          // The model to be used in the form
    'tabOptions' => [                                                           // These are the options for the Bootstrap Tab widget                                        
        'items' => [
            [
                'label' => 'Step 1',                                            // The label of the tab, if omitted, a default-label will be used (Step 1, Step 2, ...)
                'content' => $this->render('_step1', ['model' => $model]),      // Either the content of the tab
            ],
            [
                'label' => 'Step 2',
                'view' => '/test/_step2',                                       // or a view to be rendered. $model and $form are passed to the view
                'params' => ['a' => 1, 'b' => 2]                                // Pass additional parameters to the view
            ]
        ],
        'navType' => 'nav-pills'
    ],
    // optional
    'validateSteps' => [                                                        // Optional, pass the fields to be validated for each step.                 
        ['name', 'surname'],
        [],                                                                     // Leave array empty if no validation is needed  
        ['email', 'password']
    ],
    'options' => [],                                                            // Wizard-container html options
    'formOptions' => [],                                                        // Form html options
    'buttonOptions' => [                                                        // Button html options
        'previous' => [
            'class' => ['btn', 'btn-secondary'],
            'data' => [
                'formwizard' => 'previous'                                      // If you change this, make sure the clientOptions match
            ]
        ],
        'next' => [...],
        'finish' => [...]
    ],
    'clientOptions' => [                                                        // Client options for the form wizard, if you need to change them
        // 'finishSelector' => '...',
        // 'nextSelector' => '...',
        // 'previousSelector' => '...',
        // 'keepPosition' => true                                               // Keep scroll position on step change.
                                                                                // Set to false to disable, or pass a selector if you have a custom scroll container.
                                                                                // Defaults to true.
    ],
    'clientEvents' => [                                                         // Client events for the form wizard
        // 'onNext' => 'function () {...}',
        // 'onPrevious' => 'function () {...}',
        // 'onFinish' => 'function (){...}'
    ]
]);

Contributing

Contributions are welcome.

If you have any questions, ideas, suggestions or bugs, please open an issue.

Testing

This package uses codeception for testing. To run the tests, run the following commands:


#### Unit tests

run ```php.exe .\vendor\bin\codecept run Unit``` in the root directory of this repository.

#### Functional tests

run ```php.exe .\vendor\bin\codecept run Functional``` in the root directory of this repository.

#### Accpetance tests

To be able to run acceptance tests, a few requirements are needed:

For Windows:\

- install java runtime environment
- install nodejs
- install selenium-standalone: `npm install -g selenium-standalone`
- start selenium-standalone: `selenium-standalone install && selenium-standalone start`
- host a yii2 application on a server or locally via ```./yii serve```
    - add this plugin as a dependency to your ```composer.json``` and update dependencies
    - site must be reachable over http://formwizard.com/
    - add an action ```actionTest``` to the ```SiteController```, as described below
    - this action must return a view file, as described below
    - run ```php.exe .\vendor\bin\codecept run Acceptance```

For Linux:  
Never did that before, but I think it is similar to the Windows setup.

The action in the SiteController:

```php
public function actionTest(): string
{
    include __DIR__ . '/../vendor/sandritsch91/yii2-widget-form-wizard/tests/Support/Data/models/User.php';

    $model = new User();

    if (Yii::$app->request->post() && $model->load(Yii::$app->request->post()) && $model->validate()) {
        return 'success';
    }

    return $this->render('test', [
        'model' => new User()
    ]);
}
```

The view returned by the action:

```php
/** @var User $model */

use sandritsch91\yii2\formwizard\FormWizard;
use sandritsch91\yii2\formwizard\tests\Support\Data\models\User;

$wizard = FormWizard::widget([
    'model' => $model,
    'tabOptions' => [
        'options' => [
            'class' => 'mb-3'
        ],
        'items' => [
            [
                'label' => 'Step 1',
                'view' => '@app/vendor/sandritsch91/yii2-widget-form-wizard/tests/Support/Data/views/site/step1',
                'linkOptions' => [
                    'id' => 'step1-link',,
                    'params' => [
                        'test' => 'some test variable'
                    ]
                ]
            ],
            [
                'label' => 'Step 2',
                'view' => '@app/vendor/sandritsch91/yii2-widget-form-wizard/tests/Support/Data/views/site/step2',
                'linkOptions' => [
                    'id' => 'step2-link',
                ]
            ],
            [
                'label' => 'Step 3',
                'view' => '@app/vendor/sandritsch91/yii2-widget-form-wizard/tests/Support/Data/views/site/step3',
                'linkOptions' => [
                    'id' => 'step3-link',
                ]
            ]
        ],
        'navType' => 'nav-pills'
    ],
    'validateSteps' => [
        ['firstname', 'lastname'],
        ['username', 'password', 'password_validate'],
        ['email']
    ],
    'clientOptions' => [
        'keepPosition' => true
    ]
]);

echo \yii\helpers\Html::tag('div', $wizard, [
    'class' => 'col-4'
]);
```

After the initial installation, you only have to start the selenium-standalone server ```selenium-standalone start```
and run the tests ```php.exe .\vendor\bin\codecept run Acceptance``` in the root directory of this repository.

If you do not want to setup an application, just run the unit and functional tests by
running ```php.exe .\vendor\bin\codecept run Unit,Functional```, I can modify and run the acceptance tests for you,
after you opened a pull request.

]]>
0
[news] Minor releases of Yii Runner, Yii HTTP Runner and Yii Console Runner Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/news/620/minor-releases-of-yii-runner-yii-http-runner-and-yii-console-runner https://www.yiiframework.com/news/620/minor-releases-of-yii-runner-yii-http-runner-and-yii-console-runner vjik vjik

Three runner pacakges was released.

Yii Runner 2.2 and Yii Console Runner 2.2

  • Added ability to set custom config merge plan file path, config and vendor directories.

Yii HTTP Runner 2.3

  • Added ability to set custom config merge plan file path, config and vendor directories.
  • Support stream output headers.
  • Don't used buffered output in SapiEmitter when body size is less than buffer.
  • Added support for psr/http-message version ^2.0.
]]>
0
[news] Big release of RBAC packages Thu, 07 Mar 2024 12:30:44 +0000 https://www.yiiframework.com/news/619/big-release-of-rbac-packages https://www.yiiframework.com/news/619/big-release-of-rbac-packages arogachev arogachev

RBAC packages got big update:

Let's see, what's new and what was fixed.

Raising PHP version

We bumped minimum PHP version to 8.1 in all RBAC packages.

Code quality

We raised the code quality according to set standards in all packages:

  • 1st level in static analysis by Psalm.
  • 100% code coverage by unit tests.
  • 100% in mutation testing.
Using storages via Manager

In the previous version we didn't pay sufficient attention to testing storages in conjunction with Manager. Remember that interaction with storages must be done exactly using Manager, not directly. The corresponding tests were added and a number of related problems were fixed.

In particular, the coupling of storages was removed (it was more actual for DB based implementations). Thus, it's now possible to fully use different combinations of storages in the way how it was initially planned. It was possible in Yii 2 too. One of the frequent cases is when permissions and roles are stored in PHP file (less updated), while assignments - in database (updated more often).

Also, the massive performance optimization was done.

Checking access
  • We fixed the issue, when rules were not executed for guests and other items in hierarchy tree branch.
  • Besides permission, now it's possible to check existence for user roles as well.
  • A permission is now allowed for a user with granted access at least by one branch of hierarchy tree.
Simple storage implementations

Simple storages implementations were added. Their main feature is filling the properties of a corresponding class during reading data from storages and further working with these properties. In addition to full testing of base package (it can't be used on its own) it allowed to get rid of repeated code in PHP implementation (with corresponding improvements for concurrency).

Specific changes for different storage implementations
RBAC PHP Storage
  • Concurrency support was added, this can be helpful when file is being edited simultaneously.
  • The problems with saving creation and update time of roles / permissions and assignments were fixed.
  • We slightly simplified the process of setting filename while configuring storages.

The feature of manual file edit was saved. Note that in this case you need to manually sync the changes in other storage. If you prefer this method - optional fields can be omitted, along with timestamps - when they are missing, file modification time will be used as a fallback. The file modification time is now customizable as well. Besides timestamps, its change acts like a trigger for updating data with enabled concurrency handling mode.

RBAC Cycle DB & RBAC Yii DB storages
Performance optimization

In addition to optimization in the base package, specific DB optimization was done.

Migrations

We finally decided, how migrations are supposed to be handled. We decided to reject experimental approaches with raw SQL, CLI and use corresponding packages - Cycle Migrations and Yii DB Migration (moreover the latter received the first stable version not a long time ago).

To continue the topic of removing coupling - each storage got its own independent set of migrations. Therefore, if you decide to store assignments only in the database - the tables for items will not be even created initially.

Also, the migration of existing data from Yii 2 was simplified by keeping original property names of entities, as much as possible.

All tables how have yii prefix by default for preventing potential collisions with user tables and easier identification. We plan to incorporate such approach to other DB dependent packages too.

In storages default table names were added for simplifying configuration.

Transactions

The decorator for Manager was added which guarantee data integrity during simultaneous changes in different storages, in particular during update of permissions and roles.

Full changes list can be found in CHANGELOG:

]]>
0
[news] Yii Validating Hydrator 2.0 Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/news/618/yii-validating-hydrator-2-0 https://www.yiiframework.com/news/618/yii-validating-hydrator-2-0 vjik vjik

Major release of yiisoft/hydrator-validator was tagged.

  • Throws LogicException on call ValidatedInputInterface::getValidatedInput() method when object is not validated.
]]>
0
[wiki] Integrating Yii3 packages into WordPress Mon, 04 Mar 2024 16:34:16 +0000 https://www.yiiframework.com/wiki/2579/integrating-yii3-packages-into-wordpress https://www.yiiframework.com/wiki/2579/integrating-yii3-packages-into-wordpress glpzzz glpzzz
  1. Source code available
  2. Goal
  3. Approach
  4. Conclusion

I was recently assigned with the task of integrating several extensive forms into a WordPress website. These forms comprised numerous fields, intricate validation rules, dynamic fields (one to many relationships) and even interdependencies, where employing PHP inheritance could mitigate code duplication.

Upon initial exploration, it became evident that the conventional approach for handling forms in WordPress typically involves either installing a plugin or manually embedding markup using the editor or custom page templates. Subsequently, one largely relies on the plugin's functionality to manage form submissions or resorts to custom coding.

Given that part of my task entailed logging data, interfacing with API endpoints, sending emails, and more, I opted to develop the functionality myself, rather than verifying if existing plugins supported these requirements.

Furthermore, considering the current landscape (as of March 2024) where most Yii 3 packages are deemed production-ready according to official sources, and being a long-time user of the Yii framework, I deemed it an opportune moment to explore and acquaint myself with these updates.

Source code available

You can explore the entire project and review the code by accessing it on Github.

Additionally, you can deploy it effortlessly using Docker by simply executing docker-compose up from the project's root directory. Check the Dockerfile for the WordPress setup and content generation which is done automatically.

Goal

My objective was to render and manage forms within a WordPress framework utilizing Yii3 packages. For demonstration purposes, I chose to implement a basic Rating Form, where the focus is solely on validating the data without executing further actions.

Approach

To proceed, let's start with a minimalistic classic theme as an example. I created a WordPress page named "The Rating Form" within the dashboard. Then, a file named page-the-rating-form.php is to be created within the theme's root folder to display this specific page.

This designated file serves as the blueprint for defining our form's markup.

Adding Yii3 Packages to the Project:

To harness Yii3's functionalities, we'll incorporate the following packages:

To begin, let's initialize a Composer project in the root of our theme by executing composer init. This process will generate a composer.json file. Subsequently, we'll proceed to include the Yii3 packages in our project.

composer require yiisoft/form-model:dev-master yiisoft/validator yiisoft/form:dev-master

and instruct the theme to load the composer autoload by adding the following line to the functions.php file:

require __DIR__ . '/vendor/autoload.php';
Create the form model

Following the execution of the composer init command, a src directory has been created in the root directory of the theme. We will now proceed to add our form model class within this directory.

Anticipating the expansion of the project, it's imperative to maintain organization. Thus, we shall create the directory src/Forms and place the RatingForm class inside it.

<?php

namespace Glpzzz\Yii3press\Forms;

use Yiisoft\FormModel\FormModel;

class RatingForm extends FormModel
{

	private ?string $name = null;
	private ?string $email = null;
	private ?int $rating = null;
	private ?string $comment = null;
	private string $action = 'the_rating_form';

	public function getPropertyLabels(): array
	{
		return [
			'name' => 'Name',
			'email' => 'Email',
			'rating' => 'Rating',
			'comment' => 'Comment',
		];
	}

}

Beyond the requisite fields for our rating use case, it's crucial to observe the action class attribute. This attribute is significant as it instructs WordPress on which theme hook should manage the form submission. Further elaboration on this will follow.

Adding Validation Rules to the Model:

Now, let's incorporate some validation rules into the model to ensure input integrity. Initially, we'll configure the class to implement the RulesProviderInterface. This enables the form package to access these rules and augment the HTML markup with native validation attributes.

class RatingForm extends FormModel implements RulesProviderInterface

Now we need to implement the getRules() method on the class.

public function getRules(): iterable
{
	return [
		'name' => [
			new Required(),
		],
		'email' => [
			new Required(),
			new Email(),
		],
		'rating' => [
			new Required(),
			new Integer(min: 0, max: 5),
		],
		'comment' => [
			new Length(min: 100),
		],
	];
}
Create the form markup

To generate the form markup, we require an instance of RatingForm to be passed to the template. In WordPress, the approach I've adopted involves creating a global variable (admittedly not the most elegant solution) prior to rendering the page.


$hydrator = new Hydrator(
	new CompositeTypeCaster(
		new NullTypeCaster(emptyString: true),
		new PhpNativeTypeCaster(),
		new HydratorTypeCaster(),
	)
);

add_filter('template_redirect', function () use ($hydrator) {
	// Get the queried object
	$queried_object = get_queried_object();

	// Check if it's a page
	if ($queried_object instanceof WP_Post && is_page()) {
		if ($queried_object->post_name === 'the-rating-form') {
			global $form;
			if ($form === null) {
				$form = $hydrator->create(RatingForm::class, []);
			}
		}
	}
});

It's worth noting that we've instantiated the Hydrator class outside any specific function, enabling us to reuse it for all necessary callbacks. With the RatingForm instance now available, we'll proceed to craft the markup for the form within the page-the-rating-form.php file.


<?php

use Glpzzz\Yii3press\Forms\RatingForm;
use Yiisoft\FormModel\Field;
use Yiisoft\Html\Html;

/** @var RatingForm $form */
global $form;

?>


<?php get_header(); ?>

<h1><?php the_title(); ?></h1>

<?php the_content(); ?>

<?= Html::form()
  ->post(esc_url(admin_url('admin-post.php')))
  ->open()
?>

<?= Field::hidden($form, 'action')->name('action') ?>
<?= Field::text($form, 'name') ?>
<?= Field::email($form, 'email') ?>
<?= Field::range($form, 'rating') ?>
<?= Field::textarea($form, 'comment') ?>

<?= Html::submitButton('Send') ?>

<?= "</form>" ?>

<?php get_footer(); ?>

In the markup generation of our form, we've leveraged a combination of Yii3's Html helpers and the Field class. Notable points include:

  • The form employs the POST method with the action specified as the admin-post.php WordPress endpoint.
  • To include the action value in the form submission, we utilized a hidden field named 'action'. We opted to rename the input to 'action' as the Field::hidden method generates field names in the format TheFormClassName[the_field_name], whereas we required it to be simply named 'action'.

This adjustment facilitates hooking into a theme function to handle the form request, as elucidated in the subsequent section.

Before delving further, let's capitalize on Yii's capabilities to enhance the form. Although we've already defined validation rules in the model for validating input post-submission, it's advantageous to validate input within the browser as well. While we could reiterate defining these validation rules directly on the input elements, Yii offers a streamlined approach. By incorporating the following code snippet into the functions.php file:

add_action('init', function () {
	ThemeContainer::initialize([
			'default' => [
				'enrichFromValidationRules' => true,
			]
		], 'default', new ValidationRulesEnricher()
	);
});

By implementing this code snippet, we activate the ValidationRulesEnricher for the default form theme. Upon activation, we'll notice that the form fields are now enriched with validation rules such as 'required', 'min', and ' max', aligning with the validation rules previously defined in the model class. This feature streamlines the process, saving us valuable time and minimizing the need for manual code composition. Indeed, this showcases some of the remarkable functionality offered by Yii3.

Process the POST request

When the form is submitted, it is directed to admin-post.php, an endpoint provided by WordPress. However, when dealing with multiple forms, distinguishing the processing of each becomes essential. This is where the inclusion of the action value in the POST request proves invaluable.

Take note of the initial two lines in the following code snippet: the naming convention for the hook is admin_post_<action_name>. Therefore, if a form has action = 'the-rating-form', the corresponding hook name will be admin_post_the_rating_form.

As for the inclusion of both admin_post_<action_name> and admin_post_nopriv_<action_name>, this is because WordPress allows for different handlers depending on whether the user is logged in or not. In our scenario, we require the same handler regardless of the user's authentication status.

add_action('admin_post_the_rating_form', fn() => handleForms($hydrator));
add_action('admin_post_nopriv_the_rating_form', fn() => handleForms($hydrator));

function handleForms(Hydrator $hydrator): void
{
  global $form;
  $form = $hydrator->create(RatingForm::class, $_POST['RatingForm']);
  $result = (new Yiisoft\Validator\Validator())->validate($form);

  if ($form->isValid()) {
    // handle the form
  }

  get_template_part('page-the-rating-form');
}

Returning to the Yii aspect: we instantiate and load the posted data into the form utilizing the hydrator. We then proceed to validate the data. If the validation passes successfully, we can proceed with the intended actions using the validated data. However, if validation fails, we re-render the form, populating it with the submitted data and any error messages generated during validation.

Conclusion

  • This was my first attempt at mixing Yii3 packages with a WordPress site. While I'm satisfied with the result, I think it can be improved, especially regarding the use of global variables. Since I'm not very experienced with WordPress, I'd appreciate any suggestions for improvement.
  • The Yii3 packages I used are ready for real-world use and offer the same quality and features as their older versions.
  • Now you can use these Yii packages independently. This means you can apply your Yii skills to any PHP project.
  • This project shows how we can enhance a WordPress site by tapping into the powerful features of Yii, while still keeping the simplicity of the CMS.

Originally posted on https://glpzzz.dev/2024/03/03/integrating-yii3-packages-into-wordpress.html

]]>
0
[news] Yii Data Response 2.1 Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/news/617/yii-data-response-2-1 https://www.yiiframework.com/news/617/yii-data-response-2-1 vjik vjik

Yii Data Response package was updated with the following enhancements and fixes:

  • added PlainTextDataResponseFormatter formatter and FormatDataResponseAsPlainText middleware;
  • added support for psr/http-message version ^2.0;
  • raised minimum PHP version to 8.1 and refactor code;
  • explicitly added transitive dependencies psr/http-factory and psr/http-server-handler.
]]>
0
[news] Yii Request Provider 1.0 Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/news/616/yii-request-provider-1-0 https://www.yiiframework.com/news/616/yii-request-provider-1-0 vjik vjik

First stable release of yiisoft/request-provider was tagged.

The package provides current PSR-7 request as a dependency.

When you need current request, get RequestProviderInterface as dependency and obtain the request from it:

use \Yiisoft\RequestProvider\RequestProviderInterface;

final class MyService
{
    public function __construct(
        private readonly RequestProviderInterface $requestProvider
    ) {    
    }
    
    public function doIt()
    {
        $request = $this->requestProvider->get();
        // ...
    }
}
]]>
0
[news] Yii Runner RoadRunner 3.0 Thu, 22 Feb 2024 13:31:13 +0000 https://www.yiiframework.com/news/615/yii-runner-roadrunner-3-0 https://www.yiiframework.com/news/615/yii-runner-roadrunner-3-0 vjik vjik

Major version of Yii Runner RoadRunner package was released.

In this release:

  • added runner for gRPC requests;
  • renamed RoadRunnerApplicationRunner to RoadRunnerHttpApplicationRunner;
  • increased minimum PHP version to 8.1;
  • added support for psr/http-message of ^2.0 version.
]]>
0
[news] Yii Validator 1.2 Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/news/614/yii-validator-1-2 https://www.yiiframework.com/news/614/yii-validator-1-2 vjik vjik

Yii Validator package was updated with the following enhancements and fixes:

  • added debug collector for yiisoft/yii-debug;
  • added $escape parameter to methods Result::getAttributeErrorMessagesIndexedByPath() and Result::getErrorMessagesIndexedByPath() that allow change or disable symbol which will be escaped in value path elements;
  • added OneOf rule;
  • minor refactoring of EmailHandler::validate() method;
  • added more specific psalm type for "skip on empty" callable;
  • $isAttributeMissing parameter of empty conditions (NeverEmpty, WhenEmpty, WhenMissing, WhenNull) is made optional;
  • disabled escaping of asterisk char in value path returned by Error::getValuePath(true).
]]>
0
[news] Yii Router 3.1 Tue, 20 Feb 2024 11:47:42 +0000 https://www.yiiframework.com/news/613/yii-router-3-1 https://www.yiiframework.com/news/613/yii-router-3-1 vjik vjik

Yii Router package was updated with the following enhancements:

  • added RouteArgument attribute for Yii Hydrator;
  • added support for psr/http-message version ^2.0.
]]>
0
[news] Yii Console 2.2 Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/news/612/yii-console-2-2 https://www.yiiframework.com/news/612/yii-console-2-2 vjik vjik

Version 2.2 of Yii Console package was tagged.

In this version allowed to use ErrorListiner without logger.

]]>
0
[extension] neoacevedo/yii2-auditing Wed, 28 Feb 2024 00:31:28 +0000 https://www.yiiframework.com/extension/neoacevedo/yii2-auditing https://www.yiiframework.com/extension/neoacevedo/yii2-auditing NestorAcevedo NestorAcevedo

Yii2 Auditing

  1. Instalación
  2. Uso
  3. Desplegando la información

Registra cambios de sus modelos ActiveRecord de Yii2.

Este paquete permite mantener un historial de cambios de los modelos proveyendo información sobre posibles discrepancias o anomalías en la información que puedan indicar actividades sospechosas. La información recibida y almacenada se puede posteriormente desplegar de diversas maneras.

Instalación

La forma preferida de instalar esta extensión es a través de composer.

Luego ejecute

php composer.phar require --prefer-dist neoacevedo/yii2-auditing "*"

o agregue

"neoacevedo/yii2-auditing": "*"

a la sección require de su archivo composer.json.

Uso

Una vez que la extensión está instalada, en el archivo de configuración de la consola de su aplicación, agregue en la zona migrationPath

...
'@vendor/neoacevedo/yii2-auditing/neoacevedo/auditing/migrations',
...

luego, agregue en el código de su modelo dentro del método behaviors:

public function behaviors()
{
    return [
        \neoacevedo\auditing\behaviors\AuditBehavior::class,
        ...
    ];
}

Desplegando la información

Puede desplegar la información como cualquier modelo que haya implementado dentro de su aplicación web.

Puede hacer uso de un controlador y una vista que use GridView para listar el historial. Por ejemplo, puede crear un controllador que se llame AuditingController y crear el método actionIndex como lo siguiente:

    /**
     * Lists all Auditing models.
     *
     * @return string
     */
    public function actionIndex()
    {
        $searchModel = new AuditingSearch();
        $dataProvider = $searchModel->search($this->request->queryParams);

        return $this->render('index', [
            'searchModel' => $searchModel,
            'dataProvider' => $dataProvider,
        ]);
    }

Para visualizar los datos, crear el método actionView:

    /**
     * Displays a single Auditing model.
     * @param int $id ID
     * @return string
     * @throws NotFoundHttpException if the model cannot be found
     */
    public function actionView($id)
    {
        return $this->render('view', [
            'model' => $this->findModel($id),
        ]);
    }

Dentro de la vista view puede agregar el GridView para listar el histórico:

...
    <?= GridView::widget([
        'dataProvider' => $dataProvider,
        'filterModel' => $searchModel,
        'columns' => [
            ['class' => 'yii\grid\SerialColumn'],
            'id',
            'user_id',
            'description',
            'event',
            'model',
            'attribute',
            'old_value',
            'new_value',
            'action',
            'ip',
            'created_at',
        ],
    ]); ?>
...
]]>
0
[news] Yii Hydrator 1.1 Thu, 04 Apr 2024 08:09:39 +0000 https://www.yiiframework.com/news/611/yii-hydrator-1-1 https://www.yiiframework.com/news/611/yii-hydrator-1-1 vjik vjik

Minor version of Yii Hydrator package is released.

  • Added NullTypeCaster.
]]>
0
[news] Yii CSRF 2.1 Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/news/610/yii-csrf-2-1 https://www.yiiframework.com/news/610/yii-csrf-2-1 vjik vjik

Minor verison of Yii CSRF package was released.

  • Added StubCsrfToken.
  • Explicitly added transitive dependencies yiisoft/strings, psr/http-server-handler and ext-hash.
]]>
0
[extension] luguohuakai/yii2-dm Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/luguohuakai/yii2-dm https://www.yiiframework.com/extension/luguohuakai/yii2-dm luguohuakai luguohuakai

Database extension for DM

  1. Installation
  2. Usage

A database extension for DM database

Installation

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist luguohuakai/yii2-dm "*"

or add

"luguohuakai/yii2-dm": "*"

to the require section of your composer.json file.

Usage

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

'components' => [
    'db' => [
        'class' => 'luguohuakai\db\dm\Connection',
        'dsn' => 'dm:host=localhost:xxx;schema=xxx',
        'username' => 'SYSDBA',
        'password' => 'SYSDBA',
    ]
]
]]>
0
[news] Yii Validating Hydrator 1.0 Fri, 02 Feb 2024 08:14:41 +0000 https://www.yiiframework.com/news/609/yii-validating-hydrator-1-0 https://www.yiiframework.com/news/609/yii-validating-hydrator-1-0 vjik vjik

First stable release of yiisoft/hydrator-validator was tagged.

The package provides a hydrator that also does validation, including raw data. It's useful when input data comes from a user, and you need to validate it and then put it into DTOs.

To use it, the object being validated must implement ValidatedInputInterface. You can use ValidatedInputTrait to easily create such object. The validation rules for raw values of the object are defined with Validate PHP attribute.

Example of object:

use Yiisoft\Hydrator\Validator\Attribute\Validate;
use Yiisoft\Hydrator\Validator\ValidatedInputInterface;
use Yiisoft\Hydrator\Validator\ValidatedInputTrait;
use Yiisoft\Validator\Rule\Email;
use Yiisoft\Validator\Rule\Required;

final class InputDto implements ValidatedInputInterface 
{
    use ValidatedInputTrait;
    
    #[Email]
    private string $email;
    
    #[Validate(new Required())]
    private string $name;
}

Validation result could be obtained via its getValidationResult() method.

Validating hydrator usage example:

use Yiisoft\Hydrator\HydratorInterface;
use Yiisoft\Hydrator\Validator\ValidatingHydrator;

public function actionEdit(RequestInterface $request, ValidatingHydrator $hydrator): ResponseInterface
{
    $data = $request->getParsedBody();    
    $inputDto = $hydrator->create(InputDto::class, $data);
    
    if (!$inputDto->getValidationResult()->isValid()) {
        // Validation didn't pass :(
    }
    
    // Everything is fine. You can use $inputDto now.    
}
]]>
0
[extension] rashedalkhatib/yii2-datatables Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/rashedalkhatib/yii2-datatables https://www.yiiframework.com/extension/rashedalkhatib/yii2-datatables RashedAlkhatib RashedAlkhatib

DataTable Widget

  1. Overview
  2. installation
  3. Usage Example (PHP Widget)
  4. Usage Example (Java Script)
  5. Usage API Side
  6. Feel Free to contact me : alkhatib.rashed@gmail.com

Overview

The DataTable widget is used to create interactive and dynamic data tables. The provided JavaScript code demonstrates how to initialize DataTable with server-side processing, custom data handling, and column rendering and with full serverside Export .

installation

in your Yii2 application :
  • Run : `composer require rashedalkhatib/yii2-datatables:1.0.0`
  • go to your `../frontend/assets/AppAsset.php`
    • add rashedalkhatib\datatables\DataTableAsset your $depends array
    • Ex:
              public $depends = [
                'yii\web\YiiAsset',
                'yii\bootstrap\BootstrapAsset',
                'yii\bootstrap\BootstrapPluginAsset',
                'rashedalkhatib\datatables\DataTableAsset'
        ];
      

Usage Example (PHP Widget)

- application side
$searchFormSelector = '#searchForm';
$ajaxUrl = Url::to(['api/yourEndPoint']); // Adjust the URL based on your routes

// Define your DataTable columns
$columns = [
    [
        'title' => 'ID',
        'data' => 'id',
        'visible' => true,
        'render' => new JsExpression('function(data, type, row) {
            return "demo";
        }'),
    ],
];

// Configure other DataTable parameters
$processing = true;
$serverSide = true;
$pageLength = 10;
$dom = 'Btip';
$buttons = [
    [
        'extend' => 'excel',
        'text' => 'Excel',
        'titleAttr' => 'Excel',
        'action' => new JsExpression('exportAll') // this is required 
    ],
];

// Configure Ajax settings
$ajaxConfig = [
    'url' => $ajaxUrl,
    'bdestroy' => true,
    'type' => 'POST',
    'data' => new JsExpression('function(d) {
            var searchForm = $('body').find('#searchForm').serializeArray();
            
            searchForm[searchForm.length] = { name: 'YourModel[page]', value: d.start }; // required
            searchForm[searchForm.length] = { name: 'YourModel[length]', value: d.length }; // required
            searchForm[searchForm.length] = { name: 'YourModel[draw]', value: d.draw }; // required
            
            var order = {
                'attribute': d.columns[d.order[0]['column']]['data'],
                'dir': d.order[0]['dir']
            }; // required
            
            searchForm[searchForm.length] = { name: 'YourModel[order]', value: JSON.stringify(order) };
            return searchForm;
    }'),
    'dataSrc' => new JsExpression('function(d) {
        var searchForm = $("' . $searchFormSelector . '").serializeArray();
        if (d.validation) {
            searchForm.yiiActiveForm("updateMessages", d.validation, true);
            return [];
        }
        return d.data;
    }'),
];

// Use the DataTableWidget with configured parameters
DataTable::widget([
    'id' => 'yourDataTable',
    'ajaxConfig' => $ajaxConfig,
    'columns' => $columns,
    'processing' => $processing,
    'serverSide' => $serverSide,
    'pageLength' => $pageLength,
    'dom' => $dom,
    'buttons' => $buttons,
]);

// The HTML container for your DataTable
echo '<form id="searchForm">// your inputs </form>';
echo '<table id="yourDataTable" class="display"></table>';

Usage Example (Java Script)

- application side
front end
<form id="searchForm">
// your inputs 
</form>

<table id="yourDataTable" class="display" style="width:100%">


</table>
var arrayToExport = [0,1];
$('#yourDataTable').DataTable({
    "ajax": {
        // Server-side processing configuration
        "url": "../api/yourEndPoint",
        "bdestroy": true, // this allows you to re init the dataTabel and destory it 
        "type": "POST", // request method
        "data": function (d) { // this represent the data you are sending with your ajax request
            // Custom function for sending additional parameters to the server
            var searchForm = $('body').find('#searchForm').serializeArray();
            
            searchForm[searchForm.length] = { name: "YourModel[page]", value: d.start }; // required
            searchForm[searchForm.length] = { name: "YourModel[length]", value: d.length }; // required
            searchForm[searchForm.length] = { name: "YourModel[draw]", value: d.draw }; // required
            
            var order = {
                'attribute': d.columns[d.order[0]['column']]['data'],
                'dir': d.order[0]['dir']
            }; // required
            
            searchForm[searchForm.length] = { name: "YourModel[order]", value: JSON.stringify(order) };
            return searchForm;
        },
        dataSrc: function (d) {
            // Custom function to handle the response data
            // EX:
            var searchForm = $('body').find('#searchForm').serializeArray();
            if (d.validation) {
                searchForm.yiiActiveForm('updateMessages', d.validation, true);
                return [];
            }
            return d.data;
        }
    },
    "columns": [{
        // Column configurations
        "title": "ID",
        "data": "id",
        "visible": true // visablity of column 
    },
    // ... (other columns)
    {
        "title": "Actions",
        "data": "id",
        "visible": actionCol,
        "render": function (data, type, row) {
            // Custom rendering function for the "Actions" column
            return '<a class="showSomething" data-id="' + row.id + '">View</a>';
        }
    }],
    processing: true,
    serverSide: true,
    "pageLength": 10,
    dom: "Btip",
    "buttons": [{
        // "Excel" button configuration
        "extend": 'excel',
        exportOptions: {
            columns: arrayToExport
        },
        "text": '  Excel',
        "titleAttr": 'Excel',
        "action": exportAll // newexportaction this action is to allow you exporting with server side without rendaring data 
    }],
});
application back end
these params should be sent to the API
// in your HTTP request you want to include these params 
   $_postData = [
   'page' => $this->page == 0 ? 0 : $this->page / $this->length, // this equation is required to handle Yii2 Data provider Logic
   'limit' => $this->length,
   'export' => $this->export,
   'order' => $this->order,
   // add your custom params .....
   ];
these params should be returned to the Datatable endpoint
return $this->asJson(
                    [
                        'data' => $_scoreData->data,
                        'draw' => $_scoreSearchForm->draw,
                        'recordsTotal' => $_scoreData->count, 
                        'recordsFiltered' => $_scoreData->count
                ]);

Usage API Side

yourEndPoint action
    public function actionYourEndPoint()
    {

        $searchModel = new SearchModel();

        $dataProvider = $searchModel->search(Yii::$app->request->get());
        return $this->asJson(
            array(
                'data' => $dataProvider['data'],
                'count' => $dataProvider['count']
            )
        );

    }
search function
    public function search($params)
    {
        $this->load($params, ''); // load your values into the model
        $query = Data::find(); // Data model is your link to the database

        $_order = json_decode($this->order);
        if ($this->export == 'true') {
            $dataProvider = new ActiveDataProvider([
                'query' => $query
                // we removed the page and pageSize keys to allow all data to be exported
            ]);
        } else {
            $_orderType = SORT_ASC;
            if ($_order->dir == 'desc')
                $_orderType = SORT_DESC;
            $query->orderBy([$_order->attribute => $_orderType]);
            $dataProvider = new ActiveDataProvider([
                'query' => $query,
                'pagination' => [
                    'pageSize' => $this->limit,
                    'page' => $this->page,
                ],
            ]);
        }


        return array(
            'data' => $dataProvider->getModels(),
            'count' => $dataProvider->getTotalCount()
        );
    }

Feel Free to contact me : alkhatib.rashed@gmail.com

]]>
0
[extension] eluhr/yii2-json-attribute-behavior Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/eluhr/yii2-json-attribute-behavior https://www.yiiframework.com/extension/eluhr/yii2-json-attribute-behavior eluhr eluhr

Yii2 JSON Attribute Behavior

  1. Installation
  2. Usage
  3. Testing

This behavior automatically decodes attributes from JSON to arrays before validation, handling errors and re-encoding if validation fails. With this a "real" json string can be further processed.

CI Workflow

Installation

The preferred way to install this extension is through composer.

Either run

composer require --prefer-dist eluhr/yii2-json-attribute-behavior "*"

or add

"eluhr/yii2-json-attribute-behavior": "*"

to the require section of your composer.json file.

Usage

In a yii\base\Model or a derivation thereof, the behavior can be used as follows:

public function behaviors(): array
{
    $behaviors = parent::behaviors();
    $behaviors['json-attribute'] = [
        'class' => eluhr\jsonAttributeBehavior\JsonAttributeBehavior::class,
        'attributes' => [
            'data_json'
        ]
    ];
    return $behaviors;
}

By using this behavior it does not matter if the attribute is a string or an array. The behavior will always ensure, that the attribute is an array before saving the data to the database and yii will handle the rest.

This behavior supports i18n. By adding the json-attribute-behavior category in your config you can overwrite the default error messages.

Testing

After installing dependencies via composer you can run the tests with:

make test
]]>
0
[extension] ip2location/ip2proxy-yii Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/ip2location/ip2proxy-yii https://www.yiiframework.com/extension/ip2location/ip2proxy-yii hexasoft hexasoft

IP2Proxy Yii extension

  1. INSTALLATION
  2. USAGE
  3. DEPENDENCIES
  4. SUPPORT

IP2Proxy Yii extension enables the user to query an IP address if it was being used as open proxy, web proxy, VPN anonymizer and TOR exit nodes, search engine robots, data center ranges, residential proxies, consumer privacy networks, and enterprise private networks. It lookup the proxy IP address from IP2Proxy BIN Data file or web service. Developers can use the API to query all IP2Proxy BIN databases or web service for applications written using Yii.

INSTALLATION

For Yii2

  1. Run the command: php composer.phar require ip2location/ip2proxy-yii to download the plugin into the Yii2 framework.
  2. Download latest IP2Proxy BIN database
  3. Unzip and copy the BIN file into the Yii2 framework.

Note: The BIN database refers to the binary file ended with .BIN extension, but not the CSV format. Please select the right package for download.

USAGE

use IP2ProxyYii\IP2Proxy_Yii;

// (required) Define IP2Proxy database path.
define('IP2PROXY_DATABASE', '/path/to/ip2proxy/database');

// (required) Define IP2Location.io API key.
define('IP2LOCATION_IO_API_KEY', 'your_api_key');

// (optional) Define Translation information. Refer to https://www.ip2location.io/ip2location-documentation for available languages.
define('IP2LOCATION_IO_LANGUAGE', 'en');

$IP2Proxy = new IP2Proxy_Yii();

$record = $IP2Proxy->get('1.0.241.135');
echo 'Result from BIN Database:<br>';
echo '<p><strong>IP Address: </strong>' . $record['ipAddress'] . '</p>';
echo '<p><strong>IP Number: </strong>' . $record['ipNumber'] . '</p>';
echo '<p><strong>IP Version: </strong>' . $record['ipVersion'] . '</p>';
echo '<p><strong>Country Code: </strong>' . $record['countryCode'] . '</p>';
echo '<p><strong>Country: </strong>' . $record['countryName'] . '</p>';
echo '<p><strong>State: </strong>' . $record['regionName'] . '</p>';
echo '<p><strong>City: </strong>' . $record['cityName'] . '</p>';
echo '<p><strong>Proxy Type: </strong>' . $record['proxyType'] . '</p>';
echo '<p><strong>Is Proxy: </strong>' . $record['isProxy'] . '</p>';
echo '<p><strong>ISP: </strong>' . $record['isp'] . '</p>';
echo '<p><strong>Domain: </strong>' . $record['domain'] . '</p>';
echo '<p><strong>Usage Type: </strong>' . $record['usageType'] . '</p>';
echo '<p><strong>ASN: </strong>' . $record['asn'] . '</p>';
echo '<p><strong>AS: </strong>' . $record['as'] . '</p>';
echo '<p><strong>Last Seen: </strong>' . $record['lastSeen'] . '</p>';
echo '<p><strong>Threat: </strong>' . $record['threat'] . '</p>';
echo '<p><strong>Provider: </strong>' . $record['provider'] . '</p>';

$record = $IP2Proxy->getWebService('1.0.241.135');
echo 'Result from Web service:<br>';
echo '<pre>';
print_r ($record);
echo '</pre>';

DEPENDENCIES

This library requires IP2Proxy BIN or IP2Proxy API key data file to function. You may download the BIN data file at

You can also sign up for IP2Location.io IP Geolocation API to get one free API key.

SUPPORT

Email: support@ip2location.com

Website: https://www.ip2location.com

]]>
0
[extension] ip2location/ip2location-yii Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/ip2location/ip2location-yii https://www.yiiframework.com/extension/ip2location/ip2location-yii hexasoft hexasoft

IP2Location Yii extension

  1. INSTALLATION
  2. USAGE
  3. DEPENDENCIES
  4. SUPPORT

IP2Location Yii extension enables the user to find the country, region, city, coordinates, zip code, time zone, ISP, domain name, connection type, area code, weather, MCC, MNC, mobile brand name, elevation, usage type, IP address type and IAB advertising category from IP address using IP2Location database. It has been optimized for speed and memory utilization. Developers can use the API to query all IP2Location BIN databases or web service for applications written using Yii

INSTALLATION

For Yii2

  1. Run the command: composer require ip2location/ip2location-yii to download the extension into the Yii2 framework.
  2. Download latest IP2Location BIN database
  3. Unzip and copy the BIN file into the Yii2 framework.

Note: The BIN database refers to the binary file ended with .BIN extension, but not the CSV format. Please select the right package for download.

USAGE

use IP2LocationYii\IP2Location_Yii;

// (required) Define IP2Location database path.
define('IP2LOCATION_DATABASE', '/path/to/ip2location/database');

// (required) Define IP2Location.io API key.
define('IP2LOCATION_IO_API_KEY', 'your_api_key');

// (optional) Define Translation information. Refer to https://www.ip2location.io/ip2location-documentation for available languages.
define('IP2LOCATION_IO_LANGUAGE', 'en');

// (optional) Define Translation information. Refer to https://www.ip2location.com/web-service/ip2location for available languages.
define('IP2LOCATION_LANGUAGE', 'en');

$IP2Location = new IP2Location_Yii();

$record = $IP2Location->get('8.8.8.8');
echo 'Result from BIN Database:<br>';
echo 'IP Address: ' . $record['ipAddress'] . '<br>';
echo 'IP Number: ' . $record['ipNumber'] . '<br>';
echo 'ISO Country Code: ' . $record['countryCode'] . '<br>';
echo 'Country Name: ' . $record['countryName'] . '<br>';
echo 'Region Name: ' . $record['regionName'] . '<br>';
echo 'City Name: ' . $record['cityName'] . '<br>';
echo 'Latitude: ' . $record['latitude'] . '<br>';
echo 'Longitude: ' . $record['longitude'] . '<br>';
echo 'ZIP Code: ' . $record['zipCode'] . '<br>';
echo 'Time Zone: ' . $record['timeZone'] . '<br>';
echo 'ISP Name: ' . $record['isp'] . '<br>';
echo 'Domain Name: ' . $record['domainName'] . '<br>';
echo 'Net Speed: ' . $record['netSpeed'] . '<br>';
echo 'IDD Code: ' . $record['iddCode'] . '<br>';
echo 'Area Code: ' . $record['areaCode'] . '<br>';
echo 'Weather Station Code: ' . $record['weatherStationCode'] . '<br>';
echo 'Weather Station Name: ' . $record['weatherStationName'] . '<br>';
echo 'MCC: ' . $record['mcc'] . '<br>';
echo 'MNC: ' . $record['mnc'] . '<br>';
echo 'Mobile Carrier Name: ' . $record['mobileCarrierName'] . '<br>';
echo 'Elevation: ' . $record['elevation'] . '<br>';
echo 'Usage Type: ' . $record['usageType'] . '<br>';
echo 'Address Type: ' . $record['addressType'] . '<br>';
echo 'Category: ' . $record['category'] . '<br>';

$record = $IP2Location->getWebService('8.8.8.8');
echo 'Result from Web service:<br>';
echo '<pre>';
print_r ($record);
echo '</pre>';

DEPENDENCIES

This library requires IP2Location BIN data file or IP2Location API key to function. You may download the BIN data file at

You can also sign up for IP2Location.io IP Geolocation API to get one free API key.

SUPPORT

Email: support@ip2location.com

Website: https://www.ip2location.com

]]>
0
[wiki] Create Bootstrap5 based Image carousel with thumbnails Mon, 04 Dec 2023 13:03:38 +0000 https://www.yiiframework.com/wiki/2578/create-bootstrap5-based-image-carousel-with-thumbnails https://www.yiiframework.com/wiki/2578/create-bootstrap5-based-image-carousel-with-thumbnails pravi pravi

Use the following css styles for carousel to work as expected.


  .product_img_slide {
    padding: 100px 0 0 0;
  }

  .product_img_slide > .carousel-inner > .carousel-item {
    overflow: hidden;
    max-height: 650px;
  }

  .carousel-inner {
    position: relative;
    width: 100%;
  }

  .product_img_slide > .carousel-indicators {
    top: 0;
    left: 0;
    right: 0;
    width: 100%;
    bottom: auto;
    margin: auto;
    font-size: 0;
    cursor: e-resize;
    /* overflow-x: auto; */
    text-align: left;
    padding: 10px 5px;
    /*  overflow-y: hidden;*/
    white-space: nowrap;
    position: absolute;
  }

  .product_img_slide > .carousel-indicators li {
    padding: 0;
    width: 76px;
    height: 76px;
    margin: 0 5px;
    text-indent: 0;
    cursor: pointer;
    background: transparent;
    border: 3px solid #333331;
    -webkit-border-radius: 0;
    border-radius: 0;
    -webkit-transition: all 0.7s cubic-bezier(0.22, 0.81, 0.01, 0.99);
    transition: all 1s cubic-bezier(0.22, 0.81, 0.01, 0.99);
  }

  .product_img_slide > .carousel-indicators .active {
    width: 76px;
    border: 0;
    height: 76px;
    margin: 0 5px;
    background: transparent;
    border: 3px solid #c13c3d;
  }

  .product_img_slide > .carousel-indicators > li > img {
    display: block;
    /*width:114px;*/
    height: 76px;
  }

  .product_img_slide .carousel-inner > .carousel-item > a > img, .carousel-inner > .carousel-item > img, .img-responsive, .thumbnail a > img, .thumbnail > img {
    display: block;
    max-width: 100%;
    line-height: 1;
    margin: auto;
  }

  .product_img_slide .carousel-control-prev {
    top: 58%;
    /*left: auto;*/
    right: 76px;
    opacity: 1;
    width: 50px;
    bottom: auto;
    height: 50px;
    font-size: 50px;
    cursor: pointer;
    font-weight: 700;
    overflow: hidden;
    line-height: 50px;
    text-shadow: none;
    text-align: center;
    position: absolute;
    background: transparent;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.6);
    -webkit-box-shadow: none;
    box-shadow: none;
    -webkit-border-radius: 0;
    border-radius: 0;
    -webkit-transition: all 0.6s cubic-bezier(0.22, 0.81, 0.01, 0.99);
    transition: all 0.6s cubic-bezier(0.22, 0.81, 0.01, 0.99);
  }

  .product_img_slide .carousel-control-next {
    top: 58%;
    left: auto;
    right: 25px;
    opacity: 1;
    width: 50px;
    bottom: auto;
    height: 50px;
    font-size: 50px;
    cursor: pointer;
    font-weight: 700;
    overflow: hidden;
    line-height: 50px;
    text-shadow: none;
    text-align: center;
    position: absolute;
    background: transparent;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.6);
    -webkit-box-shadow: none;
    box-shadow: none;
    -webkit-border-radius: 0;
    border-radius: 0;
    -webkit-transition: all 0.6s cubic-bezier(0.22, 0.81, 0.01, 0.99);
    transition: all 0.6s cubic-bezier(0.22, 0.81, 0.01, 0.99);
  }

  .product_img_slide .carousel-control-next:hover, .product_img_slide .carousel-control-prev:hover {
    color: #c13c3d;
    background: transparent;
  }

Here is a Corousel widget that is an extension of yii\bootstrap5\Carousel, to show image thumbnails as indicators for the carousel.

Here is the widget code.

<?php
namespace app\widgets;
use Yii;
use yii\bootstrap5\Html;

class Carousel extends \yii\bootstrap5\Carousel
{
    public $thumbnails = [];

    public function init()
    {
        parent::init();     
        Html::addCssClass($this->options, ['data-bs-ride' => 'carousel']);
        if ($this->crossfade) {
            Html::addCssClass($this->options, ['animation' => 'carousel-fade']);
        }
    }

    public function renderIndicators(): string
    {
        if ($this->showIndicators === false){
            return '';
        }
        $indicators = [];
        for ($i = 0, $count = count($this->items); $i < $count; $i++){
            $options = [
                'data' => [
                    'bs-target' => '#' . $this->options['id'],
                    'bs-slide-to' => $i
                ],
                'type' => 'button',
                'thumb' => $this->thumbnails[$i]['thumb']
            ];
            if ($i === 0){
                Html::addCssClass($options, ['activate' => 'active']);
                $options['aria']['current'] = 'true';
            }       

             $indicators[] = Html::tag('li',Html::img($options['thumb']), $options);
        }
        return Html::tag('ol', implode("\n", $indicators), ['class' => ['carousel-indicators']]);
    } }

You can use the above widget in your view file as below:

    <?php  
$indicators = [
   '0' =>[ 'thumb' => "https://placehold.co/150X150?text=A"],
   '1' => ['thumb' => 'https://placehold.co/150X150?text=B'],
   '2' => [ 'thumb' => 'https://placehold.co/150X150?text=C']
];
$items = [
    [ 'content' =>Html::img('https://live.staticflickr.com/8333/8417172316_c44629715e_w.jpg')],
    [ 'content' =>Html::img('https://live.staticflickr.com/3812/9428789546_3a6ba98c49_w.jpg')],
    [ 'content' =>Html::img('https://live.staticflickr.com/8514/8468174902_a8b505a063_w.jpg')]   
];

echo Carousel::widget([
    'items' => 
        $items,
     'thumbnails'  => $indicators,
     'options' => [       
          'data-interval' => 3, 'data-bs-ride' => 'scroll','class' => 'carousel product_img_slide',
      ],

]);
]]>
0
[wiki] How to add a DropDown Language Picker (i18n) to the Menu Sat, 16 Dec 2023 15:42:40 +0000 https://www.yiiframework.com/wiki/2577/how-to-add-a-dropdown-language-picker-i18n-to-the-menu https://www.yiiframework.com/wiki/2577/how-to-add-a-dropdown-language-picker-i18n-to-the-menu JQL JQL

How To Add Internationalisation to the NavBar Menu in Yii2

  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.

Screenshot_i18n_s.png

The Github repository also contains the language flags, some country flags, a list of languages codes and their language names and a list of the languages Yii recognises "out of the box". A video will be posted on YouTube soon.

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 ISO 639.1 in lowercase (with optional Country code ISO 3166 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 Language 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 /web/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 Language 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 /web/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:

i18ntutorial on Github

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
[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
[extension] nicksdr/nkchartjs Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/nicksdr/nkchartjs https://www.yiiframework.com/extension/nicksdr/nkchartjs CarlosQS CarlosQS

nkchartjs

]]>
0
[extension] exocet/yii2-upload-file-behavior Fri, 19 Apr 2024 11:44:45 +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
[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. 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\MaterialAsset::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 assets
    'exocet\bootstrap5md\MaterialAsset',
    
    // include Fontawesome icons (optional)
    'exocet\bootstrap5md\FontawesomeAsset',

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

In order for it to work properly, the files must be patched to accept the settings in the same way as the original Bootstrap does. So we have to add this in our composer.json so that it is always done automatically. `json

"scripts": {
    "post-install-cmd": [
        "@composer run-script post-install-cmd --working-dir=vendor/exocet/yii2-bootstrap-material-design"
    ],
    "post-update-cmd": [
        "@composer run-script post-update-cmd --working-dir=vendor/exocet/yii2-bootstrap-material-design"
    ]
}

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


```php
// @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
[extension] ssiva/yii2-mpesa-sdk Fri, 19 Apr 2024 11:44:45 +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 Fri, 19 Apr 2024 11:44:45 +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
[extension] ishukrullo/yii2-errorsender Fri, 19 Apr 2024 11:44:45 +0000 https://www.yiiframework.com/extension/ishukrullo/yii2-errorsender https://www.yiiframework.com/extension/ishukrullo/yii2-errorsender iShukrullo iShukrullo ]]> 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 Fri, 19 Apr 2024 11:44:45 +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 Fri, 19 Apr 2024 11:44:45 +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
[wiki] How to Create and Use Validator Using Regular expressions Fri, 19 Apr 2024 11:44:45 +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. Fri, 19 Apr 2024 11:44:45 +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 Fri, 19 Apr 2024 11:44:45 +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 Fri, 19 Apr 2024 11:44:45 +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 Fri, 19 Apr 2024 11:44:45 +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 Fri, 19 Apr 2024 11:44:45 +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. Fri, 19 Apr 2024 11:44:45 +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