Live News for Yii Framework News, fresh extensions and wiki articles about Yii framework. Sat, 10 Jan 2026 08:37:29 +0000 Zend_Feed_Writer 2 (http://framework.zend.com) https://www.yiiframework.com/ [news] Yii View Twig Renderer 3.0 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/779/yii-view-twig-renderer-3-0 https://www.yiiframework.com/news/779/yii-view-twig-renderer-3-0 vjik vjik

Yii Framework Twig Extension 3.0.0

Yii View Twig Renderer version 3.0.0 was released. In this version:

  • Rename ViewRenderer to more understandable TwigTemplateRenderer
  • Remove YiiTwigExtension
  • Update version of yiisoft/view to ^11|^12
  • Change PHP constraint in composer.json to 8.1 - 8.5
  • Raise the minimum PHP version to ^8.1
]]>
0
[extension] layanan-halo-bca Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/layanan-halo-bca https://www.yiiframework.com/extension/layanan-halo-bca halo halo

HaloBCA melalui 628131500646 WhatsApp dapat diakses 24 jam, memungkinkan Anda mendapatkan bantuan dan informasi...

]]>
0
[news] Queue extension 2.3.8 released Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/778/queue-extension-2-3-8-released https://www.yiiframework.com/news/778/queue-extension-2-3-8-released samdark samdark

Version 2.3.8 of Queue extension for Yii 2 was released.

This release fixes problems with SQS and Redis, prevents multiple execution of aborted jobs, and adds PHP 8.4 compatibility. Environment variables are now passed to sub-processes.

See the CHANGELOG for details.

]]>
0
[extension] codechap/yii2-ai-boost Fri, 02 Jan 2026 20:01:22 +0000 https://www.yiiframework.com/extension/codechap/yii2-ai-boost https://www.yiiframework.com/extension/codechap/yii2-ai-boost codeChap codeChap ]]> 0 [news] Yii3 is released Wed, 31 Dec 2025 11:41:38 +0000 https://www.yiiframework.com/news/777/yii3-is-released https://www.yiiframework.com/news/777/yii3-is-released samdark samdark

It happened! Yii3 is officially released after years of intensive development and polishing.

Yii was always following the same principles of being performant, flexible but with good defaults, practice-oriented, simple, explicit, and consistent. Yii3 is not an exception.

While Yii 1.1 and Yii 2.0 are good frameworks, Yii3 was born to improve things even more, addressing the following drawbacks:

  1. Yii 2.0 had a closed ecosystem with difficulties configuring general PHP packages.
  2. Some magic and implicitness. Non-standard PHP behavior for objects introduced in Yii 2.0.
  3. PHP standards compatibility and modern PHP usage were not in place due to backwards compatibility constraints.
  4. Some anti-patterns, such as the service locator being available out of the box. That was affecting overall projects' testability and maintainability in the long run.

In the following we will summarize some of the highlights of this long-awaited release. You may check out the Getting Started section if you want to rush to try it out first.

Independent packages

Compared to Yii 1.1 and Yii 2.0 which were monolithic frameworks, Yii3 is a package ecosystem with more than 130 official packages. These packages could be used in any PHP code or as a whole framework relatively similar to Yii 2.0 or Yii 1.1. Application templates help a lot in this case.

Application templates

There are three application templates available out of the box:

  • Web — for classic server-rendered applications.
  • API — for APIs.
  • Console — for console-only tools and background workers.

Unlike Yii 2.0, these are very minimalistic, so you start with basic things such as routing, configuration, DI container, or environment already wired together for you. But, at the same time, additional functionality, such as connecting to a database, isn't in templates by default. Install only what you need—no bloat, just solutions.

Ability to use any packages

Instead of focusing on Yii-specific extensions, as it was with Yii 2.0, we've made the framework to work well with any PHP packages available at Packagist, be they PSR-compatible ones, Symfony packages, or generic PHP code. The container is able to configure any of these.

Yii3 embraces the entire PHP ecosystem instead of reinventing the wheel. It integrates seamlessly with any PHP library. No vendor lock-in. No proprietary APIs. Just modern PHP standards enabling you to leverage the entire ecosystem's innovation.

First-class DI container

The central part of the framework is its dependency injection container. It brings all the individual packages together, automatically resolving dependencies and helping to obtain classes requested. Usual usage is very simple and explicit:

return [
   // Interface to class mapping
   EngineInterface::class => EngineMarkOne::class,
   
   // Detailed configuration
   MyServiceInterface::class => [
       'class' => MyService::class,
       '__construct()' => [42],
       'setDiscount()' => [10],
   ],

   // Factory closures
   'api' => static fn(ApiConfig $config) => new ApiClient($config),
];

// Dependencies are automatically injected based on type hints
final readonly class MyController
{
   public function __construct(
       private CacheInterface $cache
   ) {}
}

Another advantage of the container is that it is very fast and works at runtime. Your configuration is what is actually executed, so there is no magic, and you can even step-debug it with XDebug if needed.

Configuration

Following the traditional way of configuring things with Yii 2.0, we've extracted what was included into an advanced application supporting multiple environments, config overrides, etc. The configuration in Yii3 is very powerful and could be configured to follow basically any layout of the project if needed. By default, application templates are supplying configuration that is divided into web (or API) and console, also having some common shared configs. The configuration is divided into two main parts there: DI container configuration, which maps configured classes to interfaces, and parameters, which are used for configuring these classes.

'yiisoft/view' => [
    'basePath' => null,
    'parameters' => [
        'assetManager' => Reference::to(AssetManager::class),
        'applicationParams' => Reference::to(ApplicationParams::class),
        'aliases' => Reference::to(Aliases::class),
        'urlGenerator' => Reference::to(UrlGeneratorInterface::class),
        'currentRoute' => Reference::to(CurrentRoute::class),
    ],
],

'yiisoft/yii-view-renderer' => [
    'viewPath' => null,
    'layout' => '@src/Web/Shared/Layout/Main/layout.php',
    'injections' => [
        Reference::to(CsrfViewInjection::class),
    ],
],

Security

Similar to Yii 1.1 and Yii 2.0, we take security seriously. It was considered when implementing every package, such as the presentation layer and database abstraction.

As for features:

There are also helpers for common security-related actions, such as the CSRF-protection package.

Documentation contains a dedicated guide page with established security response processes.

Databases

Yii 2.0 had an exceptional database abstraction layer. At first, we extracted it into a separate package and then reimagined and improved it. It resulted in yiisoft/db, which is database access, schema management, and a powerful query builder. Extra tools and storage, such as migrations, database caching, etc., were built on top of DB as well as a more explicit but still rapid Active Record implementation.

$posts = $connection
   ->select(['id', 'title', 'created_at'])
   ->from('{{%posts}}')
   ->where(['status' => 'published'])
   ->andWhere(['>', 'views', 1000])
   ->orderBy(['created_at' => SORT_DESC])
   ->limit(10)
   ->all();

Other than our own database layer, you can use Cycle ORM or Doctrine, make queries using PDO or native drivers. Whatever you prefer. Yii3 does not force you to stick to a single implementation.

Data abstraction

Powerful data abstraction yiisoft/data, and widgets for it, such as grid view, are there and are perfect for creating powerful universal admin panels. The abstraction includes data formatting, pagination, sorting, and an ability to use any data source with any action/presentation and vice versa.

Caching 

Caching got even more advanced than in Yii 2.0. First of all, out of the box there's a cache stampede protection. Second, cache drivers are PSR-16 compatible. That means you can use any PSR-16 implementation for a very specific storage, and it will work right away.

The following backends were implemented by our team already:

  • APCu
  • DB
  • Files
  • Memcached
  • Redis

Standards first

Similar to caching, the framework leverages PSR interfaces for more:

  • Logging so you can either stick to Yii's logger or replace it with Monolog if you prefer it.
  • Middleware so you can bring endless solutions from Packagist, such as CORS handling, and use these without any changes to the code.
  • Request-response abstraction that allows optimal testing and worker mode.
  • Even a DI container could be either replaced or combined with another PSR-compliant one.

Standards-first architecture ensures your code stays portable and future-proof while maintaining full access to modern PHP tooling.

Worker mode

In traditional PHP servers, framework initialization and database connection are done for every request, which is not great for performance. Yii3 may run in worker mode together with RoadRunner, Swoole, or FrankenPHP. In this mode it initializes once and then serves many request-response cycles, which results in drastically lower response times.

While this mode is great, the state is largely shared between multiple request-response processing, so developers should take care of isolating the state (or not storing it at all) and potential memory leaks. Yii3 helps with it a lot: all the packages were designed either to not use any state or to reset it properly on every start of the request.

Everything for classic web applications

Not every project should be an API with a client so everything for classic server-rendered web applications is available: Widgets, Views with template engines support such as Twig, Forms, asset management, HTML helpers, etc.

Additionally, there are ready-to-use widgets for Bootstrap 5 and Bulma.

A toolset for building APIs

Yii3 provides many tools that help build APIs:

We plan to add more in the future but the current set is already enough for building powerful APIs.

HTTP layer and networking

Yii3 works with the HTTP layer on a more precise level than previous versions of the framework and follows PSR interfaces. On top of these relatively low-level interfaces, there are handy abstractions:

Additionally, network utilities are there to help with the IP protocol. 

User input and validator

Yii3 has a powerful validator powered by attributes, the ability to fill forms with data from HTTP (input-http) or elsewhere, tools to work with forms, and more.


<?php

declare(strict_types=1);

namespace App\Web\Echo;

use Yiisoft\FormModel\FormModel;
use Yiisoft\Validator\Label;
use Yiisoft\Validator\Rule\Length;

final class Form extends FormModel
{
    #[Label('The message to be echoed')]
    #[Length(min: 2)]
    public string $message = '';
}

Helpers

There are various helpers available to simplify common programming tasks.

Internationalization

Since the Yii framework community and the team members are from all over the world, we take internationalization seriously and understand it very well.

Out of the box, Yii3 has:

  • Message translation with intl-powered ICU formatting.
  • Support in a view layer.
  • Support in the router.

Testing

Proper dependency Inversion makes it easy to unit test the code created with Yii3. Following PSR for request and response allows you to perform API testing without actually running an HTTP server.

To ease testing even more, a set of test-specific PSR implementations are provided by yiisoft/test-support.

Error handling

We value developers' time. That's why we pay attention to error messages and the error page. All these are exceptionally useful. Messages provide context. We'll never use "Error" as a message. Additionally, we took it to the next level with "friendly exceptions." In development mode these are rendered with the information block that explains why the error happened and how to likely fix it. In the future we'll try to provide a button that will automatically try to apply the fix.

The error page contains the error. For each stack trace level, it displays the source code. The line where the error happened is highlighted. Stacktrace items that are part of the framework packages are collapsed by default because these are tested so much that it's unlikely that the error is there.

The handler itself, yiisoft/error-handler, is even more interesting than the one of Yii2. It handles out-of-memory errors and fatals, can maps certain exception types to custom exceptions, respond in different formats, etc.

Additionally, there is a package to integrate Sentry for error collection.

Documentation

There are the following resources available:

  • The definitive guide — the main source about using the framework as a whole.
  • Community cookbook — how to do X with Yii3.
  • API docs for all packages — all the public API rendered nicely for browsing
  • Individual package readme and docs — explains how to use each individual package

Exceptional quality standards

Yii3 maintains exceptional code quality standards across all packages. Every line undergoes rigorous verification through multiple layers of automated testing and analysis. 

For all the packages, we have nearly 100% coverage with tests, strict Psalm/PhpStan types, and a close to 100% mutation score. Every code change is reviewed publicly. All these measures give an exceptionally stable and predictable foundation for your projects.

Predictable releases

The release policy is SemVer. Each package is versioned independently. Patch versions never break anything while fixing bugs. The minor version introduces enhancements and features but is still compatible. The major version introduces backwards incompatible changes requiring code updates.

And more

There are many additional features that are less exciting but are very important:

Likely we forgot to mention some parts of the framework, so go ahead and explore.

Useful resources

Below are some useful links about Yii3:

Thank you!

Thank you for your support and patience! We did it together. All the core team members, community contributors, and backers.

We're pretty sure the Yii3 codebase will serve us well in at least the next 10 years or even more.

Future plans are:

  1. Guide improvements and missing pieces.
  2. More package releases to enrich the ecosystem and tooling. Queue, the debug panel, and Gii already work well, but we're still polishing these.
  3. Checking feedback, fixing, and improving things.
  4. Website improvements.

Merry Christmas and Happy New Year! Enjoy!

]]>
0
[news] Redis extension 2.1.0 released Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/776/redis-extension-2-1-0-released https://www.yiiframework.com/news/776/redis-extension-2-1-0-released samdark samdark

We are very pleased to announce the release of Redis extension version 2.1.0.

This version fixes one PHP 8.4 compatibility issue that was found since last release, adds support for predis, changes yii\redis\Cache::$forceClusterMode to false, and makes yii\redis\Connection implement yii\redis\ConnectionInterface.

See the CHANGELOG for details.

]]>
0
[news] Yii 1.1.32 is released Tue, 23 Dec 2025 10:19:58 +0000 https://www.yiiframework.com/news/775/yii-1-1-32-is-released https://www.yiiframework.com/news/775/yii-1-1-32-is-released marcovtwout marcovtwout

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

Yii 1.1.32 adds support for PHP 8.4 and improves compatibility with PHP 8.

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

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

]]>
0
[news] Yii Form Model 1.1 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/774/yii-form-model-1-1 https://www.yiiframework.com/news/774/yii-form-model-1-1 vjik vjik

Yii Form Model version 1.1.0 was released. In this version:

  • Add populateFromGet() and populateFromGetAndValidate() to FormHydrator
  • Add PHP 8.5 support
]]>
0
[news] Yii3 API project template 1.1 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/773/yii3-api-project-template-1-1 https://www.yiiframework.com/news/773/yii3-api-project-template-1-1 vjik vjik

Yii3 API project template version 1.1.0 was released. In this version:

  • Add Makefile stop goal for stopping Docker containers (@samdark,
  • Prune unused container and do not detach on make prod-deploy
  • Add PHP 8.5 support
  • Update composer dependencies
]]>
0
[news] Cycle ORM query adapter for yiisoft/data 1.0.0 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/772/cycle-orm-query-adapter-for-yiisoftdata-1-0-0 https://www.yiiframework.com/news/772/cycle-orm-query-adapter-for-yiisoftdata-1-0-0 samdark samdark

Cycle ORM query adapter for yiisoft/data version 1.0.0 was released.

There package provides Cycle ORM query adapter and writer for Yii Data.

See package documentation for usage.

]]>
0
[news] Yii3 web application template 1.1 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/771/yii3-web-application-template-1-1 https://www.yiiframework.com/news/771/yii3-web-application-template-1-1 vjik vjik

Yii3 web application template version 1.1.0 was released. In this version:

  • Add Makefile stop goal for stopping Docker containers
  • Update structure in src/ directory
  • Improve message for missing or invalid APP_ENV
  • Remove <meta http-equiv="X-UA-Compatible" content="IE=edge"> from layout
  • Update composer dependencies
  • Use relative path for Psalm cache directory
  • Prune unused container and do not detach on make prod-deploy
  • Add PHP 8.5 support
  • Always use only one goal in Makefile
  • Add DI container delegates configuration
  • Fix fake goals in Makefile
]]>
0
[news] Yii RBAC DB 2.1 Fri, 19 Dec 2025 12:20:01 +0000 https://www.yiiframework.com/news/770/yii-rbac-db-2-1 https://www.yiiframework.com/news/770/yii-rbac-db-2-1 vjik vjik

Yii RBAC Database storage 2.1.0

Yii RBAC DB version 2.1.0 was released. In this version:

  • Adapt to Yii DB 2
  • Change PHP constraint in composer.json to 8.1 - 8.5
  • Refactor AssignmentStorage::filterUserItemNames() method
  • Bump yiisoft/rbac version to ^2.1
  • Mark internal class ItemTreeTraversalFactory as final
]]>
0
[news] Yii Event 2.2 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/769/yii-event-2-2 https://www.yiiframework.com/news/769/yii-event-2-2 vjik vjik

Yii Event version 2.2.0 was released. In this version:

  • Change PHP constraint in composer.json to 8.1 - 8.5
  • Minor refactoring of CallableFactory and ListenerCollectionFactory
  • Raise minimum PHP version to ^8.1 and refactor code
]]>
0
[news] Yii Cache DB Handler 1.1 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/768/yii-cache-db-handler-1-1 https://www.yiiframework.com/news/768/yii-cache-db-handler-1-1 vjik vjik

Yii Cache DB Handler version 1.1.0 was released. In this version:

  • Bump supported version of yiisoft/db to ^2.0
  • Change supported PHP versions to 8.1 - 8.5
  • Mark DbSchemaManager::$db property as readonly
]]>
0
[news] Yii Logging Library 2.2 and targets Tue, 16 Dec 2025 22:38:00 +0000 https://www.yiiframework.com/news/767/yii-logging-library-2-2-and-targets https://www.yiiframework.com/news/767/yii-logging-library-2-2-and-targets samdark samdark

Yii Logging Library version 2.2.0 was released. In this version:

  • Add optional $levels parameter to Target constructor allowing log level filtering at instantiation
  • Add ext-psr to conflict section in composer.json

All targets were updated as well to allow $levels in constructor. Additionally:

  • File Target 3.1.0 added categories, except and exportInterval setters to default config and replaced rotate by rename to rotate by copy.
  • Syslog Target 2.1.0 removed dead code that checked case when syslog() returned false.
  • DB Target 1.1.0 bumped minimal required PHP version to 8.1.
]]>
0
[news] Yii DataView 1.0 Sun, 14 Dec 2025 07:04:35 +0000 https://www.yiiframework.com/news/766/yii-dataview-1-0 https://www.yiiframework.com/news/766/yii-dataview-1-0 vjik vjik

The first stable release of a powerful set of data displaying widgets is now available.

Yii DataView provides three flexible widgets for presenting data in web applications:

  • ListView — display data as a customizable list.

  • GridView — present data in a feature-rich grid with sorting, filtering, and pagination.

  • DetailView — show detailed information about a single record.

Data Source Independence. Works seamlessly with database, CSV, REST API, or any data source through the Yii Data package.

Built-in Pagination. Handle large datasets efficiently with integrated pagination controls.

Internationalization. Full translation support for multilingual applications.

Theming Support. Customize appearance to match your application design.

Flexible Column Types. DataColumn with custom value presenters, ActionColumn, etc.

Modern PHP. Built for PHP 8.1 - 8.5 with type safety and modern best practices.

]]>
0
[news] Yii Data DB 1.0 Sat, 13 Dec 2025 17:26:26 +0000 https://www.yiiframework.com/news/765/yii-data-db-1-0 https://www.yiiframework.com/news/765/yii-data-db-1-0 vjik vjik

Yii Data DB, a database query adapter for Yii Data, is now available.

This package provides a data reader implementation that bridges Yii DB with the Yii Data reading interface, making it easy to work with database queries in a flexible and consistent way.

  • QueryDataReader. Wraps database queries to provide pagination, sorting, and filtering capabilities.

  • Field Mapping. Map data reader field names to database columns for clean API design.

  • Batch Processing. Process large datasets efficiently with configurable batch sizes.

  • Multi-Database Support. Tested with SQLite, MySQL, PostgreSQL, Microsoft SQL Server, and Oracle.

]]>
0
[news] Yii Data 2.0 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/764/yii-data-2-0 https://www.yiiframework.com/news/764/yii-data-2-0 vjik vjik

Yii Data 2 is now available, bringing major improvements and modernization to this package that provides generic data abstractions for reading, writing, and processing.

  • PHP 8.1+ required: modernized codebase with readonly properties and improved type safety.

  • Enhanced filtering: added Stringable support, nested value filtering, case-sensitive Like filter with matching modes, and new All/None filters.

  • Improved pagination: new LimitableDataInterface, nextPage()/previousPage() methods, PageToken class, and better limit handling in paginators.

  • Better developer experience: comprehensive Psalm annotations, PageNotFoundException for clearer error handling, and OrderHelper for low-level order operations.

For a complete list of changes, see the CHANGELOG.md.

]]>
0
[news] Yii Middleware Dispatcher 5.4 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/763/yii-middleware-dispatcher-5-4 https://www.yiiframework.com/news/763/yii-middleware-dispatcher-5-4 vjik vjik

Yii Middleware Dispatcher version 5.4.0 was released. In this version:

  • Add support for using middlewares from container-registered identifier
  • Add PHP 8.5 support
]]>
0
[news] Yii HTML 3.12 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/762/yii-html-3-12 https://www.yiiframework.com/news/762/yii-html-3-12 vjik vjik

Yii HTML version 3.12.0 was released. In this version:

  • Add Tag::addStyle() and Tag::removeStyle() methods
  • Add Color class, Html::color() and Input::color() methods
  • Add PHP 8.5 support
  • Minor refactor RadioList::renderUncheckInput() and CheckboxList::renderUncheckInput() methods
]]>
0
[news] Yii Caching Library 3.2 Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/news/760/yii-caching-library-3-2 https://www.yiiframework.com/news/760/yii-caching-library-3-2 vjik vjik

Yii Caching Library version 3.2.0 was released. In this version:

  • Add Ttl value object for working with time-to-live duration
  • Add PHP 8.5 support
]]>
0
[extension] davidrnk/yii2-recurring-date Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/davidrnk/yii2-recurring-date https://www.yiiframework.com/extension/davidrnk/yii2-recurring-date DavidRmz DavidRmz

yii2-recurring-date

  1. Main Features
  2. Installation
  3. Usage
  4. JSON Format (persisted)
  5. Calculation of the Next Expiration Date
  6. Configuration and Customization
  7. Validations and UX Behavior
  8. Internationalization (i18n)
  9. Tests
  10. Best Practices and Notes
  11. Contributing
  12. License

A Yii2 extension/widget that provides a simple and intuitive interface to define and manage recurring date patterns. It is designed to simplify the configuration of renewals, expirations, and periodic events, with clean integration into Yii2 forms and internationalization support.

Main Features

  • Visual interface to configure recurring periods: no expiration, interval (days/months/years), monthly (day of the month), yearly (day + month), and specific date.
  • Handling of edge cases (e.g., days 29/30/31 and February 29) with configurable adjustment policy (previous | next).
  • Persistence of the recurrence scheme in a hidden field as JSON ready to be sent to the server.
  • Backend function to calculate the next expiration date based on a base date and the configuration.
  • Localization (i18n) with translations in English and Spanish.

Installation

Install the extension with Composer:

composer require davidrnk/yii2-recurring-date

Register the asset (if the widget does not do it automatically) and add the widget to your views/forms according to the usage examples.

Usage

The extension can be used with Yii2 models (ActiveForm) or independently.

Usage with model (ActiveForm):

use davidrnk\RecurringDate\Widget\RecurringDate;

echo $form->field($model, 'recurrence_config')->widget(RecurringDate::class, [
    // options
    'options' => ['class' => 'form-control my-custom-class'],
    'labels' => [
        'title_modal' => 'Configure recurrence',
        // you can override other labels
    ],
]);

Usage without model:

echo davidrnk\RecurringDate\Widget\RecurringDate::widget([
    'name' => 'recurrence',
    'value' => json_encode(['type' => 'monthly', 'day' => 1]),
    'options' => ['class' => 'form-control'],
]);

The widget renders a read-only text control with a button to open a modal where recurrence is configured. The resulting JSON is saved in a hidden field (input.hidden) and is the content you should store in the database.

JSON Format (persisted)

The widget persists the configuration in JSON format with the following main structure (examples):

  • No expiration
{"type": "no_expiration"}
  • Interval
{ "type": "interval", "value": 10, "unit": "days" }
  • Monthly (day of the month) + optional adjustment
{ "type": "monthly", "day": 31, "adjust": "previous" }
  • Yearly (day + month) + optional adjustment
{ "type": "yearly", "day": 29, "month": 2, "adjust": "previous" }
  • Specific date
{ "type": "specific_date", "date": "2025-12-31" }

Relevant keys:

  • type: one of no_expiration, interval, monthly, yearly, specific_date.
  • value, unit: used by interval (unit: days|months|years).
  • day: day of the month (1-31).
  • month: month (1-12).
  • date: ISO date for specific_date.
  • adjust: policy when the day does not exist in the period (values: previous — adjust to the last valid day of the month, or next — move to the next day).

Calculation of the Next Expiration Date

In the backend, the library provides a function to calculate the resulting date based on a base date and the JSON configuration. In the code the function is called:

use davidrnk\RecurringDate\Core\RecurringDateEngine;

$nextDueDate = RecurringDateEngine::calculateExpiration($startDate, $configArray);
// returns DateTime instance or null if configuration is invalid

In the documentation and examples of this README, we refer to this date as nextDueDate. If calculateExpiration returns null, the combination of parameters is invalid or could not be calculated.

Quick example:

$start = new \DateTime('2025-01-31');
$cfg = ['type' => 'monthly', 'day' => 31, 'adjust' => 'previous'];
$next = RecurringDateEngine::calculateExpiration($start, $cfg);
echo $next ? $next->format('Y-m-d') : 'invalid';

Configuration and Customization

The widget exposes several ways to adjust its visual and textual behavior:

  • options (array): HTML attributes for the visible text field (e.g., class, style, placeholder).
  • labels (array): you can override texts and labels used in the modal. Examples of keys you can customize:
    • title_modal, type, configure, preview, save, cancel, quantity, unit, month_day, adjust, adjust_previous, adjust_next, etc.
  • Translations: the extension includes files in src/messages/en and src/messages/es. The displayed texts are also sent to JavaScript for preview.

Example of label customization:

echo $form->field($model, 'recurrence_config')->widget(RecurringDate::class, [
    'labels' => [
        'title_modal' => 'Schedule repetition',
        'adjust_previous' => 'Adjust to the last day of the month',
    ],
]);

Validations and UX Behavior

  • The widget validates on the client side combinations that are clearly invalid (e.g., 31 in months with 30 days, February 31) and blocks saving when the selection is fatal.
  • For non-fatal cases (e.g., day >= 29 in monthly or February 29 in yearly), it shows a warning and allows the user to select the adjust policy.
  • The adjust value is persisted in JSON and is considered by RecurringDateEngine::calculateExpiration.

Internationalization (i18n)

The default language of the extension is English. Translations are included in src/messages/en and src/messages/es. Strings used in views and JavaScript translations are defined and loaded from RecurringDate::getJSTranslations().

If you need to add another language, add a file in src/messages/XX/davidrnk.recurring.php with the required keys.

Tests

Unit tests for PHP are included for the calculation logic (tests/RecurringDateEngineTest.php) and should be executed with:

vendor/bin/phpunit tests/RecurringDateEngineTest.php

Best Practices and Notes

  • Save the persisted JSON directly in a text field in the database (e.g., recurrence_config), and use RecurringDateEngine::calculateExpiration to obtain the next expiration date when needed.
  • Decide and document the default adjust policy for your domain (by default the extension uses previous — clamp to the last valid day). This avoids surprises when calculating next dates.
  • Review the locale configuration (Yii::$app->language) to ensure the UI displays the desired translations.

Contributing

Pull requests and issues are welcome. For major changes, first open an issue describing the proposed change.

License

BSD-3-Clause — see LICENSE file.

]]>
0
[extension] crenspire/yii2-react-starter Fri, 28 Nov 2025 13:03:10 +0000 https://www.yiiframework.com/extension/crenspire/yii2-react-starter https://www.yiiframework.com/extension/crenspire/yii2-react-starter akshaypjoshi akshaypjoshi

993323

Yii2 - Modern Starter Kit

  1. Screenshots
  2. Requirements
  3. Installation
  4. Production Build
  5. Configuration
  6. Directory Structure
  7. Testing

Yii2 - Modern Starter Kit is a modern, full-featured Yii 2 application template with React frontend powered by Inertia.js.

The template includes a beautiful UI built with Shadcn UI components, dark/light theme support, user authentication, CRUD operations, and all the modern features you need to rapidly build web applications.

Screenshots

Light Theme
#### Home Page Home Page - Light Theme #### Sign In Sign In - Light Theme #### Sign Up Sign Up - Light Theme #### Dashboard Dashboard - Light Theme #### Users Management Users Management - Light Theme #### Settings Settings - Light Theme #### Forgot Password Forgot Password - Light Theme
Dark Theme
#### Home Page Home Page - Dark Theme #### Sign In Sign In - Dark Theme #### Sign Up Sign Up - Dark Theme #### Dashboard Dashboard - Dark Theme #### Users Management Users Management - Dark Theme #### Settings Settings - Dark Theme #### Forgot Password Forgot Password - Dark Theme

Requirements

Before you begin, ensure you have the following installed on your system:

  • PHP >= 7.4.0 (PHP 8.0+ recommended)
  • Composer - PHP dependency manager (Install Composer)
  • Node.js >= 18.0.0 and npm (or yarn)
  • MySQL >= 5.7 or MariaDB >= 10.3
  • Web Server (Apache/Nginx) or PHP built-in server

Installation

Step 1: Clone the Repository
git clone [email protected]:crenspire/yii2-react-starter.git
cd yii2-react-starter

Or download and extract the project archive to your desired directory.

Step 2: Install PHP Dependencies

Install all PHP dependencies using Composer:

composer install

This will install all required PHP packages including Yii2 framework and Inertia.js adapter.

Step 3: Install Node.js Dependencies

Install all frontend dependencies:

npm install

This will install React, Inertia.js, Shadcn UI components, Tailwind CSS, and all other frontend dependencies.

Step 4: Configure Database
  1. Create a MySQL database for your application:
CREATE DATABASE yii2basic CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  1. Update the database configuration in config/db.php:
return [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=localhost;dbname=yii2basic',
    'username' => 'root',
    'password' => 'your_password',
    'charset' => 'utf8mb4',
];

Replace your_password with your MySQL root password (or your database user credentials).

Step 5: Run Database Migrations

Run the migrations to create all necessary database tables:

php yii migrate

This will create the following tables:

  • users - User accounts with soft deletes
  • password_reset_tokens - Password reset functionality
  • And other required tables
Step 6: Seed Admin User (Optional)

Create an admin user for testing:

php yii seed/admin

This creates an admin user with:

Step 7: Configure Cookie Validation Key

The cookie validation key should be automatically generated during composer install. If it wasn't, you can generate it manually:

  1. Open config/web.php
  2. Find the cookieValidationKey in the request component
  3. Set it to a random string (32 characters recommended):
'request' => [
    'cookieValidationKey' => 'your-random-32-character-string-here',
],

You can generate a random string using:

php -r "echo bin2hex(random_bytes(16));"
Step 8: Start Development Servers

You need to run two servers simultaneously:

  1. PHP Development Server (Backend): `bash php yii serve `

  2. Vite Development Server (Frontend): `bash npm run dev `

Or use concurrently to run both at once (if installed):

npx concurrently "php yii serve" "npm run dev"
Step 9: Access the Application

Open your browser and navigate to:

http://localhost:8080

You should see the home page. You can now:

  • Sign Up: Create a new account at /auth/register
  • Sign In: Login at /auth/login (or use admin credentials if you seeded)
  • Dashboard: Access the dashboard at /dashboard (requires login)

Production Build

For production deployment, build the frontend assets:

npm run build

This will compile and optimize all React components and assets into the web/dist directory.

Configuration

Environment Configuration

The application uses Yii2's environment configuration. You can set the environment by:

  1. Copying config/params.php and modifying as needed
  2. Setting YII_ENV constant in web/index.php:
    • YII_ENV_DEV - Development mode
    • YII_ENV_PROD - Production mode
Additional Configuration Files
  • config/web.php - Web application configuration
  • config/console.php - Console application configuration
  • config/db.php - Database configuration
  • config/params.php - Application parameters

NOTES:

  • Make sure the runtime/ and web/assets/ directories are writable by the web server
  • For production, disable debug mode and enable schema caching in config/web.php
  • Configure your web server to point to the web/ directory as the document root

Directory Structure

assets/             contains assets definition
commands/           contains console commands (controllers)
config/             contains application configurations
controllers/        contains Web controller classes
mail/               contains view files for e-mails
models/             contains model classes
migrations/         contains database migrations
resources/          contains frontend resources (React, CSS, JS)
  js/               React components and pages
  css/              Stylesheets
runtime/            contains files generated during runtime
tests/              contains various tests for the basic application
vendor/             contains dependent 3rd-party packages
views/              contains view files for the Web application
web/                contains the entry script and Web resources
  images/           contains screenshots and images
  dist/             contains built frontend assets (production)

Testing

Tests are located in tests directory. They are developed with Codeception PHP Testing Framework. By default, there are 3 test suites:

  • unit
  • functional
  • acceptance

Tests can be executed by running

vendor/bin/codecept run

The command above will execute unit and functional tests. Unit tests are testing the system components, while functional tests are for testing user interaction. Acceptance tests are disabled by default as they require additional setup since they perform testing in real browser.

Running acceptance tests

To execute acceptance tests do the following:

  1. Rename tests/acceptance.suite.yml.example to tests/acceptance.suite.yml to enable suite configuration

  2. Replace codeception/base package in composer.json with codeception/codeception to install full-featured version of Codeception

  3. Update dependencies with Composer

    composer update

  4. Download Selenium Server and launch it:

    java -jar ~/selenium-server-standalone-x.xx.x.jar

    In case of using Selenium Server 3.0 with Firefox browser since v48 or Google Chrome since v53 you must download GeckoDriver or ChromeDriver and launch Selenium with it:

    ` # for Firefox java -jar -Dwebdriver.gecko.driver=~/geckodriver ~/selenium-server-standalone-3.xx.x.jar

    # for Google Chrome java -jar -Dwebdriver.chrome.driver=~/chromedriver ~/selenium-server-standalone-3.xx.x.jar `

    As an alternative way you can use already configured Docker container with older versions of Selenium and Firefox:

    docker run --net=host selenium/standalone-firefox:2.53.0

  5. (Optional) Create yii2basic_test database and update it by applying migrations if you have them.

    tests/bin/yii migrate
    

    The database configuration can be found at config/test_db.php.

  1. Start web server:

    tests/bin/yii serve

  2. Now you can run all available tests

    # run all available tests
    vendor/bin/codecept run
    
    # run acceptance tests
    vendor/bin/codecept run acceptance
    
    # run only unit and functional tests
    vendor/bin/codecept run unit,functional
    
Code coverage support

By default, code coverage is disabled in codeception.yml configuration file, you should uncomment needed rows to be able to collect code coverage. You can run your tests and collect coverage with the following command:

#collect coverage for all tests
vendor/bin/codecept run --coverage --coverage-html --coverage-xml

#collect coverage only for unit tests
vendor/bin/codecept run unit --coverage --coverage-html --coverage-xml

#collect coverage for unit and functional tests
vendor/bin/codecept run functional,unit --coverage --coverage-html --coverage-xml

You can see code coverage output under the tests/_output directory.

]]>
0
[extension] crenspire/yii2-inertia Thu, 11 Dec 2025 12:26:03 +0000 https://www.yiiframework.com/extension/crenspire/yii2-inertia https://www.yiiframework.com/extension/crenspire/yii2-inertia akshaypjoshi akshaypjoshi ]]> 0 [wiki] Building Modern SPAs with Yii2 and Inertia.js Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/wiki/2582/building-modern-spas-with-yii2-and-inertia-js https://www.yiiframework.com/wiki/2582/building-modern-spas-with-yii2-and-inertia-js akshaypjoshi akshaypjoshi
  1. Introduction
  2. Installation
  3. Quick Setup
  4. Core Features
  5. Best Practices
  6. Common Patterns
  7. Troubleshooting
  8. Additional Resources
  9. Conclusion

Introduction

Inertia.js is a modern approach to building single-page applications (SPAs) without the complexity of building an API. It allows you to use modern JavaScript frameworks like React, Vue, or Svelte while keeping your Yii2 controllers and routing intact.

Why Inertia.js?

  • No need to build a separate API
  • Keep your existing Yii2 controllers and models
  • Server-side routing and validation
  • Better SEO than traditional SPAs
  • Simpler architecture than API + SPA

The crenspire/yii2-inertia package provides a seamless Inertia.js adapter for Yii2, matching the developer experience of popular frameworks like Laravel's Inertia adapter.

Installation

Install via Composer:

composer require crenspire/yii2-inertia

Quick Setup

1. Configure the View Renderer

Add to your config/web.php:

'view' => [
    'renderers' => [
        'inertia' => \Crenspire\Yii2Inertia\ViewRenderer::class,
    ],
],
2. Create Root Template

Create views/layouts/inertia.php:

<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= Html::encode($this->title) ?></title>
    <script type="module" src="/dist/assets/index.js"></script>
    <link rel="stylesheet" href="/dist/assets/index.css">
</head>
<body>
    <div id="app" data-page="<?= htmlspecialchars(json_encode($page), ENT_QUOTES, 'UTF-8') ?>"></div>
</body>
</html>
3. Use in Controllers
use Crenspire\Yii2Inertia\Inertia;

class HomeController extends \yii\web\Controller
{
    public function actionIndex()
    {
        return Inertia::render('Home', [
            'title' => 'Welcome',
            'users' => User::find()->all(),
        ]);
    }
}
4. Setup Frontend

Install Inertia.js and your framework:

# For React
npm install @inertiajs/inertia @inertiajs/inertia-react react react-dom

# For Vue
npm install @inertiajs/inertia @inertiajs/inertia-vue3 vue

# For Svelte
npm install @inertiajs/inertia @inertiajs/inertia-svelte svelte

Create src/main.jsx (React example):

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createInertiaApp } from '@inertiajs/inertia-react';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';

createInertiaApp({
  resolve: (name) => {
    const pages = { Home, Dashboard };
    return pages[name];
  },
  setup({ el, App, props }) {
    ReactDOM.createRoot(el).render(<App {...props} />);
  },
});

Core Features

Sharing Global Data

Share data that should be available on every page:

// In config/bootstrap.php or base controller
use Crenspire\Yii2Inertia\Inertia;

// Share user data
Inertia::share('user', function () {
    return Yii::$app->user->identity;
});

// Share flash messages
Inertia::share('flash', function () {
    return [
        'success' => Yii::$app->session->getFlash('success'),
        'error' => Yii::$app->session->getFlash('error'),
    ];
});

// Share multiple values
Inertia::share([
    'appName' => 'My Application',
    'version' => '1.0.0',
]);

Tip: Use closures for dynamic data (like user or flash messages) to ensure fresh data on each request.

Handling Redirects

For form submissions and other redirects, use Inertia::location():

public function actionStore()
{
    $model = new User();
    
    if ($model->load(Yii::$app->request->post()) && $model->save()) {
        Yii::$app->session->setFlash('success', 'User created!');
        return Inertia::location('/users');
    }
    
    return Inertia::render('Users/Create', [
        'errors' => $model->errors,
    ]);
}

The method automatically handles both Inertia requests (409 status) and regular requests (302 redirect).

Asset Versioning

Set up versioning for cache busting:

// Automatic (uses manifest.json mtime if exists)
$version = Inertia::version();

// Manual string
Inertia::version('1.0.0');

// Callback (recommended)
Inertia::version(function () {
    return filemtime(Yii::getAlias('@webroot/dist/manifest.json'));
});
Partial Reloads

Optimize performance by only reloading specific props:

// Client requests only 'users' and 'stats' props
return Inertia::render('Dashboard', [
    'users' => $users,      // Included
    'stats' => $stats,      // Included
    'other' => $other,      // Excluded
]);

In your frontend component:

import { router } from '@inertiajs/inertia-react';

const refreshStats = () => {
  router.reload({ only: ['stats'] });
};

Best Practices

1. Organize Components by Route

Structure your frontend to match backend routes:

src/pages/
  Home.jsx
  Dashboard.jsx
  Users/
    Index.jsx
    Show.jsx
    Edit.jsx
// Maps to src/pages/Users/Index.jsx
return Inertia::render('Users/Index', ['users' => $users]);
2. Handle Forms with Inertia

Use Inertia's form helper for better UX:

import { useForm } from '@inertiajs/inertia-react';

export default function CreateUser() {
  const { data, setData, post, processing, errors } = useForm({
    name: '',
    email: '',
  });

  const submit = (e) => {
    e.preventDefault();
    post('/users');
  };

  return (
    <form onSubmit={submit}>
      <input
        value={data.name}
        onChange={(e) => setData('name', e.target.value)}
      />
      {errors.name && <div>{errors.name}</div>}
      <button disabled={processing}>Submit</button>
    </form>
  );
}
3. Error Handling

Return validation errors from your controller:

public function actionStore()
{
    $model = new User();
    
    if ($model->load(Yii::$app->request->post()) && $model->save()) {
        return Inertia::location('/users');
    }
    
    // Return with errors
    return Inertia::render('Users/Create', [
        'errors' => $model->errors,
        'values' => $model->attributes,
    ]);
}
4. Flash Messages

Create a reusable flash message component:

// Share flash messages globally
Inertia::share('flash', function () {
    return [
        'success' => Yii::$app->session->getFlash('success'),
        'error' => Yii::$app->session->getFlash('error'),
    ];
});
// FlashMessage.jsx
import { usePage } from '@inertiajs/inertia-react';

export default function FlashMessage() {
  const { flash } = usePage().props;
  
  if (!flash) return null;
  
  return (
    <div>
      {flash.success && <div className="alert-success">{flash.success}</div>}
      {flash.error && <div className="alert-error">{flash.error}</div>}
    </div>
  );
}
5. Pagination

Handle pagination with Yii2's DataProvider:

use yii\data\ActiveDataProvider;

public function actionIndex()
{
    $dataProvider = new ActiveDataProvider([
        'query' => User::find(),
        'pagination' => ['pageSize' => 15],
    ]);
    
    return Inertia::render('Users/Index', [
        'users' => $dataProvider->getModels(),
        'pagination' => [
            'currentPage' => $dataProvider->pagination->page + 1,
            'lastPage' => $dataProvider->pagination->pageCount,
            'perPage' => $dataProvider->pagination->pageSize,
            'total' => $dataProvider->totalCount,
        ],
    ]);
}
6. Optimize Database Queries

Use eager loading to prevent N+1 queries:

$users = User::find()
    ->with('posts', 'comments')
    ->all();
7. Minimize Prop Size

Only send necessary data:

// Instead of full models
return Inertia::render('Users/Index', [
    'users' => User::find()
        ->select(['id', 'name', 'email'])
        ->asArray()
        ->all(),
]);

Common Patterns

Base Controller

Create a base controller for shared functionality:

namespace app\controllers;

use Crenspire\Yii2Inertia\Inertia;
use yii\web\Controller;

abstract class BaseController extends Controller
{
    public function init()
    {
        parent::init();
        
        Inertia::share('user', function () {
            return Yii::$app->user->identity;
        });
    }
    
    protected function inertiaRender($component, $props = [])
    {
        return Inertia::render($component, $props);
    }
}
Resource Controller
class UsersController extends BaseController
{
    public function actionIndex()
    {
        $users = User::find()->all();
        return $this->inertiaRender('Users/Index', ['users' => $users]);
    }
    
    public function actionShow($id)
    {
        $user = User::findOne($id);
        if (!$user) {
            throw new NotFoundHttpException('User not found');
        }
        return $this->inertiaRender('Users/Show', ['user' => $user]);
    }
    
    public function actionStore()
    {
        $model = new User();
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            Yii::$app->session->setFlash('success', 'User created!');
            return Inertia::location(['users/show', 'id' => $model->id]);
        }
        return $this->inertiaRender('Users/Create', [
            'errors' => $model->errors,
        ]);
    }
}

Troubleshooting

Version Mismatch Issues

If you're experiencing frequent full page reloads:

  1. Ensure your version callback returns a stable value
  2. Check that manifest.json exists and is readable
  3. Verify file permissions
Inertia::version(function () {
    $manifest = Yii::getAlias('@webroot/dist/manifest.json');
    return file_exists($manifest) ? (string)filemtime($manifest) : '1';
});
Redirects Not Working

Always use Inertia::location() instead of Yii::$app->response->redirect() for Inertia requests.

Props Not Available in Frontend
  1. Verify props are passed as an array in PHP
  2. Ensure props are JSON-serializable (no closures)
  3. Use the usePage() hook correctly:
import { usePage } from '@inertiajs/inertia-react';

const { props } = usePage();
const { title, user } = props;
CSRF Token

Share CSRF token globally:

Inertia::share('csrfToken', function () {
    return Yii::$app->request->csrfToken;
});

Use in forms:

<input type="hidden" name="_token" value={props.csrfToken} />

Additional Resources

Conclusion

Inertia.js with Yii2 provides a powerful way to build modern SPAs while keeping the simplicity and power of server-side routing and validation. The crenspire/yii2-inertia package makes it easy to integrate Inertia.js into your Yii2 applications with a familiar API.

Give it a try and let us know what you think! For issues, questions, or contributions, please visit the GitHub repository.

]]>
0
[news] Yii 2.0.54 Fri, 09 Jan 2026 22:59:54 +0000 https://www.yiiframework.com/news/748/yii-2-0-54 https://www.yiiframework.com/news/748/yii-2-0-54 samdark samdark

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

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

In this release:

  • PHP 8.5 support. The minimum supported version was raised from 7.3 to 7.4.
  • PHPStan/Psalm annotations revamped, adding better IDE support and static types.
  • Fixture loader now throws exceptions instead of ignoring missing fixtures.
  • ArrayDataProvider now supports paths as keys.
  • BaseStringHelper::convertIniSizeToBytes().
  • Bug fixes.

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

Special thanks goes to Maksim Spirkov who took care of the majority of annotation-related changes.

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

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

]]>
0
[extension] ldkafka/yii2-scheduler Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/ldkafka/yii2-scheduler https://www.yiiframework.com/extension/ldkafka/yii2-scheduler ldk ldk

yii2-scheduler

  1. Installation
  2. Usage
  3. Upgrade notes
  4. Release checklist
  5. Logging
  6. Monitoring Events
  7. Cleanup and safety
  8. Compatibility
  9. License

High-resolution cron-like job scheduler for Yii2 supporting:

  • External cron mode (invoke the scheduler from system cron, typically every minute)
  • Daemon mode (single long-running loop with microsecond timing and drift correction)
  • Queue integration (yii2-queue) or synchronous execution
  • Single-instance locks with max running time and stale lock reclamation
  • Atomic distributed locks (cache add) with metadata: {pid, host, ts}
  • Robust cron pattern parsing: wildcards, ranges, steps, lists

Installation

  1. Require the package (after you publish it to a VCS):
composer require ldkafka/yii2-scheduler
  1. Configure in your Yii2 console app (e.g. console/config/main.php):
return [
    'bootstrap' => [
        // ensure the component can bootstrap its controller
        'scheduler',
    ],
    'components' => [
        'cache' => [ /* your cache config */ ],
        'queue_scheduler' => [ /* your yii2-queue config */ ],

        'scheduler' => [
            'class' => ldkafka\scheduler\Scheduler::class,
            'config' => [
                'cache' => 'cache',            // optional; default 'cache' (uses Yii::$app->cache)
                'queue' => 'queue_scheduler',  // optional; omit to run inline synchronously
            ],
            'jobs' => require __DIR__ . '/scheduler.php',
        ],
    ],
];
  1. Create console/config/scheduler.php with your jobs:
<?php

use ldkafka\scheduler\ScheduledJob;

return [
    [
        'class' => \common\jobs\ExampleJob::class, // must extend ScheduledJob
        'run' => 'EVERY_MINUTE',
        'single_instance' => true,        // default true
        'max_running_time' => 300,        // seconds; 0 = unlimited
    ],
    [
        'class' => \common\jobs\NightlyJob::class,
        'run' => [
            'minutes' => 0,
            'hours' => 2,
            'wday' => '1-5',              // Mon-Fri
        ],
    ],
];

Usage

  • External cron mode:
php yii scheduler/run
  • Daemon mode (long-running with drift correction):
php yii scheduler/daemon

Upgrade notes

Upgrading from 1.0.3 or earlier
  • Lock format changed: Locks now store {pid, host, ts} instead of bare PID. No migration needed; old locks will expire naturally (1-hour TTL).
  • Config cleanup: Remove obsolete staleLockTtl and maxJobsPerTick from your scheduler config (not implemented).
  • Cron parsing improved: The day key is now normalized to mday. Update job configs using day to use mday instead for clarity.
  • Log levels adjusted: Normal flow messages moved from warning→info. Review log filters if you relied on warning-level job execution logs.

Release checklist

  • Symbolic: EVERY_MINUTE, EVERY_HOUR, EVERY_DAY, EVERY_WEEK, EVERY_MONTH
  • Cron-like array using getdate() keys: minutes, hours, mday (day of month), wday (weekday), mon, year
    • Patterns per key:
      • * - wildcard (matches any value)
      • 5 - exact match
      • */5 - step/interval (every 5th unit: 0, 5, 10...)
      • 1-5 - range (1 through 5 inclusive)
      • 10-20/2 - range with step (10, 12, 14, 16, 18, 20)
      • 1,3,5 - list (matches 1 or 3 or 5)
    • Multiple patterns can be combined with commas for OR semantics
Writing a job
namespace common\jobs;

use ldkafka\scheduler\ScheduledJob;
use Yii;

class ExampleJob extends ScheduledJob
{
    public function execute($queue = null)
    {
        Yii::info('ExampleJob executed', 'scheduler');
        // do work
        return true; // success
    }
}

Notes:

  • When using queue mode, jobs are pushed to the configured queue component and executed by a queue worker.
  • Locks use metadata format {pid: int, host: string, ts: int} and are acquired atomically with cache->add().
  • Stale lock reclamation: if the scheduler finds an existing lock with no running jobs in cache, it safely re-reads and deletes the lock before retrying (prevents race conditions).
  • Stale jobs exceeding max_running_time are auto-removed from the run cache; if the queue driver supports remove(), the queued job is also removed.
  • max_running_time is enforced during scheduler ticks; consider queueing long-running jobs for better concurrency.

Logging

All logs use the scheduler category with production-appropriate levels:

  • info: Normal operations (job selected, queued, executed, lock acquired/released)
  • warning: Anomalies worth attention (stale lock removed, job exceeded max time, single-instance conflict, queue check failure)
  • error: Failures requiring intervention (cache lock acquisition failed, job class not found, queue push failed, critical init errors)

Configure a log target in your console/config/main.php if needed:

'log' => [
    'targets' => [
        [
            'class' => 'yii\log\FileTarget',
            'levels' => ['error', 'warning', 'info'],
            'categories' => ['scheduler'],
            'logFile' => '@runtime/logs/scheduler.log',
            'maxFileSize' => 10240, // 10 MB
        ],
    ],
],

Monitoring Events

The scheduler triggers events throughout job lifecycle for integration with monitoring systems. Attach event handlers in your scheduler configuration:

'scheduler' => [
    'class' => ldkafka\scheduler\Scheduler::class,
    'on jobBeforeRun' => function ($event) {
        // $event is SchedulerJobEvent with: job_class, job_config, start_time
        Yii::info("Starting job: {$event->job_class}", 'monitoring');
    },
    'on jobAfterRun' => function ($event) {
        // $event includes: result, start_time, end_time
        $duration = $event->end_time - $event->start_time;
        Yii::info("Job {$event->job_class} completed in {$duration}s", 'monitoring');
    },
    'on jobError' => function ($event) {
        // $event includes: error, exception, trace, error_time
        Yii::error("Job {$event->job_class} failed: {$event->error}", 'monitoring');
    },
    'on jobBlocked' => function ($event) {
        // $event->data includes: job_id, job_config, reason, running_time
        $data = $event->data;
        Yii::warning("Job {$data['job_config']['class']} blocked: {$data['reason']}", 'monitoring');
    },
    'config' => [ /* ... */ ],
    'jobs' => [ /* ... */ ],
],
Available Events
  • EVENT_JOB_BEFORE_RUN (jobBeforeRun): Job is about to execute
  • EVENT_JOB_AFTER_RUN (jobAfterRun): Job completed successfully
  • EVENT_JOB_ERROR (jobError): Job threw exception
  • EVENT_JOB_BLOCKED (jobBlocked): Job prevented from running (e.g., single-instance lock)
  • EVENT_JOB_TIMEOUT (jobTimeout): Reserved for future use

All events use SchedulerJobEvent class with typed properties for monitoring integration.

Cleanup and safety

  • All jobs are executed via a SafeJobWrapper that catches exceptions and prevents worker crashes.
  • On completion (sync or async), the wrapper will call Scheduler::finalizeRuntimeJob() to remove the job entry from the persistent run cache.

Compatibility

  • Requires PHP >= 8.0, Yii2 ~2.0.14
  • Optional pcntl for graceful signal handling (SIGINT/SIGTERM). On platforms without pcntl, the daemon stops when too many ticks are missed (configurable).

License

BSD-3-Clause

]]>
0
[extension] ldkafka/yii2-google-gemini Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/ldkafka/yii2-google-gemini https://www.yiiframework.com/extension/ldkafka/yii2-google-gemini ldk ldk

Yii2 Google Gemini Component (v2.0.0)

  1. What’s New in 2.0.0
  2. Feature Summary
  3. Requirements
  4. Installation
  5. Quick Start
  6. Usage Examples
  7. Caching Modes Deep Dive
  8. Console Commands
  9. Configuration Options
  10. Supported Models (Snapshot)
  11. Canonical Response Format
  12. Helper Methods
  13. Cache Modes (Summary)
  14. Error Handling Pattern
  15. Advanced Usage
  16. Testing
  17. Troubleshooting
  18. Production Notes
  19. Links
  20. License
  21. Support / Contributing

Native, strongly-typed Yii2 component for the Google Gemini REST API. No external SDKs – only yii\\httpclient. Provides generation (text & multimodal), streaming, embeddings, Files API, token counting, and flexible caching strategies.

What’s New in 2.0.0

  • Rebuilt on pure REST calls (no Gemini SDK)
  • Added multimodal inline data handling
  • Added SSE streaming helper
  • Full embeddings single + batch support
  • Files API wiring (simplified upload flow)
  • Model discovery (list + get)
  • Uniform response shape & error handling
  • Client & server caching patterns
  • Strict typing, final class, consistent helper methods

Feature Summary

Area Capabilities
Generation Text, multimodal (image/audio/video/document via inline or file references)
Streaming SSE incremental output with user callback
Caching none, client (Yii cache history), server (Gemini CachedContent)
Embeddings Single + batch embeddings for RAG / similarity
Files Upload, list, get, delete (simplified direct PUT)
Models Enumerate models, inspect limits/capabilities
Tokens Pre-flight token counting for cost estimation
Helpers extractText, getFinishReason, getUsageMetadata

Requirements

  • PHP >= 8.0
  • Yii2 >= 2.0.40
  • yiisoft/yii2-httpclient

Installation

composer require ldkafka/yii2-google-gemini

Quick Start

Basic Configuration
'components' => [
    'gemini' => [
        'class' => 'ldkafka\gemini\Gemini',
        'apiKey' => 'YOUR_GEMINI_API_KEY',
        'generationConfig' => [
            'temperature' => 0.7,
            'topP' => 0.95,
            'maxOutputTokens' => 2048,
        ],
    ],
],
Simple Text Generation
$gemini = Yii::$app->gemini;
$resp = $gemini->generateContent('gemini-2.5-flash', 'Explain quantum computing');

if ($resp['ok']) {
    echo $gemini->extractText($resp['data']);
}

Usage Examples

1. Basic Text Generation
$resp = $gemini->generateContent('gemini-2.5-flash', 'What is PHP?');

if ($resp['ok']) {
    $text = $gemini->extractText($resp['data']);
    $usage = $gemini->getUsageMetadata($resp['data']);
    echo "Response: $text\n";
    echo "Tokens used: {$usage['totalTokenCount']}\n";
}
2. Streaming Responses
$gemini->streamGenerateContent('gemini-2.5-flash', 'Write a short story', function($chunk) {
    if ($text = $chunk['candidates'][0]['content']['parts'][0]['text'] ?? null) {
        echo $text;
        flush();
    }
});
3. Multimodal (Text + Image)
$content = [[
    'parts' => [
        ['text' => 'What is in this image?'],
        ['inline_data' => [
            'mime_type' => 'image/jpeg',
            'data' => base64_encode(file_get_contents('/path/to/image.jpg'))
        ]]
    ],
    'role' => 'user'
]];

$resp = $gemini->generateContent('gemini-2.5-flash', $content);
4. Client-side Conversation Caching
$gemini->cacheType = 'client';
$gemini->cacheTtl = 3600;

// First message
$resp = $gemini->chat('gemini-2.5-flash', 'My name is Alice', 'user123');

// Follow-up (remembers context)
$resp = $gemini->chat('gemini-2.5-flash', 'What is my name?', 'user123');
// Response: "Your name is Alice."
5. Server-side Context Caching (Advanced)
$gemini->cacheType = 'server';

// Create cache with system instruction (requires 32k+ tokens)
$cacheName = $gemini->createServerCache(
    'gemini-2.5-flash',
    'assistant-id',
    'You are a helpful travel assistant. [... long system instruction ...]',
    3600
);

// Use cached context
$resp = $gemini->chatServer('gemini-2.5-flash', 'Best beaches in Sydney?', 'assistant-id');
6. System Instructions
$gemini->systemInstruction = [
    'parts' => [['text' => 'You are a helpful coding assistant who explains concepts simply.']]
];

$resp = $gemini->generateContent('gemini-2.5-flash', 'Explain recursion');
7. File Uploads
// Upload a large video file
$resp = $gemini->uploadFile('/path/to/video.mp4', 'My Video', 'video/mp4');
$fileUri = $resp['data']['file']['uri'];

// Use in generation
$content = [[
    'parts' => [
        ['text' => 'Summarize this video'],
        ['file_data' => [
            'file_uri' => $fileUri,
            'mime_type' => 'video/mp4'
        ]]
    ]
]];
$resp = $gemini->generateContent('gemini-2.5-flash', $content);

// List uploaded files
$files = $gemini->listFiles();

// Delete file
$gemini->deleteFile('files/abc123');
8. Embeddings
// Single embedding
$resp = $gemini->embedContent(
    'text-embedding-004',
    'Hello world',
    'RETRIEVAL_QUERY'
);
$embedding = $resp['data']['embedding']['values'];

// Batch embeddings
$requests = [
    [
        'content' => ['parts' => [['text' => 'Document 1']]],
        'taskType' => 'RETRIEVAL_DOCUMENT'
    ],
    [
        'content' => ['parts' => [['text' => 'Document 2']]],
        'taskType' => 'RETRIEVAL_DOCUMENT'
    ],
];
$resp = $gemini->batchEmbedContents('text-embedding-004', $requests);
9. Token Counting
$tokens = $gemini->countTokens('gemini-2.5-flash', 'Your prompt text here');
echo "This prompt will use approximately $tokens tokens\n";
10. Model Discovery
// List all available models
$models = $gemini->listModels();
foreach ($models['data']['models'] as $model) {
    echo "{$model['name']}: {$model['displayName']}\n";
}

// Get specific model details
$model = $gemini->getModel('gemini-2.5-flash');
echo "Context window: {$model['data']['inputTokenLimit']} tokens\n";

Caching Modes Deep Dive

Mode Purpose Storage Pros Cons
none Stateless requests None Simplicity No memory of prior turns
client Short/medium chats Yii cache (gem_chat_<id>) Fast, light, adjustable TTL History grows; prune for very long sessions
server Large domain context Gemini CachedContent Huge reusable context on provider side Requires ~32K+ tokens; creation often fails if too small

Server cache creation requires a very large system instruction document. Use countTokens() before attempting createServerCache().

Console Commands

Example test commands in console/controllers/TestController.php:

# Stateless generation
php yii test/gemini-none "What is the capital of France?"

# Client-side caching (conversation)
php yii test/gemini-client

# Server-side caching
php yii test/gemini-server "Tell me about Sydney beaches"

# Clear cache
php yii test/gemini-client test-chat 1

Configuration Options

Component Properties
Property Type Default Description
apiKey string required Your Gemini API key (Get one)
baseUrl string https://generativelanguage.googleapis.com/v1/ API base URL
generationConfig array [] Default generation parameters
safetySettings array [] Content safety filters
systemInstruction array|null null Default system instruction
cacheType string 'none' Cache mode: 'none', 'client', 'server'
cacheTtl int 3600 Cache TTL in seconds
cacheComponent string|null 'cache' Yii cache component name
httpConfig array [] Custom HTTP client configuration
Generation Config Options
'generationConfig' => [
    'temperature' => 0.7,          // 0.0-2.0, creativity level
    'topP' => 0.95,                // 0.0-1.0, nucleus sampling
    'topK' => 40,                  // Token selection limit
    'maxOutputTokens' => 2048,     // Max response length
    'stopSequences' => ['END'],    // Stop generation triggers
    'candidateCount' => 1,         // Number of responses
]

Supported Models (Snapshot)

Model Description Context Window
gemini-2.5-pro Most powerful thinking model 2M tokens
gemini-2.5-flash Balanced, fast, 1M context 1M tokens
gemini-2.5-flash-lite Fastest, cost-efficient 1M tokens
text-embedding-004 Text embeddings for RAG N/A

See Gemini Models Documentation for full list.

Canonical Response Format

All methods return:

[
    'ok' => true|false,      // Success status
    'status' => 200,         // HTTP status code
    'data' => [...],         // Response data
    'error' => null|string   // Error message if failed
]
Helper Methods
// Extract text from response
$text = $gemini->extractText($resp['data']);

// Get finish reason ('STOP', 'MAX_TOKENS', 'SAFETY', etc.)
$reason = $gemini->getFinishReason($resp['data']);

// Get usage metadata
$usage = $gemini->getUsageMetadata($resp['data']);
// ['promptTokenCount' => 10, 'candidatesTokenCount' => 50, 'totalTokenCount' => 60]

Helper Methods

$text   = $gemini->extractText($resp['data']);
$reason = $gemini->getFinishReason($resp['data']);
$usage  = $gemini->getUsageMetadata($resp['data']);

Cache Modes (Summary)

None (Stateless)
$gemini->cacheType = 'none';
$resp = $gemini->generateContent('gemini-2.5-flash', 'Hello');
// Each request is independent
Client (Local Conversation History)
$gemini->cacheType = 'client';
$resp = $gemini->chat('gemini-2.5-flash', 'My name is Bob', 'user123');
$resp = $gemini->chat('gemini-2.5-flash', 'What is my name?', 'user123');
// Conversation stored in Yii cache component
Server (Gemini Context Caching)
$gemini->cacheType = 'server';
$cacheName = $gemini->createServerCache(
    'gemini-2.5-flash',
    'id',
    '[Large system instruction 32k+ tokens]',
    3600
);
$resp = $gemini->chatServer('gemini-2.5-flash', 'Question', 'id');
// System instruction cached on Google's servers

Note: Server caching requires minimum 32,000 tokens in cached content.

Error Handling Pattern

$resp = $gemini->generateContent('gemini-2.5-flash', 'Hello');

if (!$resp['ok']) {
    Yii::error("Gemini API error: {$resp['error']} (HTTP {$resp['status']})");
    
    // Common error codes:
    // 400 - Bad request (invalid parameters)
    // 401 - Invalid API key
    // 429 - Rate limit exceeded
    // 500 - Server error
}

Advanced Usage

Custom HTTP Configuration
'gemini' => [
    'class' => 'ldkafka\gemini\Gemini',
    'apiKey' => 'YOUR_KEY',
    'httpConfig' => [
        'timeout' => 60,
        'transport' => 'yii\httpclient\CurlTransport',
    ],
],
Multimodal with Multiple Images
$content = [[
    'parts' => [
        ['text' => 'Compare these images'],
        ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => base64_encode($image1)]],
        ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => base64_encode($image2)]],
    ]
]];
Custom Safety Settings
$gemini->safetySettings = [
    ['category' => 'HARM_CATEGORY_HARASSMENT', 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'],
    ['category' => 'HARM_CATEGORY_HATE_SPEECH', 'threshold' => 'BLOCK_ONLY_HIGH'],
];
Safety Settings Explained

safetySettings lets you tell Gemini which kinds of harmful content to filter and at what strictness. The value is an array of objects with a category and a threshold.

  • Common categories: HARM_CATEGORY_HARASSMENT, HARM_CATEGORY_HATE_SPEECH, HARM_CATEGORY_SEXUALLY_EXPLICIT, HARM_CATEGORY_DANGEROUS_CONTENT, HARM_CATEGORY_VIOLENCE.
  • Typical thresholds (in order of strictness): BLOCK_NONE, BLOCK_LOW_AND_ABOVE, BLOCK_MEDIUM_AND_ABOVE, BLOCK_ONLY_HIGH.

Example JSON payload as sent to the API:

[
    { "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE" },
    { "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_ONLY_HIGH" }
]

Notes:

  • If safetySettings is empty, no custom filters are applied (provider defaults may still apply).
  • You can mix categories with different thresholds.
  • Overly strict settings can block benign answers; adjust to your domain’s tolerance.

Testing

The package includes comprehensive test actions:

  1. actionGeminiNone - Test stateless generation
  2. actionGeminiClient - Test client-side conversation caching
  3. actionGeminiServer - Test server-side context caching

Troubleshooting

"API key not configured"

Ensure your API key is set in the component configuration or params.

"Failed to create server cache"

Server caching requires:

  • Minimum 32,000 tokens in the cached content
  • Supported model (gemini-2.5-flash, gemini-2.5-pro)
  • System instruction or large document

Use client-side caching for shorter conversations.

Streaming not working

Ensure your HTTP client supports Server-Sent Events (SSE). The default Yii2 HTTP client may need custom transport configuration.

Production Notes

  • Implement backoff + retry for 429 & transient 5xx responses.
  • Prune client cache histories when token counts get large (outside of scope for this base component).
  • For server caching: build and store a domain knowledge base (e.g., large markdown/text corpus) and verify token count with countTokens().
  • Log latency and token usage: Yii::info([...], 'gemini') for observability.

Links

License

BSD-3-Clause (matches class header).

Support / Contributing

Open issues or PRs at: https://github.com/ldkafka/yii2-google-gemini

When reporting an issue, include:

  1. PHP / Yii versions
  2. Failing method and sample call
  3. Full response array (mask secrets)
  4. Expected vs actual behavior

Enjoy building with Gemini! Suggestions & improvements welcome.

]]>
0
[extension] rft/yii2-searchable-depdrop Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/rft/yii2-searchable-depdrop https://www.yiiframework.com/extension/rft/yii2-searchable-depdrop zenjaku zenjaku

Searchable & Dependent Dropdown Widget for Yii2

  1. Features
  2. Installation
  3. Usage
  4. Styling Features
  5. Configuration Options
  6. Recent Changes (v1.0.1)
  7. License

A reusable Yii2 widget that provides a searchable dropdown list with support for dependent (cascading) dropdowns.
It is designed to work seamlessly within the wbraganca/yii2-dynamicform widget and has no dependency on any specific CSS framework like Bootstrap.

Note: This package uses the rft\searchabledepdrop\widgets namespace. Make sure to update your imports if you're upgrading from an older version.

Features

  • Searchable dropdown list.
  • Support for dependent dropdowns (e.g., State -> City).
  • Multiple selection support - Allow users to select multiple values.
  • Works with wbraganca/yii2-dynamicform for creating dynamic forms.
  • Framework-independent styling with modern, responsive design.
  • Custom styling for multiple selection with removable tags.
  • Compatible with PHP 5.6+ and modern Yii2 projects.

Installation

Via Composer (Recommended)

The preferred way to install this extension is through Composer.

composer require rft/yii2-searchable-depdrop

Yii2 will automatically load the widget via Composer’s autoloader.

Manual Installation (Alternative)

If you don't want to use Composer, you can still install it manually:

  1. Download the source files from the src/ directory
  2. Place them in your project's widget directory (e.g., common/widgets/searchable_dep_drop/)
  3. Ensure the namespace in the widget files matches the new location if you change it
  4. Include the CSS and JS assets from the src/assets/ directory

Usage

1. Controller Action for Dependent Data

For dependent dropdowns, you need a controller action that returns data in JSON format.
The widget expects the parent value as a POST parameter (the name of the parameter is derived from the parent field's name).

The action should return a JSON object with an output key, which is an array of objects, each having an id and text property.

Example Controller Action:

public function actionListCities()
{
    \Yii::\$app->response->format = \yii\web\Response::FORMAT_JSON;
    $out = ['output' => [], 'selected' => ''];

    if (Yii::\$app->request->post('state')) {
        $state = Yii::\$app->request->post('state');
        if ($state) {
            $cities = AddressCity::find()
                ->where(['state' => $state])
                ->orderBy('name')
                ->all();

            $output = [];
            foreach ($cities as $city) {
                $output[] = ['id' => $city->id, 'text' => $city->name];
            }
            $out['output'] = $output;
        }
    }

    return $out;
}
2. View File Setup

In your view file, you can use the widget like any other Yii2 input widget.

A. Standalone Searchable Dropdown

use rft\searchabledepdrop\widgets\SearchableDepDrop;

echo $form->field($model, 'state')->widget(SearchableDepDrop::class, [
    'data' => [
        'California' => 'California',
        'Texas' => 'Texas',
        // ... other states
    ],
    'placeholder' => 'Select a state...',
]);

B. Multiple Selection Dropdown

use rft\searchabledepdrop\widgets\SearchableDepDrop;

echo $form->field($model, 'tags')->widget(SearchableDepDrop::class, [
    'data' => [
        '1' => 'PHP',
        '2' => 'JavaScript',
        '3' => 'Python',
        '4' => 'Java',
        '5' => 'C#',
        // ... other options
    ],
    'allowMultiple' => true,
    'placeholder' => 'Select multiple technologies...',
]);

C. Dependent Dropdown

Example for a State → City dropdown setup:

use rft\searchabledepdrop\widgets\SearchableDepDrop;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;
use common\models\AddressCity;

// State Dropdown (Parent)
echo $form->field($model, 'state')->widget(SearchableDepDrop::class, [
    'data' => ArrayHelper::map(
        AddressCity::find()->select('state')->distinct()->orderBy('state')->all(),
        'state',
        'state'
    ),
    'options' => [
        'id' => 'address-state',
    ],
    'placeholder' => 'Select a state...',
]);

// City Dropdown (Child)
echo $form->field($model, 'city_id')->widget(SearchableDepDrop::class, [
    'options' => [
        'id' => 'address-city',
    ],
    'placeholder' => 'Select City/Municipality',
    'pluginOptions' => [
        'depends' => ['address-state'],
        'url' => Url::to(['/site/list-cities']),
    ],
])->label('City/Municipality');

Styling Features

The widget comes with built-in CSS that provides:

  • Modern Design: Clean, professional appearance with subtle shadows and borders
  • Responsive Layout: Adapts to different screen sizes and container widths
  • Multiple Selection Tags: Selected items appear as removable tags with:
    • Black background with white text
    • Rounded corners (12px border-radius)
    • Remove button (×) with hover effects
    • Text truncation for long items
  • Search Interface: Dedicated search input with clear visual separation
  • Dropdown Styling:
    • Smooth hover effects
    • Scrollable list (max-height: 200px)
    • Active item highlighting
    • No results message styling
  • Framework Independence: No dependency on Bootstrap or other CSS frameworks
Custom Styling

You can override the default styles by targeting the CSS classes:

/* Main container */
.sdd-container {
  /* Your custom styles */
}

/* Display area */
.sdd-display {
  border: 2px solid #your-color;
  border-radius: 6px;
}

/* Selected items in multiple selection */
.sdd-selected-item {
  background-color: #your-color;
  border-radius: 8px;
}

.sdd-selected-item-container {
  max-width: 100px; /* Adjust tag width */
}

.sdd-item-text {
  font-size: 12px; /* Adjust text size */
}

.sdd-remove-btn {
  color: #your-remove-color;
}

/* Dropdown */
.sdd-dropdown {
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  border-radius: 4px;
}

/* Search input */
.sdd-search {
  padding: 10px 12px;
  font-size: 14px;
}

/* List items */
.sdd-list li {
  padding: 10px 15px;
}

.sdd-list li:hover {
  background-color: #your-hover-color;
}
Available CSS Classes
Class Purpose
.sdd-container Main widget container
.sdd-display Display area showing selected values
.sdd-selected-item Individual selected item tag
.sdd-selected-item-container Container for selected item and remove button
.sdd-item-text Text within selected item
.sdd-remove-btn Remove button (×) for selected items
.sdd-dropdown Dropdown container
.sdd-search Search input field
.sdd-list List of available options
.sdd-active Currently highlighted option
.sdd-no-results No results message

Configuration Options

The widget supports several configuration options:

Option Type Default Description
data array [] Array of options for the dropdown
url string null URL for dependent dropdown data
depends array [] Array of parent field IDs for dependent dropdowns
paramNames array [] Custom parameter names for dependent requests
placeholder string 'Select...' Placeholder text for the dropdown
allowMultiple boolean false Enable multiple selection
rowSelector string '.item-item, .item' CSS selector for dynamic form rows
pluginOptions array [] Additional JavaScript options
3. Dynamic Form Integration

The widget works seamlessly with wbraganca/yii2-dynamicform. Here's how to implement dependent dropdowns in dynamic forms:

Essential Widget Usage:

use rft\searchabledepdrop\widgets\SearchableDepDrop;

// State dropdown (parent)
echo $form->field($addresses[$i], "[{$i}]state")->widget(SearchableDepDrop::classname(), [
    'data' => ArrayHelper::map(
        AddressCity::find()->select('state')->distinct()->orderBy('state')->all(),
        'state', 'state'
    ),
    'placeholder' => 'Select State...',
    'options' => ['class' => 'form-control state-dropdown'],
]);

// City dropdown (child - depends on state)
echo $form->field($addresses[$i], "[{$i}]city_id")->widget(SearchableDepDrop::classname(), [
    'data' => [],
    'placeholder' => 'Select City...',
    'options' => ['class' => 'form-control city-dropdown'],
    'pluginOptions' => [
        'depends' => ['.state-dropdown'],
        'paramNames' => ['state'],
        'url' => Url::to(['/site/cities']),
    ]
]);

Required JavaScript for Dynamic Forms:

function initSearchableDepDrop(context) {
  $(context)
    .find(".sdd-container")
    .each(function () {
      var $container = $(this);
      if ($container.data("searchableDepDrop")) return;

      var optionsJson = $container.data("sdd-options");
      if (optionsJson) {
        var options = typeof optionsJson === "string" ? JSON.parse(optionsJson) : optionsJson;
        $container.searchableDepDrop(options);
      }
    });
}

// Initialize widgets on new form rows
$(".dynamicform_wrapper").on("afterInsert", function (e, item) {
  initSearchableDepDrop(item);
});

// Initialize existing widgets
initSearchableDepDrop(document.body);

Note: Replace .dynamicform_wrapper with your actual widgetContainer class from DynamicFormWidget::begin().

Multiple Selection Example

To enable multiple selection in any dropdown, simply set allowMultiple => true:

// Multiple selection for skills/tags
echo $form->field($model, 'skills')->widget(SearchableDepDrop::classname(), [
    'data' => [
        '1' => 'PHP',
        '2' => 'JavaScript',
        '3' => 'Python',
        '4' => 'Java',
        '5' => 'C#',
        '6' => 'Ruby',
        '7' => 'Go',
        '8' => 'Swift',
    ],
    'allowMultiple' => true,
    'placeholder' => 'Select your skills...',
    'options' => ['class' => 'form-control skills-dropdown'],
]);

// Multiple selection for categories in dynamic form
echo $form->field($contact, "[{$i}]categories")->widget(SearchableDepDrop::classname(), [
    'data' => [
        '1' => 'Business',
        '2' => 'Personal',
        '3' => 'Emergency',
        '4' => 'Family',
        '5' => 'Friend',
    ],
    'allowMultiple' => true,
    'placeholder' => 'Select contact categories...',
    'options' => ['class' => 'form-control categories-dropdown'],
]);

Important Notes:

  • For multiple selection, your model attribute should be an array or JSON field
  • The widget automatically handles the serialization of multiple values
  • Selected items appear as removable tags in the display area
  • The paramNames option is crucial for dependent dropdowns to work properly

Recent Changes (v1.0.1)

Fixed Issues
  • Fixed PSR-4 Autoloading: Reorganized file structure to match namespace requirements
    • Moved SearchableDepDrop.php to src/widgets/SearchableDepDrop.php
    • Moved SearchableDepDropAsset.php to src/widgets/SearchableDepDropAsset.php
    • Updated asset sourcePath to use vendor directory path
    • This resolves the "Class not found" error after composer install
Enhanced Features
  • Improved CSS Styling: Enhanced dropdown list items with text wrapping and visual separation
  • Better Documentation: Added comprehensive styling documentation and usage examples
  • Multiple Selection Support: Full support for selecting multiple values with removable tags
Package Structure
src/
├── widgets/
│   ├── SearchableDepDrop.php
│   └── SearchableDepDropAsset.php
└── assets/
    ├── css/
    │   └── searchable-dep-drop.css
    └── js/
        └── searchable-dep-drop.js
Migration Guide

If you're upgrading from a previous version:

  1. Update your composer package: composer update rft/yii2-searchable-depdrop
  2. The namespace remains the same: use rft\searchabledepdrop\widgets\SearchableDepDrop;
  3. No code changes required in your existing implementations

License

This project is licensed under the MIT License.

]]>
0
[extension] mspirkov/yii2-web Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/mspirkov/yii2-web https://www.yiiframework.com/extension/mspirkov/yii2-web max-s-lab max-s-lab

993323

Yii2 WEB Extension

  1. Installation
  2. Components

A package of helper classes for working with web components in Yii2.

PHP Yii 2.0.x Tests PHPStan Coverage PHPStan Level Max

Installation

Run

php composer.phar require mspirkov/yii2-web

or add

"mspirkov/yii2-web": "^0.1"

to the require section of your composer.json file.

Components

Request

A wrapper for \yii\web\Request for easier handling of GET and POST parameters.

It contains the following methods:

  • getGetInt - gets the value of a GET parameter by its name and tries to convert it to an integer.
  • getGetFloat - gets the value of the GET parameter by its name and tries to convert it to a floating-point number.
  • getGetBool - gets the value of the GET parameter by its name and tries to convert it to a boolean.
  • getGetString - gets the value of the GET parameter by its name and tries to convert it to a string.
  • getGetArray - gets the value of the GET parameter by its name and tries to convert it to an array.
  • getPostInt - gets the value of a POST parameter by its name and tries to convert it to an integer.
  • getPostFloat - gets the value of the POST parameter by its name and tries to convert it to a floating-point number.
  • getPostBool - gets the value of the POST parameter by its name and tries to convert it to a boolean.
  • getPostString - gets the value of the POST parameter by its name and tries to convert it to a string.
  • getPostArray - gets the value of the POST parameter by its name and checks that the value is an array.
Configuration

First, you need to replace the request component in the configuration:

<?php

use MSpirkov\Yii2\Web\Request;

return [
    ...
    'components' => [
        'request' => [
            'class' => Request::class,
            ...
        ],
        ...
    ],
];
IDE Autocomplete (Optional)

You also need to specify this class in __autocomplete.php so that the IDE knows which class to use:

<?php

use yii\BaseYii;
use yii\web\Application;
use MSpirkov\Yii2\Web\Request;

class Yii extends BaseYii
{
    /** @var __Application */
    public static $app;
}

/**
 * @property-read Request $request
 */
class __Application extends Application {}
Basic Controller (Optional)

I also recommend that you create your own basic controller and specify Request there:

use MSpirkov\Yii2\Web\Request;

/**
 * @property Request $request
 */
class Controller extends \yii\web\Controller
{
    public function init(): void
    {
        parent::init();

        $this->request = Instance::ensure($this->request, Request::class);
    }
}
Usage example:
class ProductController extends Controller
{
    public function __construct(
        string $id,
        Module $module,
        private readonly ProductService $service,
        array $config = [],
    ) {
        parent::__construct($id, $module, $config);
    }

    public function actionDelete(): array
    {
        $this->response->format = Response::FORMAT_JSON;

        return $this->service->delete($this->request->getPostInt('id'));
    }
}
CookieManager

A utility class for managing cookies.

This class encapsulates the logic for adding, removing, checking existence, and retrieving cookies, using the \yii\web\Request and \yii\web\Response objects. It simplifies working with cookies by abstracting implementation details and providing more convenient methods.

It contains the following methods:

  • has - checks if a cookie with the specified name exists.
  • get - returns the cookie with the specified name.
  • add - adds a cookie to the response.
  • remove - removes a cookie.
  • removeAll - removes all cookies.
Usage example:
class CookieManager extends \MSpirkov\Yii2\Web\CookieManager
{
    public function __construct()
    {
        parent::__construct(
            Instance::ensure('request', Request::class),
            Instance::ensure('response', Response::class),
        );
    }
}
class ExampleService
{
    public function __construct(
        private readonly CookieManager $cookieManager,
    ) {}

    public function addCookie(): void
    {
        $this->cookieManager->add([
            'name' => 'someCookieName',
            'value' => 'someCookieValue',
        ]);
    }
}
]]>
0
[extension] mspirkov/yii2-db Sun, 02 Nov 2025 12:46:55 +0000 https://www.yiiframework.com/extension/mspirkov/yii2-db https://www.yiiframework.com/extension/mspirkov/yii2-db max-s-lab max-s-lab

993323

Yii2 DB Extension

  1. Installation
  2. Components

A package of helper classes for working with databases in Yii2.

PHP Yii 2.0.x Tests PHPStan Coverage PHPStan Level Max

Installation

Run

php composer.phar require mspirkov/yii2-db

or add

"mspirkov/yii2-db": "^0.2"

to the require section of your composer.json file.

Components

AbstractRepository

An abstract class for creating repositories that interact with ActiveRecord models. Contains the most commonly used methods: findOne, findAll, save and others. It also has several additional methods: findOneWith, findAllWith.

This way, you can separate the logic of executing queries from the ActiveRecord models themselves. This will make your ActiveRecord models thinner and simpler. It will also make testing easier, as you can mock the methods for working with the database.

Usage example:
/**
 * @extends AbstractRepository<Customer>
 */
class CustomerRepository extends AbstractRepository
{
    public function __construct()
    {
        parent::__construct(Customer::class);
    }
}
class CustomerService
{
    public function __construct(
        private readonly CustomerRepository $customerRepository,
    ) {}

    public function getCustomer(int $id): ?Customer
    {
        return $this->customerRepository->findOne($id);
    }
}
DateTimeBehavior

Behavior for ActiveRecord models that automatically fills the specified attributes with the current date and time.

By default, this behavior uses the current date, time, and time zone. If necessary, you can specify your own attributes and time zone.

Usage example:
/**
 * @property int $id
 * @property string $content
 * @property string $created_at
 * @property string|null $updated_at
 */
class Message extends ActiveRecord
{
    public static function tableName(): string
    {
        return '{{messages}}';
    }

    public function behaviors(): array
    {
        return [
            DateTimeBehavior::class,
        ];
    }
}
TransactionManager

A utility class for managing database transactions with a consistent and safe approach.

This class simplifies the process of wrapping database operations within transactions, ensuring that changes are either fully committed or completely rolled back in case of errors.

It provides two main methods:

  • safeWrap - executes a callable within a transaction, safely handling exceptions and logging them.
  • wrap - executes a callable within a transaction.
Usage example:
class TransactionManager extends \MSpirkov\Yii2\Db\TransactionManager
{
    public function __construct()
    {
        parent::__construct(Yii::$app->db);
    }
}
class ProductService
{
    public function __construct(
        private readonly TransactionManager $transactionManager,
        private readonly ProductFilesystem $productFilesystem,
        private readonly ProductRepository $productRepository,
    ) {}

    /**
     * @return array{success: bool, message?: string}
     */
    public function deleteProduct(int $id): array
    {
        $product = $this->productRepository->findOne($id);

        // There's some logic here. For example, checking for the existence of a product.

        $transactionResult = $this->transactionManager->safeWrap(function () use ($product) {
            $this->productRepository->delete($product);
            $this->productFilesystem->delete($product->preview_filename);

            return [
                'success' => true,
            ];
        });

        if ($transactionResult === false) {
            return [
                'success' => false,
                'message' => 'Something went wrong',
            ];
        }

        return $transactionResult;
    }
}
]]>
0
[extension] dacheng-php/yii2-swoole Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/dacheng-php/yii2-swoole https://www.yiiframework.com/extension/dacheng-php/yii2-swoole dacheng-gao dacheng-gao ]]> 0 [extension] neoacevedo/yii2-fastly-cache Tue, 28 Oct 2025 16:22:26 +0000 https://www.yiiframework.com/extension/neoacevedo/yii2-fastly-cache https://www.yiiframework.com/extension/neoacevedo/yii2-fastly-cache NestorAcevedo NestorAcevedo ]]> 0 [wiki] Yii3 - How to start Thu, 16 Oct 2025 07:27:01 +0000 https://www.yiiframework.com/wiki/2581/yii3-how-to-start https://www.yiiframework.com/wiki/2581/yii3-how-to-start rackycz rackycz

(draft - all will be retested later)

Intro

  1. PSR Standards by Framework Interoperability Group
  2. Dependency injection + container
  3. invoke()
  4. Hash annotations for class attributes
  5. Running the demo application
  6. Adding DB into your project
  7. Enabling MariaDB (MySQL) and migrations
  8. Creating a migration
  9. Running the migrations
  10. Reading data from DB
  11. Seeding the database
  12. Using Repository and the Model class
  13. API login + access token
  14. JS client - Installable Vuejs3 PWA

In Yii3 it is not as easy to start as it was with Yii2. You have to install and configure basic things on your own. Yii3 uses the modern approach based on independent packages and dependency injection, but it makes it harder for newcomers. I am here to show all how I did it.

All the code is available in my new GitHub repository. I will be using it as a boiler-plate for my future projects so it should be always up-to-date and working.

Instead of installing local WAMP- or XAMPP-server I will be using Docker. Do not forget about a modern IDE like PhpStorm, which comes bundled with all you will ever need.

PSR Standards by Framework Interoperability Group

First of all, learn what PHP Standards Recommendations by Framework Interoperability Group (FIG) are. It will help you understand why so many "weird" PSR imports are in the Yii3 code. In short: These interfaces help authors of different frameworks to write compatible classes so they can be reused in any other framework following these principles.

Dependency injection + container

Check this YouTube video for explanation

invoke()

The __invoke() public method is called when you call the instance as a method. (Therefore the constructor was already executed)

$obj = new MyObj(); // Now the __construct() is executed.
$obj(); // Now the __invoke() is executed (The instance is needed!)

I never used it, but prepared a following example that shows when invoking can be applied:

class MyUpper
{
    public function __invoke($a) { return $this->go($a); }
    public function go($a) { return strtoupper($a); }
}
$instance = new MyUpper();
$array = ['a', 'B', 1, '1'];

// __invoke is used:
var_dump($instance($array[0])); 
var_dump(array_map($instance, $array));

// These do the same without invoking:
var_dump(array_map('strtoupper', $array));
var_dump(array_map([$instance, 'go'], $array));
var_dump(array_map(function($a) use ($instance) { return $instance->go($a); }, ['a','B',1,'1']));

Hash annotations for class attributes

PHP 8 introduces annotations like this (not only for class attributes):

  • #[Column(type: 'primary')]
  • #[Column(type: 'string(255)', nullable: true)]
  • #[Entity(repository: UserRepository::class)]
  • #[ManyToMany(target: Role::class, through: UserRole::class)]

They should replace the original DocBlock annotatinos and provide more new functionalities.

Learn what they mean and how they are used by Yii3. To me this is a brand new topic as well.

Yii3 - How to start

Yii3 offers more basic applications: Web, Console, API. I will be using the API application:

Clone it like this:

.. and follow the docker instructions in the documentation.

If you don't have Docker, I recommend installing the latest version of Docker Desktop:

Running the demo application

You may be surprised that docker-compose.yml is missing in the root. Instead the "make up" and "make down" commands are prepared. If you run both basic commands as mentioned in the documentation:

  • make composer update
  • make up

... then the web will be available on URL

If you want to modify the data that was returned by the endpoint, just open the action-class src/Api/IndexAction.php and add one more element to the returned array.

You may be missing 'docker compose stop' or 'make stop', because 'make down' removes your containers and drops your DB. In that case you can add it to the Makefile in the root (see below). If you then type 'make help' you will see the new command.

ifeq ($(PRIMARY_GOAL),stop)
stop: ## Stop the dev environment
	$(DOCKER_COMPOSE_DEV) stop
endif

Adding DB into your project

Your project now does not contain any DB. Let's add MariaDB and Adminer (DB browser) into file docker/dev/compose.yml:

In my case the resulting file looks like this:

services:
  app:
    container_name: yii3api_php
    build:
      dockerfile: docker/Dockerfile
      context: ..
      target: dev
      args:
        USER_ID: ${UID}
        GROUP_ID: ${GID}
    env_file:
      - path: ./dev/.env
      - path: ./dev/override.env
        required: false
    restart: unless-stopped
    depends_on:
      - db
    ports:
      - "${DEV_PORT:-80}:80"
    volumes:
      - ../:/app
      - ../runtime:/app/runtime
      - caddy_data:/data
      - caddy_config:/config
    tty: true
  db:
    image: mariadb:12.0.2-noble
    container_name: yii3api_db
    environment:
      MARIADB_ROOT_PASSWORD: root
      MARIADB_DATABASE: db
      MARIADB_USER: db
      MARIADB_PASSWORD: db
  adminer:
    image: adminer:latest
    container_name: yii3api_adminer
    environment:
      ADMINER_DEFAULT_SERVER: db
    ports:
      - ${DEV_ADMINER_PORT}:8080
    depends_on:
      - db
volumes:
  mariadb_data:

Plus add/modify these variables in file docker/.env

  • DEV_PORT=9080
  • DEV_ADMINER_PORT=9081

Then run following commands:

  • make down
  • make build
  • make up

Now you should see a DB browser on URL http://localhost:9081/?server=db&username=db&db=db

Login, server and pwd is defined in the snippet above.

If you type "docker ps" into your host console, you should see 3 running containers: yii3api_php, yii3api_adminer, yii3api_db.

The web will be, from now on, available on URL http://localhost:9080 which is more handy than just ":80" I think. (Later you may run 4 different projects at the same time and all cannot run on port 80)

Enabling MariaDB (MySQL) and migrations

Now when your project contains MariaDB, you may wanna use it in the code ...

Installing composer packages

After some time of searching you will discover you need to install these composer packages:

So you need to run following commands:

composer require yiisoft/db-mysql
composer require yiisoft/cache
composer require yiisoft/db-migration

To run composer (or any other command inside your dockerized yii3 application) you have 4 options:

  • Make: The best solution is to prepend the composer commands with "make".

Other solutions:

  • If you have Composer running locally, you can call these commands directly on your computer. (I do not recommend)

  • You can SSH into your docker container and call it there as Composer is installed inside. In that case:

    • Find the name of the PHP container by typing "docker ps"
    • Call "docker exec -it {containerName} /bin/bash"
    • Now you are in the console of your php server and you can run composer.
  • If you are using PhpStorm, find the small icon "Services" in the left lower corner (looks ca like a cog wheel), find item "Docker-compose: app-api", inside click the "app" service, then "yii3api_php" container and then hit the button "terminal" on the righthand side.

Setting up composer packages

Follow their documentations. Quick links:

The documentations want you to create 2 files:

  • config/common/di/db-mysql.php
  • config/common/db.php
  • But you actually need only one. I recommend db-mysql.php

Note: If you want to create a file using commandline, you can use command "touch". For example "touch config/common/di/db-mysql.php"

Note: In the documentation the PHP snippets do not contain tag and declaration. Prepend it:

<?php
declare(strict_types=1);
Create folder for migrations
  • src/Migration

When this is done, call "composer du" or "make composer du" and then try "make yii list". You should see the migration commands.

Creating a migration

Run the command to create a migration:

  • make yii migrate:create user

Open the file and paste following content to the up() method:

$b->createTable('user', [
'id' => $b->primaryKey(),
'name' => $b->string()->notNull(),
'surname' => $b->string()->notNull(),
'username' => $b->string(),
'email' => $b->string()->notNull()->unique(),
'phone' => $b->string(),
'admin_enabled' => $b->boolean()->notNull()->defaultValue(false)->comment('Can user access the administration?'),
'vuejs_enabled' => $b->boolean()->notNull()->defaultValue(false)->comment('Can user access the mobile application?'),
'auth_key' => $b->string(32)->notNull()->unique(),
'access_token' => $b->string(32)->unique()->comment('For API purposes'),
'password_hash' => $b->string(),
'password_default' => $b->string(),
'password_vuejs_default' => $b->string(),
'password_vuejs_hash' => $b->string(),
'password_reset_token' => $b->string()->unique(),
'verification_token' => $b->string()->unique(),
'verified_at' => $b->dateTime(),
'status' => $b->smallInteger()->notNull()->defaultValue(100),
'created_by' => $b->integer(),
'updated_by' => $b->integer(),
'deleted_by' => $b->integer(),
'created_at' => $b->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
'updated_at' => $b->dateTime(),
'deleted_at' => $b->dateTime(),
]);

The down() method should contain this:

$b->dropTable('user');

Running the migrations

Try to run "make yii migrate:up" and you will see error "could not find driver", because file "docker/Dockerfile" does not install the "pdo_mysql" extention. Add it to the place where "install-php-extensions" is called.

Then call:

  • make down
  • make build
  • make up

Now you will see error "Connection refused" It means you have to update dns, user and password in file "config/common/params.php" based on what is written in "docker/dev/compose.yml".

If you run "make yii migrate:up" it should work now and your DB should contain the first table. Check it via adminer: http://localhost:9081/?server=db&username=db&db=db

Reading data from DB

In Yii we were always using ActiveRecord and its models, but in Yii3 the package is not ready yet. The solution is to use existing class Yiisoft\Db\Query\Query.

Open class src/Api/IndexAction.php and modify it a little to return all users via your REST API. You have more options:

You can manually instantiate the Query object, but you need to provide the DB connection manually:

declare(strict_types=1);
namespace App\Api;
use App\Api\Shared\ResponseFactory;
use App\Shared\ApplicationParams;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Db\Query\Query;

final class IndexAction
{
    public function __invoke(
        ResponseFactory     $responseFactory,
        ApplicationParams   $applicationParams,
        ConnectionInterface $db,
    ): ResponseInterface
    {
        $query = (new Query($db))
            ->select('*')
            ->from('user');
        return $responseFactory->success($query->all());
    }
}

Or you can use the DI container to provide you with the instance. I like this better as I can omit input parameters:

declare(strict_types=1);
namespace App\Api;
use App\Api\Shared\ResponseFactory;
use App\Shared\ApplicationParams;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Db\Query\Query;

final class IndexAction
{
    public function __invoke(
        ResponseFactory    $responseFactory,
        ApplicationParams  $applicationParams,
        ContainerInterface $container,
    ): ResponseInterface
    {
        $query = $container->get(Query::class)
            ->select('*')
            ->from('user');
        return $responseFactory->success($query->all());
    }
}

Now you can call the URL and see all the users. (If you entered some) http://localhost:9080

Note: You can also use Injector (and method $injector->make()) instead of ContainerInterface (and method $container->get()). Injector seems to allow you to pass input arguments if needed.

PS: The input parameter of new Query(ConnectionInterface $db) is automatically provided as it is defined in DI. See the file you created earlier above: config/common/di/db-mysql.php

Seeding the database

Seeding = inserting fake data.

You can technically create a migration or a command and insert random data manually. But you can also use the Faker. In that case I needed following dependencies:

composer require fakerphp/faker
composer require yiisoft/security (not only for generating random strings)

Now find the class HelloCommand.php, copy and rename it to SeedCommand.php

Inside you will need the instance of ConnectionInterface. It can be automatically provided by the DI (because you defined it in config/common/di/db-mysql.php), you only need to create a new constructor and then use the instance in method execute():

namespace App\Console;

use Faker\Factory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Security\Random;
use Yiisoft\Yii\Console\ExitCode;

#[AsCommand(
    name: 'seed',
    description: 'Run to seed the DB',
)]
final class SeedCommand extends Command
{
    public function __construct(
        private readonly ConnectionInterface $db
    )
    {
        parent::__construct();
    }

    protected function execute(
        InputInterface  $input,
        OutputInterface $output
    ): int
    {

        $faker = Factory::create();

        for ($i = 0; $i < 10; $i++) {
            $this->db->createCommand()
                ->insert('user', [
                    'name' => $faker->firstName(),
                    'surname' => $faker->lastName(),
                    'username' => $faker->userName(),
                    'email' => $faker->email(),
                    'auth_key' => Random::string(32),
                ])
                ->execute();
        }

        $output->writeln('Seeding DONE.');

        return ExitCode::OK;
    }
}

Register the new command in file config/console/commands.php.

You can also obtain the ConnectionInterface in the same way as you did it in IndexAction with the Query object. Just use ContainerInterface $container in the constructor instead of ConnectionInterface $db. Then you can call $db = $this->container->get(ConnectionInterface::class);.

Using Repository and the Model class

Each entity should have its Model class and Repository class if you are storing it in DB. Have a look at the demo application "blog-api": https://github.com/yiisoft/demo

In my case the User model (file src/Entity/User.php) will only contain private attributes, setters and getters. UserRepository (placed in the same folder) may look like this to enable CRUD (compressed code):

<?php
declare(strict_types=1);
namespace App\Entity;
use DateTimeImmutable;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Db\Exception\Exception;
use Yiisoft\Db\Exception\InvalidConfigException;
use Yiisoft\Db\Query\Query;
final class UserRepository
{
    public const TABLE_NAME = 'user';
    public function __construct(private readonly ConnectionInterface $db){}
    public function findAll(array $orderBy = [], $asArray = false): array
    {
        $query = (new Query($this->db))->select('*')->from(self::TABLE_NAME)->orderBy($orderBy ?: ['created_at' => SORT_DESC]);
        if ($asArray) {
            return $query->all();
        }
        return array_map(
            fn(array $row) => $this->hydrate($row),
            $query->all()
        );
    }
    public function findBy(string $attr, mixed $value): ?User
    {
        $row = (new Query($this->db))->select('*')->from(self::TABLE_NAME)->where([$attr => $value])->one();
        return $row ? $this->hydrate($row) : null;
    }
    public function findByUsername(string $username): ?User
    {
        return $this->findBy('username', $username);
    }
    public function save(User $user): void
    {
        $data = ['name' => $user->getName(), 'surname' => $user->getSurname(), 'username' => $user->getUsername(), 'email' => $user->getEmail(), 'auth_key' => $user->getAuthKey()];
        if ($user->getId() === null) {
            $data['created_at'] = (new DateTimeImmutable())->format('Y-m-d H:i:s');
            $this->db->createCommand()->insert(self::TABLE_NAME, $data)->execute();
        } else {
            $this->db->createCommand()->update(self::TABLE_NAME, $data, ['id' => $user->getId()])->execute();
        }
    }
    public function delete(int $id): bool
    {
        try {
            $this->db->createCommand()->delete(self::TABLE_NAME, ['id' => $id])->execute();
        } catch (\Throwable $e) {
            return false;
        }
        return true;
    }
    private function hydrate(array $row): User
    {
        $user = new User();
        $reflection = new \ReflectionClass($user);
        $this->hydrateAttribute($reflection, $user, 'id', (int) $row['id']);
        $this->hydrateAttribute($reflection, $user, 'name', ($row['name']));
        $this->hydrateAttribute($reflection, $user, 'surname', $row['surname']);
        $this->hydrateAttribute($reflection, $user, 'username', $row['username']);
        $this->hydrateAttribute($reflection, $user, 'email', $row['email']);
        $this->hydrateAttribute($reflection, $user, 'created_at', new DateTimeImmutable($row['created_at']));
        $this->hydrateAttribute($reflection, $user, 'updated_at', new DateTimeImmutable($row['updated_at'] ?? ''));
        return $user;
    }
    private function hydrateAttribute(\ReflectionClass $reflection, object $obj, string $attribute, mixed $value)
    {
        $idProperty = $reflection->getProperty($attribute);
        $idProperty->setAccessible(true);
        $idProperty->setValue($obj, $value);
    }
}

Now you can modify IndexAction to contain this: (read above to understand details)

// use App\Entity\UserRepository;
$userRepository = $container->get(UserRepository::class);
return $responseFactory->success($userRepository->findAll([], true));

API login + access token

Once user logs in you want to create an access-token. Why? Because in APIs the PHP session is not used, so users would have to send their login in every request, which would be a potential risk. So random strings with limited lifetime are generated and users send them in their requests intstead of the login. After a few minutes or hours the access token expires and a new one must be created. Each user can have more tokens for different situations. Details here: https://goteleport.com/learn/authentication-and-authorization/simple-random-tokens-secure-authentication/

Below I am indicating how to implement "Random Token Authentication". Other options would be:

  • JWT (JSON Web Token) .. I see some disadvatages
  • OAuth, OAuth2 - too complex for a simple API

Before you start, install dependency:

composer require yiisoft/security

Let's create a migration for storing the access tokens:

// method up():
$b->createTable('user_token', [
    'id' => $b->primaryKey(),
    'id_user' => $b->integer()->notNull(),
    'token' => $b->string()->notNull()->unique(),
    'expires_at' => $b->dateTime()->notNull(),
    'created_at' => $b->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
    'updated_at' => $b->dateTime(),
    'deleted_at' => $b->dateTime(),
]);

Then create a model App\Entity\UserToken. It again contains only private properties, getters and setters. Plus I added __construct() and toArray():

// Uglified code:
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'integer')]
private int $id_user;
#[Column(type: 'string(255)', default: '')]
private string $token = '';
#[Column(type: 'datetime')]
private DateTimeImmutable $expires_at;
#[Column(type: 'datetime', nullable: true)]
private ?DateTimeImmutable $created_at;
#[Column(type: 'datetime', nullable: true)]
private ?DateTimeImmutable $updated_at;
#[Column(type: 'datetime', nullable: true)]
private ?DateTimeImmutable $deleted_at;
public function __construct(int $userId, string $token, DateTimeImmutable $expiresAt = null)
{
    $this->id_user = $userId;
    $this->token = $token;
    $this->expires_at = $expiresAt;
}
public function toArray(): array
{
    return [
        'id' => $this->id,
        'id_user' => $this->id_user,
        'token' => $this->token,
        'expires_at' => $this->expires_at->format('Y-m-d H:i:s'),
    ];
}

Then you will also need class App\Entity\UserTokenRepository for DB manipulation. Copy and modify the UserRepository. These methods will be handy:

public function findByToken(string $token): ?UserToken
{
    $tokenEntity = $this->findBy('token', $token);
    if (!$tokenEntity) {
        return null;
    }
    if ($tokenEntity->getExpiresAt() < new DateTimeImmutable()) {
        // Optionally delete expired token
        $this->delete($tokenEntity->getId());
        return null;
    }
    return $tokenEntity;
}
public function create(int $userId, ?string $token = null, ?DateTimeImmutable $expiresAt = null, $lifespan = '+2 hours'): UserToken
{
    if (!$token) {
        $token = bin2hex(Random::string(32));
        // Example: 654367506342505647634a6f4c6945784d793447355048734b364a4e62483743
    }
    if (!$expiresAt) {
        $expiresAt = (new DateTimeImmutable())->modify($lifespan);
    }
    $entity = new UserToken($userId, $token, $expiresAt);
    $this->db->createCommand()
        ->insert(self::TABLE_NAME, $entity->toArray())
        ->execute();
    return $entity;
}

The User model will need one more method:

// use Yiisoft\Security\PasswordHasher;
public function validatePassword(string $password): bool
{
    return (new PasswordHasher())->validate($password, $this->password_vuejs_hash);
}

In the end you can create the login action. Register it again in config/common/routes.php.

<?php
declare(strict_types=1);
namespace App\Api;
use App\Api\Shared\ResponseFactory;
use App\Entity\UserRepository;
use App\Entity\UserTokenRepository;
use App\Shared\ApplicationParams;
use DateTimeImmutable;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\DataResponse\DataResponse;
use Yiisoft\Http\Status;
final class LoginAction
{
    public function __construct(
        private UserRepository      $userRepository,
        private UserTokenRepository $userTokenRepository,
    ){}
    public function __invoke(
        ResponseFactory        $responseFactory,
        ApplicationParams      $applicationParams,
        ContainerInterface     $container,
        ServerRequestInterface $request
    ): ResponseInterface
    {
        $data = json_decode((string) $request->getBody(), true);
        $username = $data['username'] ?? '';
        $password = $data['password'] ?? '';
        $user = $this->userRepository->findByUsername($username);
        if (!$user || !$user->validatePassword($password)) {
            return new DataResponse(['error' => 'Invalid credentials'], Status::UNAUTHORIZED);
        }
        $this->userTokenRepository->deleteByUserId($user->getId());
        $userToken = $this->userTokenRepository->create($user->getId());
        return $responseFactory->success([
            'token' => $userToken->getToken(),
            'expires_at' => $userToken->getExpiresAt()->format(DateTimeImmutable::ATOM),
        ]);
    }
}

Next we also need an algorithm that will enforce these tokens in each request, will validate and refresh them and will restrict access only to endpoints that the user can use. This is a bigger topic for later. It may be covered by the package https://github.com/yiisoft/auth/ which offers "HTTP bearer authentication".

JS client - Installable Vuejs3 PWA

If you create a REST API you may be interested in a JS frontend that will communicate with it using Ajax. Below you can peek into my very simple VueJS3 attempt. It is an installable PWA application that works in offline mode (=1 data transfer per day, not on every mouse click) and is meant for situations when customer does not have wifi everywhere. See my Gitlab.

]]>
0
[extension] fenomviewrenderer Thu, 11 Sep 2025 19:42:18 +0000 https://www.yiiframework.com/extension/fenomviewrenderer https://www.yiiframework.com/extension/fenomviewrenderer WinterSilence WinterSilence
  1. Installation
  2. Usage

Yii 2 extension for render views using Fenom template engine.

Installation

composer require ensostudio/yii2-fenom

Usage

You can add a custom template engine by reconfiguring view component's behavior:

[
    'components' => [
        'view' => [
            'class' => yii\web\View::class,
            'renderers' => [
                'tpl' => [
                    'class' => ensostudio\yii2fenom\FenomViewRenderer::class,
                    // customize renderer options,
                ],
            ],
        ],
    ],
]

See https://www.yiiframework.com/doc/guide/2.0/en/tutorial-template-engines

]]>
0
[extension] sahmed237/yii2-admin-theme Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/sahmed237/yii2-admin-theme https://www.yiiframework.com/extension/sahmed237/yii2-admin-theme s.ahmed s.ahmed ]]> 0 [extension] jonatas-sas/yii2-m2m-behavior Wed, 23 Apr 2025 14:03:03 +0000 https://www.yiiframework.com/extension/jonatas-sas/yii2-m2m-behavior https://www.yiiframework.com/extension/jonatas-sas/yii2-m2m-behavior jonatas-sas jonatas-sas ]]> 0 [extension] andy87/yii2-file-crafter Fri, 28 Mar 2025 19:26:36 +0000 https://www.yiiframework.com/extension/andy87/yii2-file-crafter https://www.yiiframework.com/extension/andy87/yii2-file-crafter andy87 andy87

Yii2 File Crafter

Yii2 File Crafter - library for generating a many templates with minimal differences

Content

Setup

Requirements
  • php >=8.0
  • Yii2
Composer

Add package to project

Using console

(Recommended)

  • Composer ( global setup )
    composer require andy87/yii2-file-crafter:dev-master --dev
    ````  
    
  • Composer.phar ( local setup )
    php composer.phar require andy87/yii2-file-crafter:dev-master --dev
    

Using: file composer.json

Open file composer.json, in section with key require add line:
"andy87/yii2-file-crafter": "dev-master"

dont forget update composer `bash composer update `

- - - - -

Config

Config in the configuration file.

  • basic:config/(web|web-local|local).php
  • advanced:(frontend|backend)/config/main-local.php

Minimum config `php $config['modules']['gii'] = [

'class' => yii\gii\Module::class,
'generators' => [
    'fileCrafter' => [
        'class' => andy87\yii2\file_crafter\Crafter::class,
        'options' => [
            'templates' => [
                'group_name' => [
                    // 'template' => 'path/to/file.php',
                    'common/services/PascalCaseService' => '@app/common/services/items/{PascalCase}Service.php',
                    'template/test/unit/camelCaseService.tpl' => '@backend/test/unit/{{camelCase}}Service.php',
                    'templates/view/index.php' => 'custom/dir/{{snake_case}}/index.php',
                ]
            ]
        ]
    ]
],

]; `

Full Config with all options `php $config['modules']['gii'] = [

'class' => yii\gii\Module::class,
'generators' => [
    'fileCrafter' => [
        'class' => andy87\yii2\file_crafter\Crafter::class,
        'options' => [
            'cache' => [
                'dir' => '@runtime/yii2-file-crafter/cache',
                'ext' => '.tpl'
            ],
            'source' => [
                'dir' => '@runtime/yii2-file-crafter/templates/source',
                'ext' => '.tpl'
            ],
            'custom_fields' => [
                'singular' => 'label - one',
                'plural' => 'label - many',
           ],
            'commands' => [
                'php yii gii/model --tableName={{snake_case}} --modelClass={{PascalCase}} --ns="common\models" --interactive=0' //... 
            ],
            'eventHandler' => app\composents\behavior\FileCrafterBehavior::class,
            'autoCompleteStatus' => true,
            'autoCompleteList' => [
                'autocomplete name 1',
                'autocomplete name 2',
            ],
            'previewStatus' => true,
            'canDelete' => true,
            'parseDataBase' => ['autocomplete','fakeCache'],
            'templates' => [
                'common' => [
                    'common/services/PascalCaseService' => 'app/common/services/items/{[PascalCase]}Service.php',
                ],
                'backend' => [
                    'backend/test/unit/camelCaseService.tpl' => 'backend/test/unit/{{camelCase}}Service.php',
                ],
                'frontend' => [
                    'frontend/view/index.php' => 'app/frontend/view/{{snake_case}}/index.php',
                ],
                'all' => [
                    'common/services/PascalCaseService' => 'app/common/services/items/{PascalCase}Service.php',
                    'backend/test/unit/camelCaseService.tpl' => 'backend/test/unit/{{camelCase}}Service.php',
                    'frontend/view/index.php' => 'app/frontend/view/{{snake_case}}/index.php',
                ]
            ],
        ]
    ]
],

]; `

Using


Marks

Module use marks for replace variables in templates.

  • {{PascalCase}} - PascalCase by schema name
  • {{camelCase}} - camelCase by schema name
  • {{snake_case}} - snake_case by schema name
  • {{kebab-case}} - kebab-case by schema name
  • {{UPPERCASE}} - UPPERCASE by schema name
  • {{lowercase}} - lowercase by schema name
  • {{[key]}} - custom key from property custom_fields on config ( see Custom Fields )
Example

for schema name Product Items replace marks:

  • {{PascalCase}} - ProductItems
  • {{camelCase}} - productItems
  • {{snake_case}} - product_items
  • {{kebab-case}} - product-items
  • {{UPPERCASE}} - PRODUCT ITEMS
  • {{lowercase}} - product items

Cache
<small style="color: #009; font-size:9px">(optional)</small>

Configuration for the cache folder with schema data.

  • dir - path to the cache directory with schema data
  • ext - extension of the cache file

Default configuration: `php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'cache' => [
                'dir' => '@runtime/yii2-file-crafter/cache',
                'ext' => '.json'
            ],
            // ...
        ],
    ],
]

]; `

Source
<small style="color: #009; font-size:9px">(optional)</small>

Configuration for the source folder with templates files.

  • dir - path to the directory with the templates files source for generation
  • ext - extension of the templates file

Default configuration: `php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'source' => [
                'dir' => '@runtime/yii2-file-crafter/templates/source',
                'ext' => '.tpl'
            ],
            // ...
        ],
    ],
]

]; `

Templates
<small style="color: #900; font-size:9px">(required)</small>

Array with groups of templates for use on generate files.
`php [

['group1'] => [
    'template1' => 'path/from/project/root/to/resultFile.tpl',
    'template2.tpl' => 'path/from/project/root/to/resultFile.php',
    // ...
],
['group2'] => [
    'template1.php' => '@path/alias/to/resultFile.tpl',
    '@alias/to/template' => 'path/from/project/root/to/resultFile.php',
    // ...
],

] ` The source path may contain:

  • some @ alias ( source['dir'] - default container )
  • ext for generate any file type ( .php default )
  • some {{variable}} ( see Marks )

File source-template will be searched in the source folder.
Source folder path can be set in the configuration file. ( see Source )

The resultFile path may contain:

  • some @ alias ( @app/ - default prefix )
  • ext for generate any file type ( .php default )
  • some {{variable}} ( see Marks )

Content of the templates file rendered with the View method renderFile
And prepared with the $replaceList array contains all marks. ( see Marks )
And also passed to the render method:

  • $schema - schema object
  • $generator - self generator object
$config['modules']['gii'] = [
    'class' => Module::class,
        'generators' => [
            'fileCrafter' => [
            'options' => [
                // ... 
                'templates' => [
                    'all' => [
                        '@backend/dir/by/alias/camelCaseService.tpl' => '@backend/generate/by/alias/{{camelCase}}Service.php',
                        'dir/on/source/dir/generate_file' => 'custom/dir/on/source/dir/{{snake_case}}/generate_file.tpl',
                    ],
                ],
                // ...
            ],
        ],
    ]
];

Custom Fields
<small style="color: #009; font-size:9px">(optional)</small>

Array with custom fields for use custom variables in templates.
Using on template key wrapped in square brackets: {{%key%}}
Example: {{key_one}}, {{key_two}}...

Example simple config
`php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'custom_fields' => [
                'singular' => 'one',
                'plural' => 'many',
            ],
            // ...
        ],
    ],
]

]; with template:php Value - ONE = {{singular}} Value - MANY = ({{plural}}) `

___

Schema 1: Product Items
Field one = !!product!!
Field many = >>> products <<<

...generate...

Result: app/frontend/views/info--product_items.php `php Value - ONE = !!product!! Value - MANY = (>>> products <<<) `

___

Schema 2: Category Group
Field label one = --category--
Field label many = +++categories+++

...generate...

Result: app/frontend/views/info--category_group.php `php Value - ONE = --category-- Value - MANY = (+++categories+++) `

Autocomplete status

Key autoCompleteStatus contain status for autocomplete field Schema name in the form 200 populated values.

Variants: true or false(default)
`php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'autoCompleteStatus' => true,
            // ...
        ],
    ],
],

]; `

Autocomplete list
<small style="color: #009; font-size:9px">(optional)</small>

Key autoCompleteList contain list of autocomplete field Schema name in the form self custom list.

Type: array
`php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'autoCompleteList' => [
                'Product Items',
                'Category Group',
                'User Profile',
                // ...
            ],
            // ...
        ],
    ],
],

]; `

<h2 align="center">Preview status
<small style="color: #009; font-size:9px">(optional)</small> 

Key previewStatus contain status for preview file content on hover icon in the form.

Variants: true(default) or false
`php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'previewStatus' => true,
            // ...
        ],
    ],
],

]; `

<h2 align="center">Can delete
<small style="color: #009; font-size:9px">(optional)</small> 

Key canDelete contain status for delete schema from the form.

Variants: true(default) or false `php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'canDelete' => true,
            // ...
        ],
    ],
],

]; `

Parse data base
<small style="color: #009; font-size:9px">(optional)</small>

Key parseDataBase contain list of target for extend schema name list from database.

Variants: array with values:

  • autocomplete
  • fakeCache

Default empty; `php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'parseDataBase' => ['autocomplete','fakeCache'],
            // ...
        ],
    ],
],

]; `

Commands
<small style="color: #009; font-size:9px">(optional)</small>

Key commands contain list cli command for call before generate any file.
command make use of the {{variable}} in the command string ( see Marks )

Example: generate gii model for table name from schema name before generate fileContent

Default empty; `php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
        'fileCrafter' => [
        'options' => [
            // ... 
            'commands' => [
                'php yii gii/model --tableName={{snake_case}} --modelClass={{PascalCase}} --ns="common\models"  --interactive=0 --overwrite=1' // ... 
            ],
            // ...
        ],
    ],
],

]; `

Events
<small style="color: #009; font-size:9px">(optional)</small>

Make use of the eventHandlers key to add a behavior to the module.

Example: add behavior FileCrafterBehavior to the module

Default null; `php $config['modules']['gii'] = [

'class' => Module::class,
    'generators' => [
    'options' => [
            // ... 
            'eventHandlers' => FileCrafterBehavior::class,
            // ...
        ],
    ],
],

]; `

Before init

CrafterEvent::BEFORE_INIT before init module

After init

CrafterEvent::AFTER_INIT after init module

Come events has special properties...

Before generate

CrafterEventGenerate::BEFORE before generate all files

//class FileCrafterBehavior extends Behavior
public function beforeGenerate(CrafterEventGenerate $crafterEventGenerate): void {
    Yii::info([ 'Generated files', [
        $crafterEventGenerate->files // empty (call before generate)
    ]]); 
}
Before command

CrafterEventCommand::BEFORE before run cli command

//class FileCrafterBehavior extends Behavior
public function beforeCommand(CrafterEventCommand $crafterEventCommand): void {
       Yii::error([ __METHOD__, [
        $crafterEventCommand->cmd->exec,
        $crafterEventCommand->cmd->output, // empty (call before exec command)
        $crafterEventCommand->cmd->replaceList
    ]]);
}
Cmd

\andy87\yii2\file_crafter\components\models\Dto

  • string $exec - exec command
  • string $output - exec output
  • array $replaceList - replace map
After command

CrafterEventCommand::AFTER after run cli command

//class FileCrafterBehavior extends Behavior
public function afterCommand(CrafterEventCommand $crafterEventCommand): void {
    Yii::error([ __METHOD__, [
        $crafterEventCommand->cmd->exec,
        $crafterEventCommand->cmd->output, // output command
        $crafterEventCommand->cmd->replaceList
    ]]);
}
Before render

CrafterEventRender::BEFORE before render file

//class FileCrafterBehavior extends Behavior
public function beforeRender(CrafterEventRender $crafterEventRender): void {
    Yii::error([ __METHOD__, [
        $crafterEventRender->schema,
        $crafterEventRender->sourcePath,
        $crafterEventRender->generatePath,
        $crafterEventRender->replaceList,
        $crafterEventRender->content // empty (call before render file)
    ]]);
}
After render

CrafterEventRender::AFTER after render file

//class FileCrafterBehavior extends Behavior
public function afterRender(CrafterEventRender $crafterEventRender): void {
    Yii::error([ __METHOD__, [
        $crafterEventRender->schema,
        $crafterEventRender->sourcePath,
        $crafterEventRender->generatePath,
        $crafterEventRender->replaceList,
        $crafterEventRender->content // content file
    ]]);
}
After generate

CrafterEventGenerate::AFTER after generate all files

public function afterGenerate(CrafterEventGenerate $crafterEventGenerate): void {
    Yii::info([ 'Generated files', [
        $crafterEventGenerate->files // CodeFile[]
    ]]); 
}

Packagist

]]>
0
[extension] neoacevedo/yii2-material Wed, 26 Mar 2025 16:59:50 +0000 https://www.yiiframework.com/extension/neoacevedo/yii2-material https://www.yiiframework.com/extension/neoacevedo/yii2-material NestorAcevedo NestorAcevedo

Yii2 Material

  1. Instalación
  2. Uso
  3. Componentes
  4. Donaciones
  5. Licencia

Esta es una extensión primaria para Yii framework 2.0. Encapsula componentes de Material Design en términos de Widgets Yii.

NOTA: Material Web 3 no tiene los componentes Card, Snackbar, TopAppBar ni NavigationRail, así que se han creado desde 0 intentando seguir los lineamientos del diseño de Material 3.

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

o agregue

"neoacevedo/yii2-material": "*"

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

Uso

Incluya antes del cierre de la etiqueta 'body' de su plantilla principal lo siguiente:

<?= MaterialAsset::publishMaterialScripts() ?>

Los data-* atributos programados no funcionan en Material Design Components para la web, por lo que se tendrán que programar los elementos que tengan estos atributos de manera separada.

Componentes

Donaciones

Si este proyecto te es útil, considera hacer una donación:

| Ko-fi | Litecoin | | ------------------------------------------------------ | ------------------------------------------------------------ | | [![Ko-fi QR](ko-fi.png)](https://ko-fi.com/neoacevedo) | Litecoin | | ☕ [Ko-fi](https://ko-fi.com/neoacevedo) | Ł Donaciones Litecoin |

Licencia

Este proyecto está licenciado bajo la Licencia GPL-3.0+ - ver el archivo LICENSE.md para más detalles.

]]>
0
[extension] mgrechanik/yii2-activefield-additional-error Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/mgrechanik/yii2-activefield-additional-error https://www.yiiframework.com/extension/mgrechanik/yii2-activefield-additional-error Pathfinder Pathfinder

Additional span with error class for ActiveField

  1. What is it about?
  2. Installing
  3. How to use

What is it about?

Bootstrap 4 and 5 are expecting `html` like this to decorate validation error:

<input type="text" id="eventform-datetime" class="form-control is-invalid" name="EventForm[datetime]" aria-required="true">
<div class="invalid-feedback">Error message</div>

Element with `div.invalid-feedbackis supposed to be on the same level with yourinput.is-invalid`.

But sometimes when we are using any widgets or custom template we get html like this:

<div class="some-plugin-wrapper">
  <input type="text" id="eventform-datetime" class="form-control is-invalid" name="EventForm[datetime]" aria-required="true">
</div>  
<div class="invalid-feedback">Error message</div>

, so our error message is not shown.

Of cource you can make `div.invalid-feedback` visible by css for this page.

But if that does not suit you, this library propose another solution.

We are adding special `to a field template right before{error}part. And we **synchronize** thiswith the **input field** so it gets.is-invalid` class when input does

Installing

Installing through composer::

The preferred way to install this library is through composer.

Either run composer require --prefer-dist mgrechanik/yii2-activefield-additional-error

or add "mgrechanik/yii2-activefield-additional-error " : "~1.0.0" to the require section of your composer.json.

How to use

in your `viewfile, say it is_form.php`

use mgrechanik\additionalerror\AdditionalErrorBehavior;

<div class="event-form-form">

    <?php $form = ActiveForm::begin([
            'id' => 'event-create-form',
            // Adding behavior
            'as adderror' => [
                'class' => AdditionalErrorBehavior::class,
            ]
    ]); ?>

    <?= $form->field($model, 'datetime', [
            // Adding this hidden span before error block 
            'template' => "{label}\n{input}\n{hint}\n" . $form->getAdditionalErrorSpan($model, 'datetime') . "\n{error}"
    ])->hint('Some hint')
      ->widget(/* Some complicated widget creates a wrapper for the {input} part... */)

It will work for both server and client side.

]]>
0
[extension] mgrechanik/gridviewfilterfix Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/mgrechanik/gridviewfilterfix https://www.yiiframework.com/extension/mgrechanik/gridviewfilterfix Pathfinder Pathfinder

Fix for Yii2 GridView filter functionality to work properly with Bootstrap 4 and Bootstrap 5

  1. What is it about?
  2. Installing
  3. How to use
  4. Similar problems with Forms or GridView and Bootstrap 4 / 5

What is it about?

When you are using Yii2 default GridView you might meet a problem that validation errors for filter model are not displayed properly, like this:

Fix for Yii2 GridView DataColumn for filter validational errors to be properly shown with bootstrap 4 and 5

Installing

Installing through composer::

The preferred way to install this library is through composer.

Either run composer require --prefer-dist mgrechanik/gridviewfilterfix

or add "mgrechanik/gridviewfilterfix" : "~1.0.0" to the require section of your composer.json.

How to use

Add the following lines of code to your main configuration file: 1) For Bootstrap 4 `php

'container' => [
    'definitions' => [
        \yii\grid\GridView::class => [
            'dataColumnClass' => \mgrechanik\gridviewfilterfix\Bs4DataColumn::class
        ]
    ]
],


2) For Bootstrap 5
```php
    'container' => [
        'definitions' => [
            \yii\grid\GridView::class => [
                'dataColumnClass' => \mgrechanik\gridviewfilterfix\Bs5DataColumn::class
            ]
        ]
    ],

Similar problems with Forms or GridView and Bootstrap 4 / 5

Paginator does not look good

Solution:

    'container' => [
        'definitions' => [
            \yii\widgets\LinkPager::class => \yii\bootstrap5\LinkPager::class,
        ],
    ],
Error block under field is now shown, after failed validation, since this block is not at the same level with input.is-invalid

There is a library to solve this problem

]]>
0
[extension] sjaakp/yii2-donate Sat, 10 Jan 2026 08:37:29 +0000 https://www.yiiframework.com/extension/sjaakp/yii2-donate https://www.yiiframework.com/extension/sjaakp/yii2-donate sjaakp sjaakp

yii2-donate

  1. Basic functionality
  2. Prerequisites
  3. Installation
  4. The Donate widget
  5. Module options
  6. Internationalization
  7. Module ID
Donation widget for Yii2

Latest Stable Version Total Downloads License

Yii2-donate is a module for the Yii 2.0 PHP Framework to handle donations. It makes use of the payment service provider Mollie, which is mainly active in Western European countries.

Yii2-donate sports a widget, which can be placed on any page (or even all pages).

Basic functionality

If a visitor selects an amount and presses the 'Donate'-button, she is transfered to a Mollie payment page. If she successfully completes the payment, she is redirected to the site's donate/thanks page, where she is rewarded with a joyful shower of confetti. If the visitor did supply an email address, she receives a 'Thank you' mail. The 'thanks' page also sports a button to resume her visit to the site.

If the visitor cancels the payment, she is redirected to the site's donate/cancel page, from where she can resume her surfing.

At any time, the site's administrator can get an overview of granted donations on the donate page.

Prerequisites

You'll need a Mollie account. It's free, but depending on your country, you may need a valid registration as a (small) business. You'll get two API keys, one for testing purposes and one for the real work. One of the API keys is used tot initialize the module.

It is strongly advised that the app uses Pretty URLs.

Because Yii2-donate may send emails, the mailer component of the application has to be up and running. Be sure that the 'adminEmail' parameter of the application has a sensible value. If you prefer, you may set the 'supportEmail' parameter as well; if set, Yii2-donate will use this.

Installation

Install Yii2-donate in the usual way with Composer. Add the following to the require section of your composer.json file:

"sjaakp/yii2-donate": "*"

or run:

composer require sjaakp/yii2-donate

You can manually install yii2-comus by downloading the source in ZIP-format.

Module

Yii2-donate is a module in the Yii2 framework. It has to be configured in the main configuration file, usually called web.php or main.php in the config directory. Add the following to the configuration array:

<?php
// ...
'modules' => [
    'donate' => [
        'class' => sjaakp\donate\Module::class,
        // several options
    ],
],
// ...

The module has to be bootstrapped. Do this by adding the following to the application configuration array:

<php
// ...
'bootstrap' => [
    'donate',
]
// ...

There probably already is a bootstrap property in your configuration file; just add 'donate' to it.

Important: the module should also be set up in the same way in the console configuration (usually called console.php).

Console command

To complete the installation, a console command have to be run. This will create a database table for the donations:

yii migrate

The migration applied is called sjaakp\donate\migrations\m000000_000000_init.

The Donate widget

Placing the Donate widget in any view is trivial:

<?php
use sjaakp\donate\DonateWidget;
?>
...
<?= DonateWidget::widget() ?>
...

The small, collapsed variant is obtained by:

<?php
use sjaakp\donate\DonateWidget;
?>
...
<?= DonateWidget::widget([
    'small' => true
]) ?>
...

Module options

The Donate module has a range of options. They are set in the application configuration like so:

 <?php
 // ...
 'modules' => [
     'donate' => [
         'class' => sjaakp\donate\Module::class,
         'description' => 'Please, buy me a drink!',
         // ...
         // ... more options ...
     ],
 ],
 // ...

The options (most are optional) are:

  • mollieApiKey string One of the API keys obtained from Mollie. Not optional, must be set.
  • choices array Amounts to select from. Keys are integers representing the amounts in cents, values are textual representations. Example: [ ..., 250 => '€2,50', 500 => '€5', ... ]. Defaults: amounts of 5, 10, 25, 50, and 100.
  • header string|null Text header appearing in the donate-widget. If null (default) no header is rendered.
  • includeMessage bool Whether a 'friendly message' field is included in the widget. Default: true.
  • confetti bool Whether confetti is shown on the 'thanks' page. Default: true.
  • description string|null The textual description displayed on Mollie's payment page. If null (default), defaults to 'Donation for <site name>'.
  • locale string|null The locale sent to the payment site. If null, defaults to site's language property.
  • mailOptions array Options for the app mailer. Default: see source.
  • localTest bool|null If true, performs a dummy-payment on the local system, useful for debugging and testing. If null (default), localTest is set to true if YII_ENV === 'dev', in other words if the site is in the development environment.
  • indexAccess array The access rule for the donations overview (donate page). Default: only accessible to authenticated visitors ([ 'allow' => true, 'roles' => ['@'] ]). For most sites, you'll want to refine this.

Internationalization

All of Yii2-donate's utterances are translatable. The translations are in the 'sjaakp\donate\messages' directory.

You can override Yii2-donate's translations by setting the application's message source in the main configuration, like so:

 <?php
 // ...
 'components' => [
     // ... other components ...     
     'i18n' => [
          'translations' => [
               // ... other translations ...
              'donate' => [    // override donate's standard messages
                  'class' => yii\i18n\PhpMessageSource::class,
                  'basePath' => '@app/messages',  // this is a default
                  'sourceLanguage' => 'en-US',    // this as well
              ],
          ],
     ],
     // ... still more components ...
 ]

The translations should be in a file called 'donate.php'.

If you want a single or only a few messages translated and use Yii2-donate's translations for the main part, the trick is to set up 'i18n' like above and write your translation file something like:

  <?php
  // app/messages/nl/donate.php
  
  $donateMessages = Yii::getAlias('@sjaakp/donate/messages/nl/donate.php');
  
  return array_merge (require($donateMessages), [
     'Amount' => 'Bedrag in euro',   // your preferred translation
  ]);

At the moment, the only language implemented is Dutch. Agreed, it's only the world's 52th language, but it happens to be my native tongue. Please, feel invited to translate Yii2-donate in other languages. I'll be more than glad to include them into Yii2-donate's next release.

Module ID

By default, the Module ID is 'donate'. It is set in the module configuration. If necessary (for instance if there is a conflict with another module or application component), you may set the Module ID to something different. Important: in that case, the moduleId property of the Donate widget must be set to this new value as well.

FAQ

Can I change the layout for the Yii2-donate views?

  • Use the EVENT_BEFORE_ACTION event. One easy way is to incorporate it in the module setup, like so:

    <?php
    // ...
    'modules' => [
       'donate' => [
           'class' => sjaakp\donate\Module::class,
           'description' => 'Please, buy me a drink!',
           'on beforeAction' => function ($event) {
               $event->sender->layout = '@app/views/layouts/one_column';
           },
           // ... more options ...
       ],
    ],
    // ...
    
]]>
0
[wiki] Use Single Login Session on All Your Yii2 Application/Repository Under Same Domain/Sub Domain Tue, 10 Sep 2024 12:26:07 +0000 https://www.yiiframework.com/wiki/2580/use-single-login-session-on-all-your-yii2-applicationrepository-under-same-domainsub-domain https://www.yiiframework.com/wiki/2580/use-single-login-session-on-all-your-yii2-applicationrepository-under-same-domainsub-domain aayushmhu aayushmhu

There are multiple blog that shows how to use seperate login for yii2 application but in this article i will show you how to use a single login screen for all your YII2 Advanced, YII2 Basic, Application, It will also work when your domain on diffrent server or the same server.

Here are few Steps you need to follow ot achive this.

1. For Advanced Templates

Step 1 : Add this into your component inside

/path/common/config/main.php

  'components' => [
        'user' => [
            'identityClass' => 'common\models\User',
            'enableAutoLogin' => true,
            'identityCookie' => ['name' => '_identity', 'httpOnly' => true],
        ],
        'request' => [
            'csrfParam' => '_csrf',
        ],
    ],

Step 2: Add Session and Request into main-local.php

/path/common/config/main-local.php

   'components' => [
        'session' => [
            'cookieParams' => [
                'path' => '/',
                'domain' => ".example.com",
            ],
        ],
        'user' => [
            'identityCookie' => [
                'name' => '_identity',
                'path' => '/',
                'domain' => ".example.com",
            ],
        ],
        'request' => [
            'csrfCookie' => [
                'name' => '_csrf',
                'path' => '/',
                'domain' => ".example.com",
            ],
        ],
    ],

Note: example.com is the main domain. All other domain should be sub domain of this.

Step 3: Now Update the Same Validation Key for all the applications

/path/frontend/config/main-local.php

/path/backend/config/main-local.php

 'components' => [
        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => 'fFUeb5HDj2P-1a1FTIqya8qOE',
        ],
    ],

Note : Remove the Session and request keys from your main.php of Both frontend and backend application.

Step 4: Note Somethign that you also have and console application so update session, user,and request into the main-local.php of your console application

/path/console/config/main-local.php

 'components' => [
        'session' => null,
        'user' => null,
        'request' => null,
    ]

2. For Basic Templates

Additionaly If you have an basic templates installed for another project and you want to use same login for that templates. To Achive this follow the given steps

Step 1: Update You main-local.php of basic template

/path/basic-app/config/main-local.php


 'components' => [
        'session' => [
            'cookieParams' => [
                'path' => '/',
                'domain' => ".example.com",
            ],
        ],
        'user' => [
            'identityCookie' => [
                'name' => '_identity',
                'path' => '/',
                'domain' => ".example.com",
            ],
        ],
        'request' => [
            'csrfCookie' => [
                'name' => '_csrf',
                'path' => '/',
                'domain' => ".example.com",
            ],
        ],

    ],

I Hope you understand well how to use a single login for all of your domain and subdomain or repository.

:) Thanks for Reading

]]>
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
[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
[wiki] How to Create and Use Validator Using Regular expressions Sat, 10 Jan 2026 08:37:29 +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. Sat, 10 Jan 2026 08:37:29 +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 Tue, 24 Dec 2024 21:24:53 +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:

/**
 * 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 Sat, 10 Jan 2026 08:37:29 +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 Thu, 03 Apr 2025 17:20:29 +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!!!!

Searching the Answers of these Question Find on Dynamic Duniya

]]>
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 Thu, 31 Jul 2025 05:54:55 +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 Sat, 10 Jan 2026 08:37:29 +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('[email protected]') // 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 Sat, 10 Jan 2026 08:37:29 +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' => ['[email protected]'],
                     'to' => ['[email protected]', '[email protected]'],
                     '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. Sat, 10 Jan 2026 08:37:29 +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, 31 Jul 2025 05:54:43 +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