Live News for Yii Framework News, fresh extensions and wiki articles about Yii framework. Wed, 16 Sep 2026 00:10:52 +0000 Zend_Feed_Writer 2 (http://framework.zend.com) https://www.yiiframework.com/ [news] Yii DataView 1.3 Wed, 16 Sep 2026 00:10:51 +0000 https://www.yiiframework.com/news/824/yii-dataview-1-3 https://www.yiiframework.com/news/824/yii-dataview-1-3 vjik vjik

Yii DataView version 1.3.0 was released. In this version:

  • Add GridView::keepColumnAttributesInEmptyCell() to keep column body cell attributes on empty cells
  • Add GridView::filterRowAttributes(), and filterAttributes and filterClass parameters to DataColumn to set HTML attributes and a CSS class for filter cells
  • Add GridView methods sortableHeaderClass(), sortableHeaderAscClass(), sortableHeaderDescClass(), sortableLinkAscClass() and sortableLinkDescClass() to configure CSS classes for sortable column headers and links
  • Add GridView::captionAttributes() method and $attributes parameter to GridView::caption() to set HTML attributes for the caption tag
  • Add OffsetPagination::currentLinkAttributes() and disabledLinkAttributes(), and KeysetPagination::disabledLinkAttributes() to set HTML attributes for the current page link or a disabled control, layered on top of linkAttributes()
  • Add BaseListView::accessibility() that opts into automatically added accessibility attributes: scope="col" and aria-sort on GridView header cells, scope="row" on row header cells, and aria-current, aria-disabled, aria-label and role="link" on pagination links — and a rowHeader parameter to DataColumn that renders the column's body cells as <th> row headers
  • Add ariaLabelNav(), ariaLabelFirst(), ariaLabelPrevious(), ariaLabelNext(), ariaLabelLast() and ariaLabelPage() methods to OffsetPagination, and ariaLabelNav(), ariaLabelPrevious() and ariaLabelNext() to KeysetPagination, to set aria-label on the nav container and page links
  • Add $accessibility, $translator and $translationCategory parameters to the PaginationContext constructor and to OffsetPagination::create() and KeysetPagination::create(), and a PaginationContext::translate() method
  • Make dependency container in GridView constructor optional
  • Render disabled KeysetPagination controls ("previous" on the first page, "next" on the last page) as span elements instead of a elements without an href
  • Render disabled OffsetPagination controls ("first"/"previous" on the first page, "next"/"last" on the last page) as span elements instead of clickable a elements
  • Keep GridView and column header cell attributes when a column renders no header
]]>
0
[extension] josemodi97/yii2-gii-datasource-generator Sun, 06 Sep 2026 15:44:43 +0000 https://www.yiiframework.com/extension/josemodi97/yii2-gii-datasource-generator https://www.yiiframework.com/extension/josemodi97/yii2-gii-datasource-generator JoseModi97 JoseModi97

Yii2 Gii Datasource Generator

  1. Features
  2. Requirements
  3. Installation
  4. How it works
  5. Comparison to yii2-giiant
  6. License

Generate Yii2 models, search models, controllers and CRUD views for an entire database schema in one step — with connection management, live schema preview, and an ER diagram — instead of running Gii's Model and CRUD generators one table at a time.

Latest Stable Version Total Downloads License

Features

  • Connection registry — save multiple named database connections; each is tested live before it's stored, and persists automatically with zero manual edits to your app's config/web.php.
  • Schema preview with ER diagram — see every table and view in a connection's schema, with a live entity-relationship diagram rendered from real foreign key metadata.
  • Generate-all-at-once — pick the tables you want, review a single combined diff/preview screen covering every model, search model, controller, and CRUD view, then confirm once to write them all.
  • Collision-safe multi-connection support — every generated class and file is prefixed with the connection's name (e.g. inventory_dbInventoryDbOrder), so two connections that both have an order table never collide, and each model is pinned to its own database connection.
  • Correct relations — foreign-key-based relations between generated models point at the correct connection-prefixed class name in both directions, even across a full batch generation run.

Requirements

  • PHP >= 8.1
  • Yii2 (yiisoft/yii2 ~2.0.45)
  • yiisoft/yii2-gii ~2.2.0 (installed automatically as a dependency)
  • A relational database with a yii\db\Schema driver: MySQL/MariaDB, PostgreSQL, SQL Server, SQLite, Oracle, or CUBRID. Non-relational sources are not supported in this release.

Installation

Add the package with Composer:

composer require josemodi97/yii2-gii-datasource-generator

No further configuration is required — the extension registers itself into your application via Composer's extra.bootstrap mechanism on every request (the same pattern used by other zero-config Yii2 extensions), so config/web.php and config/console.php never need to be edited.

Once installed, visit /gii-datasource-generator/connection/index in your dev-environment application (access is restricted to 127.0.0.1/::1 by default, same as Gii itself — configure allowedIPs on the module if you need broader access).

How it works

  1. Add a connection — pick a driver, enter connection details, and it's tested live before being saved to config/gii-datasources.php (add this file to your .gitignore — it holds plaintext credentials, same trust boundary as Gii itself).
  2. Preview the schema — see every table/view, with an ER diagram for relational sources.
  3. Generate All — select the objects you want, review the combined file list (grouped into Models / Search Models / Controllers / Views, with inline diff/preview), then confirm.

Everything generated is placed in your app's normal models/, controllers/, and views/ folders, using the connection's name as a class/file prefix — so it's safe to run this against several databases in the same project.

Comparison to yii2-giiant

yii2-giiant is the established tool for batch-generating CRUD from a schema, using the db component your app already has configured. This package targets a different scenario: managing several independent database connections in one project through a UI (rather than one connection already wired into your app config), with a schema/ER preview step before generation, and connection-prefixed naming so multiple databases' generated code can never collide.

License

MIT — see LICENSE.

]]>
0
[news] Yii DataView 1.2 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/823/yii-dataview-1-2 https://www.yiiframework.com/news/823/yii-dataview-1-2 vjik vjik

Yii DataView version 1.2.0 was released. In this version:

  • Add prepareDataReader() to reuse filtered data reader
  • Add UseInlineJsInterface::useInlineJs() to DropdownFilter, SelectPageSize and InputPageSize, and BaseListView::useInlineJs() to override it for every rendered widget, so their onChange/onchange handler can be replaced with the shipped no-inline-js.js script
  • Pass DataContext to callable buttons in ActionColumn
]]>
0
[extension] mspirkov/yii2-rector Wed, 02 Sep 2026 13:30:33 +0000 https://www.yiiframework.com/extension/mspirkov/yii2-rector https://www.yiiframework.com/extension/mspirkov/yii2-rector max-s-lab max-s-lab

993323

Yii2 Rector

  1. Support
  2. Installation
  3. Usage
  4. Rules at a glance
  5. Rule reference

A set of Rector rules for Yii2 projects that I put together for my own day-to-day work. They make refactoring a Yii2 codebase easier and help keep it cleaner, automating the framework-specific patterns — magic properties, ActiveRecord/Query calls, accumulated deprecations — that a generic Rector set has no way to know about.

PHP Yii2 Rector Total Downloads Tests Coverage PHPStan Level Max

Support

If you like this project, give it a ⭐ on GitHub — it helps others discover it.

Installation

[!IMPORTANT]

It works better with the latest versions of PHP, Yii2, and Rector. The more up‑to‑date the versions, the better the refactoring.

composer require --dev mspirkov/yii2-rector

Usage

use MSpirkov\Yii2\Rector\Yii2SetList;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths(...)
    ->withSets([
        Yii2SetList::MAIN,
    ]);
Enabling individual rules

Individual rules can be enabled on their own via ->withRules([...]) instead of ->withSets([...]):

use MSpirkov\Yii2\Rector\Rules\ReplaceClassnameWithClassRector;
use MSpirkov\Yii2\Rector\Rules\ReplaceExistenceCheckWithExistsRector;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths(...)
    ->withRules([
        ReplaceClassnameWithClassRector::class,
        ReplaceExistenceCheckWithExistsRector::class,
    ]);
Skipping rules

Any rule — whether pulled in through Yii2SetList::MAIN or added individually — can be turned off entirely via ->withSkip([...]):

use MSpirkov\Yii2\Rector\Rules\MergeModelRulesRector;
use MSpirkov\Yii2\Rector\Yii2SetList;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths(...)
    ->withSets([
        Yii2SetList::MAIN,
    ])
    ->withSkip([
        MergeModelRulesRector::class,
    ]);

Mapping a rule to a list of paths/patterns instead skips it only there, leaving it active everywhere else — handy for legacy code that isn't ready for a particular rule yet:

    ->withSkip([
        MergeModelRulesRector::class => [
            __DIR__ . '/src/Legacy/*',
        ],
    ]);

A plain path/pattern (no rule class key) skips those files from every rule, Yii2-specific or not.

Configuring a rule

AddPropertyTagsRector and RemoveRedundantPropertyTagsRector accept a skippedClasses option — see the rule reference below for the exact shape of each — and AddPropertyTagsRector additionally accepts insertBeforeTags. Configure them via ->withConfiguredRule(), using the rule's own constants as keys:

use MSpirkov\Yii2\Rector\Rules\AddPropertyTagsRector;
use MSpirkov\Yii2\Rector\Yii2SetList;
use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths(...)
    ->withSets([
        Yii2SetList::MAIN,
    ])
    ->withConfiguredRule(AddPropertyTagsRector::class, [
        'skippedClasses' => [
            'App\Models\LegacyModel',
            'App\Models\Product' => ['internalNotes'],
        ],
        'insertBeforeTags' => ['@author', '@since'],
    ]);

App\Models\LegacyModel above is skipped entirely (a plain array value), while only the internalNotes property is skipped on App\Models\Product (a class-name key mapped to a list of property names) — every other property on it is still processed normally.

Rules at a glance

Rule Description
AddPropertyTagsRector Add (or correct) @property/@property-read/@property-write tags on a yii\base\BaseObject subclass, based on its own getXxx()/setXxx() method pairs and ActiveRecord relation getters (hasOne()/hasMany()).
MergeModelRulesRector Merge yii\base\Model::rules() entries that configure the same validator with the same options but a different attribute into one entry, combining their attributes into a single array (an attribute already present in another merged entry is not duplicated).
RemoveRedundantHtmlEncodeRector Remove a yii\helpers\Html::encode() call whose $content argument PHPStan proves is a numeric string — digits only can't contain a character htmlspecialchars() would touch, so the call is replaced by its bare $content argument (dropping a trailing $doubleEncode argument, if present, too).
RemoveRedundantPropertyTagsRector Remove a @property/@property-read/@property-write tag from a yii\base\BaseObject subclass when neither a matching public getXxx() nor setXxx() method exists (own or inherited) — typically left behind after the accessor it documented was renamed or removed.
ReplaceClassnameWithClassRector Replace the deprecated yii\base\BaseObject::className() call with the native ::class constant.
ReplaceExistenceCheckWithExistsRector Replace an existence check on a yii\db\QueryInterface result with the cheaper ->exists() call.
ReplaceFindWhereAllWithFindAllRector Replace find()->where([...])->all() on an ActiveRecord class with the equivalent findAll([...]).
ReplaceFindWhereOneWithFindOneRector Replace find()->where([...])->one() on an ActiveRecord class with the equivalent findOne([...]).
ReplaceGetterWithPropertyRector Replace a yii\base\BaseObject getter call with the equivalent magic-property access, when the property is documented via a class-level @property or @property-read tag whose type matches the getter's return type, and there is no public native property of the same name (which would bypass the getter entirely)
ReplaceSetterWithPropertyRector Replace a yii\base\BaseObject setter call with the equivalent magic-property assignment, when the property is documented via a class-level @property or @property-write tag whose type matches the setter's parameter type, and there is no public native property of the same name (which would bypass the setter entirely)
ReplaceWhereEqualityConditionWithArrayRector Replace a single-column string where()/andWhere()/orWhere() condition (interpolated or concatenated) with the safer array condition format

Rule reference

AddPropertyTagsRector

Add (or correct) @property/@property-read/@property-write tags on a yii\base\BaseObject subclass, based on its own getXxx()/setXxx() method pairs and ActiveRecord relation getters (hasOne()/hasMany()). A class whose __get()/__set() is overridden by something other than yii\base\BaseObject, yii\base\Component, yii\db\BaseActiveRecord, or yii\base\DynamicModel is skipped entirely, since such magic properties may not correspond to getXxx()/setXxx() methods. Configurable via skippedClasses — a plain array value (e.g. 'App\Foo') fully skips a class, while a string key mapped to a list of property names (e.g. 'App\Bar' => ['name']) skips only those properties — and insertBeforeTags, a list of PHPDoc tag names (defaulting to ['@author', '@since', '@mixin']) before which newly added @property* tags are inserted

+/**
+ * @property string $name The product name.
+ * @property-read int $price
+ * @property-write float $discount
+ */
 class Product extends BaseObject
 {
     private string $_name;

     private int $_price;

     private float $_discount;

     /**
      * @return string The product name.
      */
     public function getName(): string
     {
         return $this->_name;
     }

     /**
      * @param string $name The product name.
      */
     public function setName(string $name): void
     {
         $this->_name = $name;
     }

     public function getPrice(): int
     {
         return $this->_price;
     }

     public function setDiscount(float $discount): void
     {
         $this->_discount = $discount;
     }
 }
+/**
+ * @property-read Customer|null $customer
+ * @property-read OrderItem[] $items
+ */
 class Order extends ActiveRecord
 {
     public function getCustomer(): ActiveQuery
     {
         return $this->hasOne(Customer::class, ['id' => 'customer_id']);
     }

     public function getItems(): ActiveQuery
     {
         return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
     }
 }
MergeModelRulesRector

Merge yii\base\Model::rules() entries that configure the same validator with the same options but a different attribute into one entry, combining their attributes into a single array (an attribute already present in another merged entry is not duplicated). Two entries only merge when everything after the attribute(s) — the validator and any options — is identical; a rules() body that isn't a single return [...] of literal rule arrays is left untouched

 class LoginForm extends Model
 {
     public function rules(): array
     {
         return [
-            ['login', 'required'],
-            ['password', 'required'],
+            [['login', 'password'], 'required'],
         ];
     }
 }
RemoveRedundantHtmlEncodeRector

Remove a yii\helpers\Html::encode() call whose $content argument PHPStan proves is a numeric string — digits only can't contain a character htmlspecialchars() would touch, so the call is replaced by its bare $content argument (dropping a trailing $doubleEncode argument, if present, too). Any other $content is left untouched

 <?php
 /**
  * @var numeric-string $id
  * @var string $name
  */
 ?>
-<?= Html::encode($id) ?>
+<?= $id ?>
 <?= Html::encode($name) ?>
RemoveRedundantPropertyTagsRector

Remove a @property/@property-read/@property-write tag from a yii\base\BaseObject subclass when neither a matching public getXxx() nor setXxx() method exists (own or inherited) — typically left behind after the accessor it documented was renamed or removed. A tag backed by at least one accessor is left untouched even if it names the wrong direction (e.g. @property-read with only a setter) — correcting it to match the accessor that does exist is AddPropertyTagsRector's job, not this rule's, so the two never touch the same tag. A class whose __get()/__set() isn't the one inherited from yii\base\BaseObject or yii\base\Component — own override or inherited from some other ancestor, including yii\db\BaseActiveRecord and yii\base\DynamicModel — is skipped entirely, since its magic properties aren't necessarily backed by getter/setter methods. Configurable via skippedClasses — a plain array value (e.g. 'App\Foo') fully skips a class, while a string key mapped to a list of property names (e.g. 'App\Bar' => ['name']) skips only those properties

 /**
  * @property string $name
- * @property-read int $legacyCount
  */
 class Product extends BaseObject
 {
     private string $_name;

     public function getName(): string
     {
         return $this->_name;
     }

     public function setName(string $name): void
     {
         $this->_name = $name;
     }
 }
ReplaceClassnameWithClassRector

Replace the deprecated yii\base\BaseObject::className() call with the native ::class constant. self::className() and parent::className() are left untouched, since both are late-static-binding forwarding calls not generally equivalent to self::class/parent::class once the class is subclassed — only static::className() and an explicit class name are rewritten

-$class = SomeClass::className();
-$class = static::className();
+$class = SomeClass::class;
+$class = static::class;
ReplaceExistenceCheckWithExistsRector

Replace an existence check on a yii\db\QueryInterface result with the cheaper ->exists() call. Recognises a ->count() comparison against the boundary literals 0/1 (in either operand order) and a strict ->one() !== null / ->one() === null check. A check that means "no rows" (e.g. count() < 1, one() === null) is rewritten to the negated !exists(), not exists(). Only the boundary comparisons that map unambiguously onto a presence/absence question are recognised — count() > 1, for instance, is left untouched

 public function emailIsTaken(string $email): bool
 {
-    return User::find()->where(['email' => $email])->one() !== null;
+    return User::find()->where(['email' => $email])->exists();
 }

 public function emailIsAvailable(string $email): bool
 {
-    return User::find()->where(['email' => $email])->count() < 1;
+    return !User::find()->where(['email' => $email])->exists();
 }
ReplaceFindWhereAllWithFindAllRector

Replace find()->where([...])->all() on an ActiveRecord class with the equivalent findAll([...]). Only fires when the where() condition is a literal array keyed entirely by string literals: findAll() treats any other condition shape (scalar, list, Expression) as a primary key lookup instead of forwarding it to where() unchanged, so those shapes are intentionally left untouched.

-$customers = Customer::find()->where(['status' => 1])->all();
+$customers = Customer::findAll(['status' => 1]);
ReplaceFindWhereOneWithFindOneRector

Replace find()->where([...])->one() on an ActiveRecord class with the equivalent findOne([...]). Only fires when the where() condition is a literal array keyed entirely by string literals: findOne() treats any other condition shape (scalar, list, Expression) as a primary key lookup instead of forwarding it to where() unchanged, so those shapes are intentionally left untouched.

-$customer = Customer::find()->where(['status' => 1])->one();
+$customer = Customer::findOne(['status' => 1]);
ReplaceGetterWithPropertyRector

Replace a yii\base\BaseObject getter call with the equivalent magic-property access, when the property is documented via a class-level @property or @property-read tag whose type matches the getter's return type, and there is no public native property of the same name (which would bypass the getter entirely)

 /**
  * @property-read string $prop
  */
 class Example extends BaseObject
 {
     private string $_prop;

     public function getProp(): string
     {
         return $this->_prop;
     }
 }

-$value = (new Example())->getProp();
+$value = (new Example())->prop;
ReplaceSetterWithPropertyRector

Replace a yii\base\BaseObject setter call with the equivalent magic-property assignment, when the property is documented via a class-level @property or @property-write tag whose type matches the setter's parameter type, and there is no public native property of the same name (which would bypass the setter entirely)

 /**
  * @property-write string $prop
  */
 class Example extends \yii\base\BaseObject
 {
     private string $_prop;

     public function setProp(string $value): void
     {
         $this->_prop = $value;
     }
 }

-(new Example())->setProp('value');
+(new Example())->prop = 'value';
ReplaceWhereEqualityConditionWithArrayRector

Replace a single-column string where()/andWhere()/orWhere() condition (interpolated or concatenated) with the safer array condition format

-$query->where("column = $value");
-$query->andWhere('column = ' . $value);
+$query->where(['column' => $value]);
+$query->andWhere(['column' => $value]);
]]>
0
[news] ApiDoc extension version 4.0.1 released Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/822/apidoc-extension-version-4-0-1-released https://www.yiiframework.com/news/822/apidoc-extension-version-4-0-1-released samdark samdark

We are very pleased to announce the release of the ApiDoc extension version 4.0.1.

This release enhances the generation:

  • Recoverable HTML parsing errors in API link titles are now suppressed.
  • Removed the bold formatting from the method names for better readability.
  • Added <wbr> to namespaces so line breaks are made on \ if possible.

See the CHANGELOG for a full list of changes.

]]>
0
[extension] yiisoft/yii2-bootstrap5 Thu, 27 Aug 2026 06:39:47 +0000 https://www.yiiframework.com/extension/yiisoft/yii2-bootstrap5 https://www.yiiframework.com/extension/yiisoft/yii2-bootstrap5 samdark samdark

Yii Framework

Twitter Bootstrap 5 Extension for Yii 2

  1. Installation
  2. Translations
  3. Usage
  4. Documentation
  5. Support the project
  6. Follow updates

This is the Twitter Bootstrap extension for Yii framework 2.0. It encapsulates Bootstrap 5 components and plugins in terms of Yii widgets, and thus makes using Bootstrap components/plugins in Yii applications extremely easy.

For license information check the LICENSE-file.

Documentation is at docs/guide/README.md.

Latest Stable Version Total Downloads build codecov Static Analysis

Installation

[!IMPORTANT]

  • The minimum required PHP version is PHP 7.4.
  • It works best with PHP 8.

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist yiisoft/yii2-bootstrap5

or add

"yiisoft/yii2-bootstrap5": "*"

to the require section of your composer.json file.

Translations

The i18n configuration will be automatically added to your application configuration via bootstrapping process.

Usage

For example, the following single line of code in a view file would render a Bootstrap Progress plugin:

<?= yii\bootstrap5\Progress::widget(['percent' => 60, 'label' => 'test']) ?>

Documentation

Support the project

Open Collective

Follow updates

Official website Follow on X Telegram Slack

]]>
0
[news] Yii HTTP Middleware 1.3 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/821/yii-http-middleware-1-3 https://www.yiiframework.com/news/821/yii-http-middleware-1-3 vjik vjik

Yii HTTP Middleware version 1.3.0 was released.

In this version:

  • Add ETag value normalization in HttpCacheMiddleware via ETagValueNormalizerInterface with NoopETagValueNormalizer and SuffixETagValueNormalizer implementations
  • Add $keepHeadersOnStatusCode and $removedHeaders constructor parameters to RemoveBodyMiddleware
  • Add $removeOnStatusCode constructor parameter to ContentLengthMiddleware
  • Remove Content-Length and Transfer-Encoding headers in RemoveBodyMiddleware when the body is removed
  • Remove already present Content-Length header in ContentLengthMiddleware for status codes that must not carry one
  • Add missing 103 Early Hints status code to default status code lists in RemoveBodyMiddleware and ContentLengthMiddleware
]]>
0
[wiki] Yii2 Multi-Tenant Auth: How to Support Concurrent Multi-User Sessions in the Same Browser Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/wiki/2764/yii2-multi-tenant-auth-how-to-support-concurrent-multi-user-sessions-in-the-same-browser https://www.yiiframework.com/wiki/2764/yii2-multi-tenant-auth-how-to-support-concurrent-multi-user-sessions-in-the-same-browser braaalsalahi braaalsalahi

Yii2 Multi-Tenant Auth: How to Support Concurrent Multi-User Sessions in the Same Browser

  1. 1. Introduction & The Engineering Challenge
  2. 2. Architecture Overview: Real-Time Dynamic Context Switching
  3. 3. Step 1: The Custom User Component (common\components\User.php)
  4. 4. Step 2: The Custom Session Component (common\components\Session.php)
  5. 5. Step 3: Application Configuration (config/main.php)
  6. 6. Key Benefits & Takeaways

A Comprehensive Guide to Multi-Context Authentication, Real-Time Context Switching, and Session Isolation in Yii2 Framework.

1. Introduction & The Engineering Challenge

In modern web applications built on Yii2 Advanced Framework—such as E-Commerce platforms with a Main Admin Panel and a Merchant/Seller Portal—developers often face a critical requirement:

How can a user log into the Main Admin panel AND a Merchant portal concurrently using two different accounts in the exact same browser without session collisions?

The Core Problem:

Web browsers attach the same session cookie (PHP Session ID) to every HTTP request sent to the same domain. Out of the box, Yii2 relies on a single session key ($_SESSION['__id']) to track the authenticated identity.

When a user logs into the Merchant portal in Tab 2, PHP overwrites $_SESSION['__id'] with the Merchant User ID. The moment the user refreshes Tab 1 (Admin Panel), Yii2 loads the Merchant User identity instead of the Admin, leading to broken authorization, UI glitches, or unexpected logouts.

2. Architecture Overview: Real-Time Dynamic Context Switching

Rather than attempting to hack runtime session names or creating fragile beforeRequest event hooks, the optimal solution introduces a Zero Code Mutation Architecture.

By subclassing yii\web\User and yii\web\Session, we intercept authentication and session access points to perform Real-Time Dynamic Context Switching based on the request URI:

                  ┌──────────────────────────────────────────────┐
                  │                HTTP Request                  │
                  └──────────────────────┬───────────────────────┘
                                         │
                         ┌───────────────┴───────────────┐
                         │   URL Routing Context Check   │
                         └───────────────┬───────────────┘
                                         │
              ┌──────────────────────────┴──────────────────────────┐
              ▼                                                     ▼
   [ Main Admin Route ]                                   [ Merchant Module Route ]
   Context = 'admin'                                      Context = 'merchant'
   ------------------                                     ---------------------
   idParam = '__id'                                       idParam = '__id_merchant'
   Cookie  = '_identity_admin'                            Cookie  = '_identity_merchant'
   Session = $_SESSION['key']                             Session = $_SESSION['merchant_key']
   Flashes = $_SESSION['__flash']                         Flashes = $_SESSION['merchant___flash']

3. Step 1: The Custom User Component (common\components\User.php)

We extend yii\web\User to dynamically evaluate the active URL route. Depending on the route context, the component updates $idParam and $identityCookie on the fly and invalidates cached identity objects when switching between tabs.

<?php

namespace common\components;

use Yii;
use yii\web\User as BaseUser;

class User extends BaseUser
{
    private $_lastContext = null;

    public function init()
    {
        $this->applyContext();
        parent::init();
    }

    /**
     * Determines the active application context based on URL path.
     */
    protected function getCurrentContext()
    {
        try {
            if (Yii::$app->has('request')) {
                $path = Yii::$app->request->getPathInfo();
                $merchantPrefix = 'merchant'; // Prefix for merchant routes

                if ($path === $merchantPrefix || strpos($path, $merchantPrefix . '/') === 0) {
                    return 'merchant';
                }
            }
        } catch (\Exception $e) {
        }
        return 'admin';
    }

    /**
     * Applies context-specific idParam and identityCookie configurations.
     */
    public function applyContext()
    {
        $context = $this->getCurrentContext();
        
        if ($this->_lastContext !== $context) {
            $env = defined('YII_ENV') ? '_' . YII_ENV : '';

            if ($context === 'merchant') {
                $this->idParam = '__id_merchant';
                $this->identityCookie = [
                    'name' => '_identity_merchant' . $env,
                    'httpOnly' => true,
                ];
            } else {
                $this->idParam = '__id';
                $this->identityCookie = [
                    'name' => '_identity_admin' . $env,
                    'httpOnly' => true,
                ];
            }

            // Invalidate identity cache when switching context
            if ($this->_lastContext !== null) {
                $this->setIdentity(null);
            }

            $this->_lastContext = $context;
        }
    }

    // Intercept entry points to ensure context is applied before execution
    public function getIdentity($autoRenew = true)
    {
        $this->applyContext();
        return parent::getIdentity($autoRenew);
    }

    public function login(\yii\web\IdentityInterface $identity, $duration = 0)
    {
        $this->applyContext();
        return parent::login($identity, $duration);
    }

    public function logout($destroySession = true)
    {
        $this->applyContext();
        return parent::logout($destroySession);
    }

    protected function renewAuthStatus()
    {
        $this->applyContext();
        parent::renewAuthStatus();
    }
}

4. Step 2: The Custom Session Component (common\components\Session.php)

To prevent generic session data collisions (Yii::$app->session->set('key', $val)) and flash alert mismatches (setFlash), we subclass yii\web\Session.

This component automatically prefixes session keys and isolates flash arrays based on the active context:

<?php

namespace common\components;

use Yii;
use yii\web\Session as BaseSession;

class Session extends BaseSession
{
    /**
     * Returns key prefix based on current route context.
     */
    protected function getContextPrefix()
    {
        try {
            if (Yii::$app->has('request')) {
                $path = Yii::$app->request->getPathInfo();
                $merchantPrefix = 'merchant';

                if ($path === $merchantPrefix || strpos($path, $merchantPrefix . '/') === 0) {
                    return 'merchant_';
                }
            }
        } catch (\Exception $e) {
        }
        return '';
    }

    /**
     * Ensures session status is active before reading/writing.
     */
    protected function ensureSessionActive()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            @session_start();
        }
    }

    // --- Session Key Getters & Setters ---

    public function get($key, $defaultValue = null)
    {
        $this->ensureSessionActive();
        return parent::get($this->getContextPrefix() . $key, $defaultValue);
    }

    public function set($key, $value)
    {
        $this->ensureSessionActive();
        parent::set($this->getContextPrefix() . $key, $value);
    }

    public function remove($key)
    {
        $this->ensureSessionActive();
        return parent::remove($this->getContextPrefix() . $key);
    }

    public function has($key)
    {
        $this->ensureSessionActive();
        return parent::has($this->getContextPrefix() . $key);
    }

    // --- Flash Messages Isolation ---

    public function setFlash($key, $value = true, $removeAfterAccess = true)
    {
        $this->ensureSessionActive();
        $param = $this->getContextPrefix() . $this->flashParam;

        $counters = isset($_SESSION[$param]) && is_array($_SESSION[$param]) ? $_SESSION[$param] : [];
        $counters[$key] = $removeAfterAccess ? -1 : 0;
        $_SESSION[$param] = $counters;
        $_SESSION[$param][$key] = $value;
    }

    public function getFlash($key, $defaultValue = null, $delete = false)
    {
        $this->ensureSessionActive();
        $param = $this->getContextPrefix() . $this->flashParam;

        $counters = isset($_SESSION[$param]) && is_array($_SESSION[$param]) ? $_SESSION[$param] : [];
        if (!isset($counters[$key])) {
            return $defaultValue;
        }

        $value = isset($_SESSION[$param][$key]) ? $_SESSION[$param][$key] : $defaultValue;
        if ($delete) {
            $this->removeFlash($key);
        }

        return $value;
    }

    public function getAllFlashes($delete = false)
    {
        $this->ensureSessionActive();
        $param = $this->getContextPrefix() . $this->flashParam;

        $counters = isset($_SESSION[$param]) && is_array($_SESSION[$param]) ? $_SESSION[$param] : [];
        $flashes = [];
        foreach ($counters as $key => $count) {
            if (isset($_SESSION[$param][$key])) {
                $flashes[$key] = $_SESSION[$param][$key];
                if ($delete) {
                    unset($counters[$key], $_SESSION[$param][$key]);
                }
            }
        }
        if ($delete) {
            $_SESSION[$param] = $counters;
        }

        return $flashes;
    }

    public function removeFlash($key)
    {
        $this->ensureSessionActive();
        $param = $this->getContextPrefix() . $this->flashParam;

        $counters = isset($_SESSION[$param]) && is_array($_SESSION[$param]) ? $_SESSION[$param] : [];
        $value = isset($_SESSION[$param][$key]) ? $_SESSION[$param][$key] : null;
        unset($counters[$key], $_SESSION[$param][$key]);
        $_SESSION[$param] = $counters;

        return $value;
    }

    public function hasFlash($key)
    {
        return $this->getFlash($key) !== null;
    }
}

5. Step 3: Application Configuration (config/main.php)

Configure your custom components in backend/config/main.php (or your relevant configuration file):

return [
    'components' => [
        // 1. Custom Session Component
        'session' => [
            'class' => 'common\components\Session',
            'timeout' => 86400 * 30,
            'name' => 'advanced_backend_session',
            'cookieParams' => [
                'httpOnly' => true,
                'secure' => false,
                'lifetime' => 3600 * 24 * 30,
            ],
        ],

        // 2. Custom User Component
        'user' => [
            'class' => 'common\components\User',
            'identityClass' => 'common\models\User',
            'enableAutoLogin' => true,
            'identityCookie' => [
                'name' => '_identity_admin',
                'httpOnly' => true,
            ],
        ],
        
        // ... rest of your application components
    ],
];

6. Key Benefits & Takeaways

  1. Zero Code Mutation Across Existing Project Code:
    • Developers continue calling Yii::$app->user->identity, Yii::$app->user->id, Yii::$app->session->set(), and Yii::$app->session->setFlash() standardly without modifying views, controllers, or widgets.
  2. True Concurrent Multi-User Authentication:
    • Users can be signed into an Admin account in Tab 1 and a Merchant account in Tab 2 simultaneously within the same browser.
  3. 100% RBAC & Cache Compatibility:
    • Standard RBAC managers (yii\rbac\DbManager or custom cached managers) key permission queries by $userId (userAccessCheck:userId:permission), preserving full isolation.
Conclusion

By leveraging object-oriented extension points in Yii2's core architecture (yii\web\User and yii\web\Session), you achieve robust multi-tenant authentication and session isolation elegantly. Happy coding!

]]>
0
[extension] josemodi97/yii2-ecitizen-gateway Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/josemodi97/yii2-ecitizen-gateway https://www.yiiframework.com/extension/josemodi97/yii2-ecitizen-gateway JoseModi97 JoseModi97

yii2-ecitizen-gateway

  1. Compatibility
  2. Installation
  3. Quickstart: Laravel
  4. Quickstart: Yii2
  5. Quickstart: Standalone PHP (No Framework)
  6. Core Features & Usage Details
  7. File & Class Structure
  8. Security
  9. License

A beginner-friendly Kenya eCitizen / PesaFlow payment gateway for Laravel, Yii2, and Standalone PHP. Build signed checkout payloads, render payment buttons, and verify webhook callbacks with plain-English fields.

  • Framework Agnostic: Pure PHP with zero forced dependencies (php: >=7.4). Works seamlessly in Laravel, Yii2, Yii3, WordPress, or vanilla PHP.
  • Laravel Auto-Discovery: Includes native EcitizenServiceProvider and Ecitizen Facade with php artisan vendor:publish support.
  • Yii2 Gii Tools: Includes eCitizen Client Generator and eCitizen Controller Generator in Gii with zero view-template dependencies.
  • Zero Database Obligation: Accepts and verifies payments out of the box without requiring any database tables or migrations. Bring your own database models when ready.
  • Instant Payment Button: Render ready-to-use, HMAC-signed payment forms in one line of code (payButton()).
  • Safaricom M-Pesa STK Push: Automatic Kenyan phone normalization (PhoneHelper) to trigger instant PIN prompts on customer phones.
  • Cryptographic Signature Verification: Validate server-to-server IPN notifications with HMAC-SHA256 (verify(), isPaid()).

Compatibility

  • Requires PHP 7.4 or newer (fully tested on PHP 8.0, 8.1, 8.2, 8.3, and 8.4+).
  • Laravel: Fully compatible with Laravel 6.x, 7.x, 8.x, 9.x, 10.x, and 11.x+.
  • Yii2: Fully compatible with Yii 2.0.x and Gii code generation.
  • Standalone PHP: Core classes (EcitizenClient, EcitizenGateway, PhoneHelper) have zero third-party dependencies.

Installation

Install via Composer:

composer require josemodi97/yii2-ecitizen-gateway

Quickstart: Laravel

The package registers its Service Provider and Ecitizen Facade automatically via Laravel Package Discovery.

1. Publish Configuration

Run the Artisan publish command to create config/ecitizen.php:

php artisan vendor:publish --tag=ecitizen-config
2. Configure Environment Variables

Add your merchant credentials to your .env file:

ECITIZEN_CLIENT_ID=your_api_client_id
ECITIZEN_API_KEY=your_api_key
ECITIZEN_SECRET=your_merchant_secret
ECITIZEN_SERVICE_ID=your_service_id
ECITIZEN_GATEWAY_URL=https://payments.ecitizen.go.ke/PaymentAPI/iframev2.1.php
ECITIZEN_CURRENCY=KES
3. Create a Payment Controller

Use the Ecitizen Facade in your controller:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Ecitizen; // or: use odhis\ecitizen\adapters\laravel\Facades\Ecitizen;

class PaymentController extends Controller
{
    /**
     * Display the payment summary card and pay button.
     */
    public function pay()
    {
        $payButtonHtml = Ecitizen::payButton([
            'amount'      => 1500,
            'reference'   => 'INV-1002',
            'description' => 'Land Rates Clearance',
            'name'        => 'John Doe',
            'idNumber'    => '28374619',
            'phone'       => '0712345678', // Automatically triggers Safaricom M-Pesa STK push!
            'callbackUrl' => route('payment.success'),
            'notifyUrl'   => route('payment.notify'),
        ], 'Proceed to eCitizen', ['class' => 'btn btn-success btn-lg']);

        return view('payment.pay', compact('payButtonHtml'));
    }

    /**
     * Webhook endpoint: receives server-to-server IPN from eCitizen.
     */
    public function notify(Request $request)
    {
        $result = Ecitizen::verify($request->all());

        if ($result['success']) {
            // Payment verified and settled!
            $reference  = $result['reference'];
            $amountPaid = $result['amountPaid'];

            // Optional: update your database record here
            // Order::where('reference', $reference)->update(['status' => 'paid']);

            return response()->json(['status' => 'ok']);
        }

        return response()->json(['status' => 'error', 'message' => 'Invalid signature'], 400);
    }

    /**
     * Browser redirect landing page after payment.
     */
    public function success()
    {
        return view('payment.success');
    }
}
4. Configure Routes & Exempt CSRF

Because eCitizen servers send webhook notifications via POST, you must exempt the notification route from CSRF verification.

Routes (routes/web.php):
use App\Http\Controllers\PaymentController;

Route::get('/payment/pay', [PaymentController::class, 'pay'])->name('payment.pay');
Route::post('/payment/notify', [PaymentController::class, 'notify'])->name('payment.notify');
Route::get('/payment/success', [PaymentController::class, 'success'])->name('payment.success');
CSRF Exemption:
  • Laravel 11+ (bootstrap/app.php): `php ->withMiddleware(function (Middleware $middleware) {

    $middleware->validateCsrfTokens(except: [
        'payment/notify',
    ]);
    

    }) `

  • Laravel 6.x – 10.x (app/Http/Middleware/VerifyCsrfToken.php): `php protected $except = [

    'payment/notify',
    

    ]; `

5. Render in Blade View (resources/views/payment/pay.blade.php)
<div class="card p-4 shadow-sm text-center">
    <h2>Invoice #INV-1002</h2>
    <p class="lead">Amount: <strong>KES 1,500</strong></p>
    <div class="mt-3">
        {!! $payButtonHtml !!}
    </div>
</div>

Quickstart: Yii2

Yii2 provides dedicated visual code generators in Gii with zero view-template dependencies.

Step 1: Client Generator (ecitizen-client)

Open Gii in your browser (http://localhost/index.php?r=gii), select eCitizen Client Generator, fill in your credentials, and click Generate:

eCitizen Client Generator in Gii

This writes @app/config/ecitizen.php:

use odhis\ecitizen\EcitizenClient;

$ecitizen = new EcitizenClient([
    'apiClientID' => 'YOUR_API_CLIENT_ID',
    'apiKey'      => 'YOUR_API_KEY',
    'secret'      => 'YOUR_SECRET',
    'serviceID'   => 'YOUR_SERVICE_ID',
]);

return $ecitizen;
Step 2: Controller Generator (ecitizen-controller)

Select eCitizen Controller Generator and click Generate:

eCitizen Controller Generator in Gii

This creates @app/controllers/PaymentController.php with self-contained rendering (no separate view files needed):

  • actionPay() — Displays payment summary and payment button.
  • actionNotify() — Receives and cryptographically verifies server-to-server webhook notifications.
  • actionSuccess() — Customer confirmation landing page.
Step 3: Making Payments in Yii2

Visit http://localhost/index.php?r=payment/pay:

eCitizen Payment Pay Screen

Customize parameters on the fly via query parameters: http://localhost/index.php?r=payment/pay&amount=1200&description=Permit+Renewal&phone=0712345678

When payment completes, eCitizen redirects to your confirmation page:

eCitizen Payment Confirmation

Quickstart: Standalone PHP (No Framework)

You can use the gateway directly in any vanilla PHP script or microframework:

1. Initialize Client & Render Payment Button
require_once __DIR__ . '/vendor/autoload.php';

use odhis\ecitizen\EcitizenClient;

$ecitizen = new EcitizenClient([
    'apiClientID' => 'YOUR_API_CLIENT_ID',
    'apiKey'      => 'YOUR_API_KEY',
    'secret'      => 'YOUR_SECRET',
    'serviceID'   => 'YOUR_SERVICE_ID',
]);

echo $ecitizen->payButton([
    'amount'      => 500,
    'reference'   => 'INV-0001',
    'description' => 'School fees',
    'name'        => 'Jane Doe',
    'idNumber'    => '12345678',
    'phone'       => '0712345678', // Automatically formatted for M-Pesa STK push
    'callbackUrl' => 'https://example.com/payment/success',
    'notifyUrl'   => 'https://example.com/payment/notify',
]);
2. Verify Incoming Webhook (notify.php)
require_once __DIR__ . '/vendor/autoload.php';

use odhis\ecitizen\EcitizenClient;

$ecitizen = new EcitizenClient([
    'apiClientID' => 'YOUR_API_CLIENT_ID',
    'apiKey'      => 'YOUR_API_KEY',
    'secret'      => 'YOUR_SECRET',
    'serviceID'   => 'YOUR_SERVICE_ID',
]);

$result = $ecitizen->verify($_POST);

header('Content-Type: application/json');

if ($result['success']) {
    $ref    = $result['reference'];   // e.g. 'INV-0001'
    $paid   = $result['amountPaid'];  // e.g. 500.00
    $status = $result['status'];      // e.g. 'Settled'

    // Update your database / log settlement...

    echo json_encode(['status' => 'ok']);
    exit;
}

http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Signature verification failed']);

Core Features & Usage Details

1. Safaricom M-Pesa STK Push

When the customer provides a phone number in any common Kenyan format (0712345678, +254712345678, or 254712345678), the built-in PhoneHelper automatically normalizes it to international standard 2547XXXXXXXX and sets sendStkPush => true.

This causes eCitizen to immediately initiate an M-Pesa STK push PIN prompt on the customer's phone upon reaching the gateway.

2. Custom Checkout Form or Embedded iFrame (checkout())

If you want to render an embedded <iframe> on your page or submit via custom JavaScript instead of using payButton(), use checkout():

$checkout = $ecitizen->checkout([
    'amount'      => 2500,
    'reference'   => 'APP-4401',
    'description' => 'Building Permit',
    'name'        => 'Grace Mwangi',
    'idNumber'    => '19283746',
    'callbackUrl' => 'https://example.com/payment/success',
    'notifyUrl'   => 'https://example.com/payment/notify',
]);

// Returns:
// [
//     'url' => 'https://payments.ecitizen.go.ke/PaymentAPI/iframev2.1.php',
//     'payload' => [
//         'apiClientID'    => '...',
//         'serviceID'      => '...',
//         'billRefNumber'  => 'APP-4401',
//         'amountExpected' => '2500.00',
//         'currency'       => 'KES',
//         'secureHash'     => '...',
//         ...
//     ]
// ]
Embedded iFrame HTML Example:
<iframe
    src="<?= htmlspecialchars($checkout['url'] . '?' . http_build_query($checkout['payload'])) ?>"
    width="100%"
    height="700px"
    frameborder="0"
    allow="payment">
</iframe>
3. URLs Explained: Browser Redirect vs Server Webhook

eCitizen handles communication through two separate channels:

Parameter Initiated By Purpose Verification
callbackUrl (callBackURLOnSuccess) Customer's Browser Redirects the user back to your site after payment to show a confirmation page. Client-side redirect. Never use this alone to mark orders as paid.
notifyUrl (notificationURL) eCitizen's Server Asynchronous server-to-server webhook (IPN) carrying transaction status and cryptographic HMAC signature. Verified via verify() or isPaid(). Use this to confirm settlement in your database.
4. Bring Your Own Database (BYOD)

The library does not mandate any database tables, migrations, or schema definitions. You are free to persist transactions however your project requires:

// In your notification webhook handler:
if ($result['success']) {
    // Example with Eloquent (Laravel):
    Order::where('reference', $result['reference'])->update([
        'status'      => 'paid',
        'amount_paid' => $result['amountPaid'],
    ]);

    // Example with ActiveRecord (Yii2):
    // $order = Order::findOne(['reference' => $result['reference']]);
    // if ($order) { $order->status = 'paid'; $order->save(false); }

    // Example with PDO (Vanilla PHP):
    // $stmt = $pdo->prepare("UPDATE orders SET status = 'paid' WHERE reference = ?");
    // $stmt->execute([$result['reference']]);
}

File & Class Structure

Path Purpose
src/EcitizenClient.php Main public API (payButton(), checkout(), verify(), isPaid()).
src/EcitizenGateway.php Cryptographic HMAC-SHA256 signature generator and verifier.
src/helpers/PhoneHelper.php Normalizes Kenyan phone numbers for Safaricom M-Pesa STK push.
src/adapters/laravel/EcitizenServiceProvider.php Laravel Service Provider supporting config publishing and DI container binding.
src/adapters/laravel/Facades/Ecitizen.php Laravel Facade for static Ecitizen::... calls.
src/adapters/laravel/config/ecitizen.php Default Laravel configuration template.
src/Bootstrap.php Yii2 extension bootstrap registering Gii code generators.
src/generators/client/ Yii2 Gii Client Generator (ecitizen-client).
src/generators/controller/ Yii2 Gii Controller Generator (ecitizen-controller) with zero view dependencies.
src/interfaces/EcitizenInvoiceInterface.php Optional interface for existing invoice models.
src/controllers/EcitizenPaymentTrait.php Optional trait for webhook handling.

Security

  • Sensitive credentials (API Keys, Secrets, Passwords) should always be stored in environment variables (.env).
  • Never skip HMAC signature verification on inbound webhooks. Always use $ecitizen->verify($postData).
  • Always exempt the notification webhook URL from CSRF middleware so eCitizen's server can deliver payment updates.

License

MIT License. See LICENSE for details.

]]>
0
[news] Yii Sentry 3.0 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/820/yii-sentry-3-0 https://www.yiiframework.com/news/820/yii-sentry-3-0 vjik vjik

Yii Sentry version 3.0.0 was released. In this version:

  • Chg #40, 41, #43: Change PHP version constraint to 8.1 - 8.5
  • Bump sentry/sentry version to ^4.0
  • Add support for Sentry cron monitoring via check-ins
  • Add support for symfony/console version ^7.0
  • Explicitly import ErrorException in "use" section of config/params.php
  • Remove unused yiisoft/di dependency
]]>
0
[news] New website is live Sun, 09 Aug 2026 20:37:30 +0000 https://www.yiiframework.com/news/819/new-website-is-live https://www.yiiframework.com/news/819/new-website-is-live samdark samdark

The Yii website has a fresh new look!

We’ve redesigned it for clearer navigation, better readability, improved mobile support, and consistent dark mode—all while keeping the familiar Yii spirit.

Take a look and let us know what you think!

]]>
0
[news] Yii Auth 3.3 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/818/yii-auth-3-3 https://www.yiiframework.com/news/818/yii-auth-3-3 vjik vjik

Yii auth version 3.3.0 was released. In this version:

  • Split AuthenticationMethodInterface into focused authentication and challenge interfaces
  • Bump minimal PHP version to 8.1
  • Explicitly mark readonly properties
  • Explicitly import classes and functions in "use" section
  • Remove unnecessary files from Composer package
  • Fix authentication scheme in HttpBearer challenge according to RFC 6750
  • Fix HttpHeader authentication to correctly handle header value "0"
]]>
0
[news] Yii DB Migration 2.1 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/817/yii-db-migration-2-1 https://www.yiiframework.com/news/817/yii-db-migration-2-1 vjik vjik

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

  • Add MigrationBuilder::insertBatch() method, deprecate batchInsert()
  • Explicitly import classes, functions, and constants in "use" section
  • Remove unnecessary files from Composer package
  • Remove confirmation prompt from migrate:create command as creating a migration is non-destructive
  • Improve styling of confirmations
  • Improve output of migrate:up, migrate:down, migrate:redo, migrate:new, migrate:history, and migrate:create commands: remove redundant messages, replace >>> with cleaner output, and move "Database connection" info to the top
  • Use newMigrationPath and newMigrationNamespace as source
  • Fix migration namespaces and paths
  • Fix getNamespacePath() matching a sibling namespace as a parent
]]>
0
[extension] casbin/yii-permission Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/casbin/yii-permission https://www.yiiframework.com/extension/casbin/yii-permission techoner techoner

Yii-Permission

  1. Installation
  2. Usage
  3. Define your own model.conf
  4. Learning Casbin
  5. License

Build Status Coverage Status Latest Stable Version Total Downloads License

An authorization library for the Yii 3.0 PHP Framework, based on Casbin.

Installation

Getting Composer package

Require this package in the composer.json of your Yii 3.0 project.

Note: This package requires a database driver implementation for yiisoft/db (such as yiisoft/db-mysql, yiisoft/db-sqlite, yiisoft/db-pgsql, etc.) in your application. Make sure your project has installed a database driver.

If your project doesn't have a database driver yet, install one first (for example, SQLite or MySQL): `bash composer require yiisoft/db-mysql # or yiisoft/db-sqlite or yiisoft/db-pgsql `

composer require casbin/yii-permission
Configuring application

Yii-Permission automatically registers its parameters and DI container definitions via yiisoft/config.

You can customize parameters in your project's config/params.php:

return [
    'casbin/yii-permission' => [
        'model' => [
            // Available Settings: "file", "text"
            'config_type' => 'file',
            'config_file_path' => dirname(__DIR__) . '/config/casbin-basic-model.conf',
            'config_text' => '',
        ],
        'database' => [
            // Connection service ID in DI container, defaults to Yiisoft\Db\Connection\ConnectionInterface::class
            'connection' => null,
            'casbin_rules_table' => 'casbin_rule',
        ],
        'log' => [
            'enabled' => false,
            'logger' => null,
        ],
        'adapter' => \Yii\Permission\Adapter::class,
    ],
];
Database Migration

casbin/yii-permission automatically registers its database migration path (src/migrations) via yiisoft/config under "db-migration".

Run the Yii 3.0 database migration command to create the casbin_rule table (requires yiisoft/db-migration in your application):

composer require yiisoft/db-migration # if not installed yet
./yii migrate:up

Troubleshooting: ConnectionInterface Not Found

If running ./yii migrate:up throws an exception: No definition or class found or resolvable for "Yiisoft\Db\Connection\ConnectionInterface"

It means your Yii 3 application has not registered a default ConnectionInterface in the DI container yet. Ensure your application's DI container (e.g. config/common/di/db.php) defines Yiisoft\Db\Connection\ConnectionInterface::class.

Alternatively, if your database connection service has a custom ID in your container, set it in config/params.php: `php 'casbin/yii-permission' => [

'database' => [
    'connection' => 'your_custom_db_service_id',
],

], `

For more details, see the Yii Database Documentation.

For custom or manual database setups, see the Migration Class File for the detailed casbin_rule table schema.

Usage

Quick start

In Yii 3.0, you can directly inject native \Casbin\Enforcer into your actions, controllers or services to get 100% IDE auto-completion and full type safety:

use Casbin\Enforcer;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;

final readonly class Action
{
    public function __construct(
        private Enforcer $enforcer,
        private ResponseFactoryInterface $responseFactory
    ) {}

    public function __invoke(): ResponseInterface
    {
        // adds permissions to a user with full IDE autocomplete
        $this->enforcer->addPermissionForUser('eve', 'articles', 'read');

        // adds a role for a user
        $this->enforcer->addRoleForUser('eve', 'writer');

        // adds permissions to a policy
        $this->enforcer->addPolicy('writer', 'articles', 'edit');

        // checks permission
        if ($this->enforcer->enforce('eve', 'articles', 'edit')) {
            // permit eve to edit articles
            $response = $this->responseFactory->createResponse();
            $response->getBody()->write('<div>permit is: true</div>');
            return $response;
        } else {
            // deny the request
            $response = $this->responseFactory->createResponse();
            $response->getBody()->write('<div>permit is: false</div>');
            return $response;
        }
    }
}
Using Enforcer Api

It provides a very rich API to facilitate various operations on the Policy:

Gets all roles:

$enforcer->getAllRoles(); // ['writer', 'reader']

Gets all the authorization rules in the policy:

$enforcer->getPolicy();

Gets the roles that a user has:

$enforcer->getRolesForUser('eve'); // ['writer']

Gets the users that have a role:

$enforcer->getUsersForRole('writer'); // ['eve']

Determines whether a user has a role:

$enforcer->hasRoleForUser('eve', 'writer'); // true or false

Adds a role for a user:

$enforcer->addRoleForUser('eve', 'writer');

Adds a permission for a user or role:

// to user
$enforcer->addPermissionForUser('eve', 'articles', 'read');
// to role
$enforcer->addPermissionForUser('writer', 'articles', 'edit');

Deletes a role for a user:

$enforcer->deleteRoleForUser('eve', 'writer');

Deletes all roles for a user:

$enforcer->deleteRolesForUser('eve');

Deletes a role:

$enforcer->deleteRole('writer');

Deletes a permission:

$enforcer->deletePermission('articles', 'read'); // returns false if the permission does not exist (aka not affected).

Deletes a permission for a user or role:

$enforcer->deletePermissionForUser('eve', 'articles', 'read');

Deletes permissions for a user or role:

// to user
$enforcer->deletePermissionsForUser('eve');
// to role
$enforcer->deletePermissionsForUser('writer');

Gets permissions for a user or role:

$enforcer->getPermissionsForUser('eve'); // return array

Determines whether a user has a permission:

$enforcer->hasPermissionForUser('eve', 'articles', 'read');  // true or false

See Casbin API for more APIs.

Using a middleware

casbin/yii-permission provides two PSR-15 middlewares for HTTP route access control in Yii 3.0 applications.

Basic Enforcer Middleware

\Yii\Permission\Middleware\EnforcerMiddleware is used to check explicit permission parameters (e.g. resource and action). It provides an immutable withParams(array $params) method returning a cloned instance for safe route-level parameter binding.

use Yii\Permission\Middleware\EnforcerMiddleware;
use Yiisoft\Router\Route;

// Checks if current user has permission on 'articles' resource with 'read' action
Route::get('/articles')
    ->action([ArticleController::class, 'index'])
    ->middleware(
        fn (EnforcerMiddleware $middleware) => $middleware->withParams(['articles', 'read'])
    );
HTTP Request Middleware ( RESTful is also supported )

\Yii\Permission\Middleware\RequestMiddleware automatically extracts the request Path as the resource and HTTP Method as the action ($enforcer->enforce($userId, $path, $method)).

use Yii\Permission\Middleware\RequestMiddleware;
use Yiisoft\Router\Group;
use Yiisoft\Router\Route;

// Automatically checks permission based on Request Path & HTTP Method
Group::create('/api')
    ->middleware(RequestMiddleware::class)
    ->routes(
        Route::get('/posts')->action([PostController::class, 'index']),
        Route::post('/posts')->action([PostController::class, 'create'])
    );

Note: Both middlewares automatically fetch the current logged-in user ID via Yiisoft\User\CurrentUser::getId(). If your project needs automatic logged-in user resolution, you can install the yiisoft/user package: `bash composer require yiisoft/user ` If CurrentUser is not available or the user is a guest, it falls back to the user_id request attribute or 'guest'.

Using Yii3 AccessChecker ($user->can())

casbin/yii-permission provides \Yii\Permission\AccessChecker implementing Yiisoft\Access\AccessCheckerInterface.

1. Register as AccessCheckerInterface in DI Container (config/common/di/auth.php)

Bind AccessCheckerInterface to AccessChecker so that Yiisoft\User\CurrentUser uses Casbin under the hood:

use Yiisoft\Access\AccessCheckerInterface;
use Yii\Permission\AccessChecker;

return [
    AccessCheckerInterface::class => AccessChecker::class,
];
2. Check Permission via $user->can() in Controllers

Once registered, you can use Yii 3.0's native $user->can() method directly:

use Yiisoft\User\CurrentUser;

final readonly class PostController
{
    public function __construct(
        private CurrentUser $user
    ) {}

    public function update(): ResponseInterface
    {
        // 1. Passing resource and action as separate arguments (Recommended for Casbin)
        if ($this->user->can('articles', ['write'])) {
            // Permission granted
        }

        // 2. Or using comma-separated string format
        if ($this->user->can('articles,write')) {
            // Permission granted
        }

        // 3. Or checking a single permission string
        if ($this->user->can('updatePost')) {
            // Permission granted
        }
    }
}

Define your own model.conf

You can customize your own model configuration file (e.g. casbin-basic-model.conf). For full syntax and pre-defined model examples, see Casbin Supported Models and PHP-Casbin Models.

Learning Casbin

You can find the full documentation of Casbin on the website.

License

This project is licensed under the Apache-2.0 License.

]]>
0
[extension] josemodi97/yii2-ecitizen-payment Thu, 03 Sep 2026 12:24:46 +0000 https://www.yiiframework.com/extension/josemodi97/yii2-ecitizen-payment https://www.yiiframework.com/extension/josemodi97/yii2-ecitizen-payment JoseModi97 JoseModi97

Yii2 eCitizen Payment Extension

  1. What This Package Does
  2. What This Package Does Not Do
  3. Compatibility
  4. Installation
  5. Yii2 Configuration
  6. Environment Variables
  7. Yii2 MVC Pattern
  8. Basic Checkout Usage
  9. Callback URL Building
  10. Example Model: Payment Form
  11. Example Controller: Checkout
  12. Example View: Checkout Iframe
  13. Optional Built-In Notification Controller
  14. Example Callback Action In Your App
  15. Notification Data
  16. Query Payment Status
  17. Localhost and Public Callback URLs
  18. Security Notes
  19. Suggested Database Fields
  20. Publishing To Packagist
  21. Adding To Yii Framework Extensions
  22. Troubleshooting
  23. Development Checks

Yii2 Composer extension for eCitizen iframe payments.

This package wraps the reusable eCitizen payment gateway parts in Yii2 components so you can call them from normal Yii2 MVC code: models/forms validate user input, controllers call Yii::$app->payments->gateway('ecitizen'), and callback actions receive eCitizen asynchronous notifications.

It is intentionally not tied to a particular housing, school, fee, banking-slip, or invoice table. Your Yii application owns the order records, customer records, payment records, and reconciliation workflow.

What This Package Does

  • Builds eCitizen iframe checkout payloads
  • Generates the eCitizen secureHash
  • Validates eCitizen notification signatures
  • Extracts normalized notification data
  • Queries eCitizen payment status by invoice/reference
  • Builds callback URLs from the hosting Yii application
  • Provides an optional notification controller
  • Provides a small iframe helper view

What This Package Does Not Do

  • It does not create your application's invoices or orders.
  • It does not update your housing, student, rent, fee, or banking tables.
  • It does not decide whether a payment should be accepted.
  • It does not store callbacks automatically.
  • It does not reconcile paid invoices into your own accounting records.

The expected pattern is: this extension handles eCitizen communication; your application handles business logic and persistence.

Compatibility

  • PHP 8.0+
  • Yii2 2.0.45+
  • Composer package name: josemodi97/yii2-ecitizen-payment
  • Yii namespace: josemodi97\ecitizen

Installation

All paths in this guide are relative to the root of the Yii2 application that will use this package. For a basic Yii2 app, that is the folder that contains composer.json, config/, controllers/, models/, and views/.

After publishing to Packagist:

composer require josemodi97/yii2-ecitizen-payment

If the package is kept in a local folder, edit the consuming Yii2 app's composer.json:

Path from Yii2 app root: composer.json

{
  "repositories": [
    {
      "type": "path",
      "url": "../yii2-ecitizen-payment"
    }
  ],
  "require": {
    "josemodi97/yii2-ecitizen-payment": "*"
  }
}

Then run:

composer update josemodi97/yii2-ecitizen-payment

Yii2 Configuration

Add the payment component to the Yii2 application config.

Basic Yii2 Template

Add the component in:

  • App config: config/web.php
Advanced Yii2 Template

Add the component in each application config that needs eCitizen access:

  • Frontend: frontend/config/main.php
  • Backend: backend/config/main.php
  • Console: console/config/main.php
'components' => [
    'payments' => [
        'class' => josemodi97\ecitizen\Payment::class,
        'gateways' => [
            'ecitizen' => [
                'class' => josemodi97\ecitizen\gateways\EcitizenGateway::class,
                'apiClientID' => getenv('ECITIZEN_API_CLIENT_ID'),
                'apiKey' => getenv('ECITIZEN_API_KEY'),
                'secret' => getenv('ECITIZEN_SECRET'),
                'serviceID' => getenv('ECITIZEN_SERVICE_ID'),
                'callbackBaseUrl' => getenv('ECITIZEN_CALLBACK_BASE_URL') ?: null,
            ],
        ],
    ],
],

callbackBaseUrl is optional in normal web requests. If it is not set, the gateway tries to derive the base URL from the current Yii request, for example https://housing.example.com. Set it explicitly for console jobs, queue workers, reverse-proxy deployments, or local development through a public HTTPS tunnel.

Full Gateway Options
'ecitizen' => [
    'class' => josemodi97\ecitizen\gateways\EcitizenGateway::class,
    'apiClientID' => getenv('ECITIZEN_API_CLIENT_ID'),
    'apiKey' => getenv('ECITIZEN_API_KEY'),
    'secret' => getenv('ECITIZEN_SECRET'),
    'serviceID' => getenv('ECITIZEN_SERVICE_ID'),
    'currency' => 'KES',
    'url' => 'https://payments.ecitizen.go.ke/PaymentAPI/iframev2.1.php',
    'statusUrl' => 'https://payments.ecitizen.go.ke/api/invoice/payment/status',
    'callbackBaseUrl' => getenv('ECITIZEN_CALLBACK_BASE_URL') ?: null,
    'allowedGatewayHosts' => ['payments.ecitizen.go.ke'],
    'pictureURL' => '',
    'sendSTK' => false,
    'caBundlePath' => null,
    'timeout' => 30,
],

Environment Variables

Create a .env file in the Yii2 application root if your app uses dotenv-style environment loading.

Path from Yii2 basic app root: .env

Path from Yii2 advanced project root: .env

ECITIZEN_API_CLIENT_ID=your_api_client_id
ECITIZEN_API_KEY=your_api_key
ECITIZEN_SECRET=your_secret
ECITIZEN_SERVICE_ID=your_service_id
ECITIZEN_CALLBACK_BASE_URL=https://your-domain.example

Yii2 does not load .env files by default in every template. If your application already loads .env, getenv('ECITIZEN_API_CLIENT_ID') will work as shown above. If it does not, install and bootstrap a dotenv loader in the Yii2 application, or set these variables in your server environment.

If you choose the dotenv approach, install the loader in the Yii2 application:

composer require vlucas/phpdotenv

Example using vlucas/phpdotenv in a Yii2 basic app:

Path from Yii2 app root: web/index.php

require __DIR__ . '/../vendor/autoload.php';

if (class_exists('Dotenv\\Dotenv')) {
    $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
    $dotenv->safeLoad();
}

require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';

Example using vlucas/phpdotenv in a Yii2 advanced app:

Common entry files from project root:

  • Frontend: frontend/web/index.php
  • Backend: backend/web/index.php
  • Console: yii

Load .env before requiring common/config/bootstrap.php or before reading config files:

require __DIR__ . '/../../vendor/autoload.php';

if (class_exists('Dotenv\\Dotenv')) {
    $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2));
    $dotenv->safeLoad();
}

Do not hard-code real eCitizen keys, secrets, service IDs, or callback URLs in code. Move all secrets to environment variables.

Yii2 MVC Pattern

A clean Yii2 integration usually looks like this:

  • Model or form: validates amount, invoice reference, customer name, phone, and email.
  • Controller: receives the user request, creates a local pending payment record, builds the eCitizen payload, and renders the checkout.
  • Callback controller action: receives eCitizen notifications and stores the raw payload.
  • Service or ActiveRecord layer: marks the local order paid only after signature validation and status checks.
  • Reconciliation job: rechecks pending or suspicious payments using queryPaymentStatus().

Basic Checkout Usage

Place these calls inside your own controller action, service class, console command, or model method.

$gateway = Yii::$app->payments->gateway('ecitizen');

$payload = $gateway->createCheckoutPayload([
    'amount' => 1000,
    'reference' => 'INV-1001',
    'description' => 'Application fee',
    'clientName' => 'Jane Doe',
    'clientEmail' => 'jane@example.com',
    'clientPhone' => '0712345678',
]);

$gatewayUrl = $gateway->gatewayUrl();

The payload includes eCitizen fields such as:

apiClientID
amountExpected
serviceID
billRefNumber
billDesc
currency
clientMSISDN
clientName
clientEmail
callBackURLOnSuccess
notificationURL
secureHash

Post the returned payload to $gatewayUrl using a normal form or an iframe.

Callback URL Building

The gateway can build callback URLs from the hosting Yii application:

$notificationUrl = Yii::$app->payments
    ->gateway('ecitizen')
    ->buildCallbackUrl('/ecitizen-payment/ecitizen-notify');

You may override the base URL for one call:

$notificationUrl = Yii::$app->payments
    ->gateway('ecitizen')
    ->buildCallbackUrl('/ecitizen-payment/ecitizen-notify', 'https://tunnel.example.com');

If you do not pass notificationURL or callBackURLOnSuccess to createCheckoutPayload(), the gateway will build defaults:

'notificationURL' => '/ecitizen-payment/ecitizen-notify'
'callBackURLOnSuccess' => '/payment/success'

The final URLs are absolute URLs based on the host Yii application.

Example Model: Payment Form

Path from Yii2 app root: models/EcitizenPaymentForm.php

<?php

namespace app\models;

use yii\base\Model;

class EcitizenPaymentForm extends Model
{
    public $amount;
    public $reference;
    public $description;
    public $clientName;
    public $clientEmail;
    public $clientPhone;

    public function rules()
    {
        return [
            [['amount', 'reference', 'description', 'clientName'], 'required'],
            ['amount', 'number', 'min' => 1],
            [['reference', 'description', 'clientName', 'clientEmail', 'clientPhone'], 'trim'],
            ['reference', 'string', 'max' => 50],
            ['description', 'string', 'max' => 100],
            ['clientEmail', 'email'],
            ['clientPhone', 'string', 'max' => 20],
        ];
    }
}

Example Controller: Checkout

Path from Yii2 app root: controllers/EcitizenController.php

<?php

namespace app\controllers;

use app\models\EcitizenPaymentForm;
use Yii;
use yii\web\Controller;
use yii\web\Response;

class EcitizenController extends Controller
{
    public function actionCreate()
    {
        $model = new EcitizenPaymentForm();

        if ($model->load(Yii::$app->request->post()) && $model->validate()) {
            /*
             * Save your local pending payment record before redirecting/rendering
             * checkout. The extension does not do database writes for you.
             */
            $gateway = Yii::$app->payments->gateway('ecitizen');
            $payload = $gateway->createCheckoutPayload([
                'amount' => $model->amount,
                'reference' => $model->reference,
                'description' => $model->description,
                'clientName' => $model->clientName,
                'clientEmail' => $model->clientEmail,
                'clientPhone' => $model->clientPhone,
                'notificationURL' => $gateway->buildCallbackUrl('/ecitizen-payment/ecitizen-notify'),
                'callBackURLOnSuccess' => $gateway->buildCallbackUrl('/ecitizen/success'),
            ]);

            return $this->render('checkout', [
                'gatewayUrl' => $gateway->gatewayUrl(),
                'payload' => $payload,
            ]);
        }

        return $this->render('create', [
            'model' => $model,
        ]);
    }

    public function actionSuccess()
    {
        return $this->render('success');
    }
}

Example View: Checkout Iframe

Path from Yii2 app root: views/ecitizen/checkout.php

<?php

use yii\helpers\Html;

/** @var string $gatewayUrl */
/** @var array $payload */

$this->title = 'Continue to eCitizen';
?>

<h1><?= Html::encode($this->title) ?></h1>

<form id="ecitizen-checkout-form" method="post" action="<?= Html::encode($gatewayUrl) ?>" target="ecitizen-checkout-frame" autocomplete="off">
    <?php foreach ($payload as $name => $value): ?>
        <?= Html::hiddenInput((string) $name, (string) $value) ?>
    <?php endforeach; ?>
</form>

<iframe name="ecitizen-checkout-frame" title="eCitizen checkout" style="width:100%;min-height:640px;border:0"></iframe>

<?php
$this->registerJs("document.getElementById('ecitizen-checkout-form').submit();");

The package also includes a small reusable partial:

echo $this->render('@vendor/josemodi97/yii2-ecitizen-payment/views/payment/_iframe', [
    'gatewayUrl' => $gatewayUrl,
    'payload' => $payload,
    'frameName' => 'ecitizen-checkout-frame',
]);

Optional Built-In Notification Controller

The package includes a lightweight controller for receiving eCitizen notifications.

Add it to your app config:

'controllerMap' => [
    'ecitizen-payment' => josemodi97\ecitizen\controllers\PaymentController::class,
],

Notification endpoint:

/ecitizen-payment/ecitizen-notify

The built-in action:

  • reads POST data
  • falls back to raw JSON body
  • validates the eCitizen secure hash
  • extracts normalized notification data
  • returns JSON

It does not save to your database. For production systems, create your own callback action or extend the controller so you can store the raw payload and update your payment records.

Example Callback Action In Your App

Path from Yii2 app root: controllers/EcitizenCallbackController.php

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\web\Response;

class EcitizenCallbackController extends Controller
{
    public $enableCsrfValidation = false;

    public function actionNotify()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        $payload = Yii::$app->request->post();
        if ($payload === []) {
            $decoded = json_decode(Yii::$app->request->rawBody, true);
            $payload = is_array($decoded) ? $decoded : [];
        }

        $gateway = Yii::$app->payments->gateway('ecitizen');
        if (!$gateway->validateNotificationHash($payload)) {
            Yii::$app->response->statusCode = 400;
            return ['success' => false, 'message' => 'Invalid eCitizen notification signature.'];
        }

        $notification = $gateway->extractNotification($payload);

        /*
         * Store the raw payload first.
         * Then find your local payment using $notification['reference'].
         * Mark it paid only if the status and amount are acceptable.
         */

        return [
            'success' => true,
            'reference' => $notification['reference'],
        ];
    }
}

Notification Data

extractNotification() returns a normalized array:

[
    'provider' => 'ecitizen',
    'reference' => 'INV-1001',
    'gatewayReference' => 'ECITIZEN-INVOICE-NO',
    'amount' => 1000.00,
    'paymentDate' => '2026-07-18 12:30:00',
    'status' => 'PAID',
    'raw' => [],
]

Use:

josemodi97\ecitizen\gateways\EcitizenGateway::notificationStatusIsPaid($notification['status'])

to check common settled statuses:

PAID
SUCCESS
COMPLETED
SETTLED

Query Payment Status

Use status queries for reconciliation, manual refresh buttons, cron jobs, or when a callback was missed.

$status = Yii::$app->payments
    ->gateway('ecitizen')
    ->queryPaymentStatus('INV-1001');

The extension signs the status query using:

api_client_id
client_invoice_ref
secure_hash

Your application should inspect the response and decide whether the local payment can be marked as paid.

Localhost and Public Callback URLs

Callback actions belong in a web controller because eCitizen calls them over HTTPS.

localhost cannot receive eCitizen callbacks directly because it is only reachable from your own machine. For local development, expose the Yii app through a secure public HTTPS tunnel, then set:

ECITIZEN_CALLBACK_BASE_URL=https://your-tunnel-url.example

For a deployed housing application, you can omit callbackBaseUrl when Yii sees the correct public host. If the app runs behind a proxy or a queue/console command builds the payload, configure it explicitly:

ECITIZEN_CALLBACK_BASE_URL=https://your-public-url.example

Always store the raw callback payload before transforming it. This makes reconciliation much easier when eCitizen sends unexpected fields.

Security Notes

  • Keep callback actions CSRF-exempt because eCitizen will not send a Yii CSRF token.
  • Validate the eCitizen secure hash before trusting a callback.
  • Do not rely only on the user returning to the success URL.
  • Treat the notification URL as the source of asynchronous payment updates.
  • Re-query eCitizen before force-marking old pending payments as paid.
  • Keep credentials out of Git.
  • Use HTTPS for all callback URLs.
  • Keep allowedGatewayHosts restricted to eCitizen hosts.
  • Log failed validation attempts without logging secrets.

Suggested Database Fields

Your host application can use any schema, but these fields are useful:

id
reference
amount
currency
description
customer_name
customer_email
customer_phone
gateway
gateway_reference
status
raw_checkout_payload
raw_callback_payload
created_at
paid_at
last_status_checked_at

Suggested statuses:

pending
paid
failed
cancelled
expired
review_required

Publishing To Packagist

Recommended package name:

josemodi97/yii2-ecitizen-payment

Recommended Yii extension title:

Yii2 eCitizen Payment

Recommended short description:

Yii2 component for eCitizen iframe checkout, notification validation, and payment status queries.

Submit this public repository URL to Packagist:

https://github.com/JoseModi97/yii2-ecitizen-payment

Adding To Yii Framework Extensions

Yii2 extensions are Composer packages. To appear correctly as a Yii extension, the package should:

  • be hosted in a public VCS repository such as GitHub
  • contain a valid composer.json at the repository root
  • use a Yii-style package name such as josemodi97/yii2-ecitizen-payment
  • set Composer package type to yii2-extension
  • require yiisoft/yii2
  • be registered on Packagist

This package already uses:

{
  "name": "josemodi97/yii2-ecitizen-payment",
  "type": "yii2-extension",
  "require": {
    "yiisoft/yii2": "^2.0.45"
  }
}

After the GitHub repository is pushed and Packagist has imported it, open Yii Framework Extensions:

https://www.yiiframework.com/extensions

Search for:

josemodi97/yii2-ecitizen-payment

If it does not appear immediately, wait for Packagist/Yii indexing to refresh. Yii extension discovery depends on public Composer metadata, so the Packagist package must exist first.

Recommended Yii extension title:

Yii2 eCitizen Payment

Recommended Yii extension description:

Yii2 component for eCitizen iframe checkout, notification validation, callback URL generation, and payment status queries.

Troubleshooting

callbackBaseUrl is not configured

This means the gateway could not derive a URL from the current Yii request. Set:

ECITIZEN_CALLBACK_BASE_URL=https://your-public-url.example
eCitizen rejects the checkout payload

Check:

  • apiClientID
  • apiKey
  • secret
  • serviceID
  • amount
  • currency
  • billRefNumber
  • secureHash

Also confirm that the service ID belongs to the configured eCitizen client account.

Notification validation fails

Check that the eCitizen account sending the callback uses the same apiKey and secret configured in the Yii app. Store the raw payload temporarily during testing so you can compare the received fields.

Local callbacks do not arrive

Use a public HTTPS tunnel and configure:

ECITIZEN_CALLBACK_BASE_URL=https://your-tunnel-url.example

Do not use plain http://localhost for real gateway callbacks.

Development Checks

Run PHP syntax checks:

php -l src/gateways/EcitizenGateway.php

Validate Composer metadata:

composer validate --no-check-publish
]]>
0
[news] Yii Bulma 1.1 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/816/yii-bulma-1-1 https://www.yiiframework.com/news/816/yii-bulma-1-1 vjik vjik

Yii Bulma version 1.1.0 was released. In this version:

  • Bump yiisoft/html version to ^4.2.0
  • Adapt configuration to changes in yiisoft/form
  • Change PHP constraint in composer.json to 8.1 - 8.5
  • Adopt last change yiisoft/widget
  • Refactor yiisoft/html classes usage
  • Explicitly import constants in "use" section
  • Explicitly add transitive dependency yiisoft/files
  • Add support for yiisoft/assets version ^3.0 || ^4.0 || ^5.0
  • Allow yiisoft/arrays ^3.0
  • Fixed publish options in assets
]]>
0
[extension] josemodi97/yii2-safaricom-daraja Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/josemodi97/yii2-safaricom-daraja https://www.yiiframework.com/extension/josemodi97/yii2-safaricom-daraja JoseModi97 JoseModi97

yii2-safaricom-daraja

  1. Compatibility
  2. Installation
  3. Quickstart: Yii2 (Dedicated Modular Gii Tools)
  4. Quickstart: Laravel
  5. Quickstart: Standalone PHP
  6. Environment Variables
  7. Basic Usage
  8. Yii2 MVC Pattern
  9. Example Model: STK Push Form
  10. Example Controller
  11. STK Push and Query
  12. C2B URL Registration and Simulation
  13. B2C, B2B, and B2Pochi
  14. Reversal, Transaction Status, and Account Balance
  15. M-Pesa Ratiba Standing Orders
  16. Lipa na Bonga
  17. IMSI and SWAP CheckATI
  18. Pull Transactions API
  19. IoT SIM Portal APIs
  20. All Tools and Endpoints from the Collection
  21. Endpoint Paths
  22. Callback and Result URL Notes
  23. Error Handling
  24. Testing
  25. Notes

A beginner-friendly Safaricom Daraja M-Pesa payment gateway for Yii2, Laravel, and Standalone PHP. Configure credentials directly via Gii, trigger STK Push PIN prompts on customer phones, and verify webhook callbacks with plain-English fields.

  • Direct Gii Credentials Setup: Configure your Safaricom Consumer Key, Secret, Passkey, and ShortCode directly through Gii (daraja-client) — zero manual config file editing required.
  • Instant Payment Controller in Gii: Generate a ready-to-use MpesaController (daraja-controller) with a built-in checkout card, webhook receiver with automatic CSRF exemption, and status querying.
  • Zero Database Obligation: Trigger STK push and receive verified callbacks out of the box without requiring database migrations or tables. Bring your own database models when ready.
  • Automatic Phone Normalization: Automatically converts Kenyan phone numbers (07..., 01..., +254..., 7...) into standard 254... format via PhoneHelper.
  • Laravel Auto-Discovery: Includes native DarajaServiceProvider and Daraja Facade with php artisan vendor:publish support.
  • Framework Agnostic & Standalone Core: Pure PHP (php: >=7.4) with zero forced framework dependencies. Works in Yii2, Laravel, WordPress, or vanilla PHP.
  • Complete Safaricom API Coverage: Full support for STK Push, STK Query, C2B, B2C, B2B, Ratiba, Lipa na Bonga, Pull Transactions, IMSI/SWAP, and IoT SIM Portal APIs.

Compatibility

  • Requires PHP 7.4 or newer (fully tested on PHP 7.4, 8.0, 8.1, 8.2, 8.3, and 8.4+).
  • Yii2: Fully compatible with Yii 2.0.x and Gii code generation.
  • Laravel: Fully compatible with Laravel 6.x, 7.x, 8.x, 9.x, 10.x, and 11.x+.
  • Standalone PHP: Core classes (DarajaClient, Daraja, PhoneHelper) have zero mandatory third-party dependencies.

Installation

Install via Composer:

composer require josemodi97/yii2-safaricom-daraja

Quickstart: Yii2 (Dedicated Modular Gii Tools)

Instead of a cluttered monolithic configuration, each Safaricom Daraja feature has its own dedicated Gii generator with its bare minimum required credentials.

Unified File Architecture: All Gii tools write into the same configuration file (@app/config/daraja.php by default). When you run any tool, it automatically pre-populates existing credentials and merges newly entered settings so previously configured keys are never overwritten or lost!

Gii Generator Link ID Bare Minimum Required Credentials Target File
1. Core API Credentials daraja-client consumerKey, consumerSecret, environment @app/config/daraja.php
2. M-Pesa Express (STK Push) daraja-stk Core Auth + shortCode, passkey @app/config/daraja.php
3. Customer to Business (C2B) daraja-c2b Core Auth + shortCode, responseType @app/config/daraja.php
4. Business to Customer (B2C) daraja-b2c Core Auth + shortCode, initiatorName, securityCredential @app/config/daraja.php
5. Transactions & Balance daraja-transaction Core Auth + shortCode, initiatorName, securityCredential @app/config/daraja.php
6. IoT SIM Management daraja-iot Core Auth + iotApiKey @app/config/daraja.php
7. Payment Controller daraja-controller Controller class name, base class, component ID MpesaController.php
Live Gii Setup Demonstration with Blurred Credentials

Here is a step-by-step walkthrough demonstrating how to configure the extension in Gii using your credentials (visually blurred in the screenshots for security):

DARAJA_CONSUMER_KEY=GKMPa04A74Wy****************************pseu
DARAJA_CONSUMER_SECRET=aUEAoECfvtKW****************************iRn
DARAJA_SHORTCODE=174379
DARAJA_PASSKEY=bfb279f9aa9b****************************c919
Step 1: Access Gii Code Generator

Open http://localhost:8080/index.php?r=gii (or http://localhost/index.php?r=gii). You will see the 7 dedicated Safaricom Daraja generators ready to use:

Safaricom Daraja Gii Overview

Step 2: Configure Core API Credentials (daraja-client)

Click 1. Safaricom Daraja Core API Credentials (daraja-client). Enter your Consumer Key and Consumer Secret, select your environment (Sandbox or Production), preview the generated file, and click Generate:

Safaricom Daraja Core Credentials Setup

Your credentials are saved to @app/config/daraja.php.

Step 3: Configure M-Pesa STK Push (daraja-stk)

Click 2. Safaricom M-Pesa STK Push (Express) (daraja-stk). Notice that your Core API Credentials (consumerKey and consumerSecret) are automatically pre-populated from @app/config/daraja.php!

Enter your ShortCode (174379) and Passkey, check Generate STK Payment Controller, and click Preview:

Safaricom M-Pesa STK Push Setup

Scrolling down reveals the optional controller generator settings and the code file preview table:

Safaricom M-Pesa STK Push Scrolled Preview

Step 4: Live Generation Result & Automatic Route Setup

Upon clicking Generate, Gii creates the files and displays the success alert, controller endpoints, and ready-to-use snippet:

Safaricom Daraja Generation Success

The STK Push credentials are automatically merged into @app/config/daraja.php, and a ready-to-use controller (app\controllers\MpesaStkController) is created with checkout (/mpesa-stk/pay), webhook receiver (/mpesa-stk/callback), and query (/mpesa-stk/query) routes.

Step 5: Unified Configuration Output (@app/config/daraja.php)

Both Core authentication and STK Push settings now live cleanly together in @app/config/daraja.php:

Unified Configuration Output

Yii::$app->daraja is automatically loaded and ready to trigger payments anywhere in your application!

Additional Feature Generators (Transactions & Balance, C2B, B2C)

Why were Steps 1–5 presented first? The quickstart above deliberately spotlights the fundamental onboarding path (Authentication + STK Push) so developers can accept their first mobile money payment in under 5 minutes without wading through a massive wall of images.

However, Safaricom Daraja is modular. Each of the following specialized Gii tools automatically inherits your core credentials and writes cleanly to your unified @app/config/daraja.php:

1. Safaricom M-Pesa Transactions & Balance (daraja-transaction)

Click 5. Safaricom M-Pesa Transactions & Balance to manage account balance inquiries, transaction status queries, and payment reversals. Supply your shortcode, initiator name, security credential, and optional result/timeout webhooks:

Safaricom M-Pesa Transactions & Balance Setup

Code Usage:
// Query Account Balance
$balance = Yii::$app->daraja->checkBalance([
    'remarks' => 'Daily balance check',
]);

// Query Status of Any Transaction
$status = Yii::$app->daraja->queryTransaction([
    'transactionId' => 'NLJ7RT61SV',
    'remarks'       => 'Check transaction status',
]);

// Reverse a Transaction
$reversal = Yii::$app->daraja->reverseTransaction([
    'transactionId' => 'NLJ7RT61SV',
    'amount'        => 500,
    'remarks'       => 'Refund customer overpayment',
]);
2. Safaricom M-Pesa C2B Paybill / Buy Goods Till (daraja-c2b)

Click 3. Safaricom M-Pesa C2B (Paybill / Till) to register your validation and confirmation URLs, configure fallback behavior (Completed vs Cancelled), and optionally generate a dedicated MpesaC2bController:

Safaricom M-Pesa C2B Paybill Setup

Code Usage:
// Register Validation & Confirmation URLs with Safaricom
$response = Yii::$app->daraja->c2bRegister([
    'shortCode'       => '600981',
    'responseType'    => 'Completed', // or 'Cancelled'
    'confirmationUrl' => 'https://yourdomain.com/mpesa-c2b/confirmation',
    'validationUrl'   => 'https://yourdomain.com/mpesa-c2b/validation',
]);

// Simulate C2B Payment (Sandbox only)
$sim = Yii::$app->daraja->c2bSimulate([
    'phone'     => '0708374149',
    'amount'    => 100,
    'reference' => 'INV-001',
]);
3. Safaricom M-Pesa B2C Disbursements (daraja-b2c)

Click 4. Safaricom M-Pesa B2C (Disbursements) to configure automated business payouts (salaries, merchant payments, and promotional disbursements):

Safaricom M-Pesa B2C Disbursements Setup

Code Usage:
$response = Yii::$app->daraja->b2c([
    'phone'     => '0712345678',
    'amount'    => 1500,
    'commandId' => 'SalaryPayment', // Options: SalaryPayment, BusinessPayment, PromotionPayment
    'remarks'   => 'Monthly salary payout',
]);
Using Daraja in Your Code
1. M-Pesa STK Push (Express)
use Safaricom\Daraja\DarajaClient;

// Through the auto-registered application component:
$daraja = Yii::$app->daraja;
// or directly loading the unified config:
$daraja = require Yii::getAlias('@app/config/daraja.php');

// Trigger STK Push (Lipa Na M-Pesa Online):
$response = $daraja->stkPush([
    'phone'       => '0712345678', // Auto-normalized to 254712345678!
    'amount'      => 100,
    'reference'   => 'INV-001',
    'description' => 'Service payment',
]);
2. Verify Webhook Callbacks

In your controller action (e.g. MpesaStkController::actionCallback):

public function actionCallback()
{
    Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
    $daraja = Yii::$app->daraja;

    $result = $daraja->verifyCallback(Yii::$app->request->bodyParams);

    if ($result['success']) {
        $receipt = $result['mpesaReceiptNumber']; // e.g. NLJ7RT61SV
        $amount  = $result['amount'];             // e.g. 100.00
        $phone   = $result['phone'];              // e.g. 254712345678
        // Update your order/database here...
    }

    return ['ResultCode' => 0, 'ResultDesc' => 'Accepted'];
}
3. B2C Disbursements
$response = Yii::$app->daraja->b2c([
    'phone'     => '0712345678',
    'amount'    => 500,
    'commandId' => 'SalaryPayment', // or BusinessPayment, PromotionPayment
    'remarks'   => 'September salary',
]);
4. Query STK Push Transaction Status
$status = Yii::$app->daraja->stkQuery('ws_CO_060920261234567890');

Quickstart: Laravel

The package automatically registers its Service Provider and Daraja Facade via Laravel Package Discovery.

1. Publish Configuration
php artisan vendor:publish --tag=daraja-config
2. Configure .env
DARAJA_ENV=sandbox
DARAJA_CONSUMER_KEY=your_consumer_key
DARAJA_CONSUMER_SECRET=your_consumer_secret
DARAJA_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919
DARAJA_SHORTCODE=174379
DARAJA_CALLBACK_BASE_URL=https://yourdomain.com
3. Trigger STK Push & Handle Callbacks
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Daraja; // or use Safaricom\Daraja\adapters\laravel\Facades\Daraja;

class PaymentController extends Controller
{
    public function pay()
    {
        $response = Daraja::stkPush([
            'phone'       => '0712345678',
            'amount'      => 100,
            'reference'   => 'INV-001',
            'description' => 'Service payment',
            'callbackUrl' => route('mpesa.callback'),
        ]);

        return response()->json($response);
    }

    public function callback(Request $request)
    {
        $result = Daraja::verifyCallback($request->all());

        if ($result['success']) {
            $receipt = $result['mpesaReceiptNumber'];
            $amount  = $result['amount'];
            // Update order status in your database...
        }

        return response()->json(['ResultCode' => 0, 'ResultDesc' => 'Accepted']);
    }
}

CSRF Exemption in Laravel: Exclude your webhook route from CSRF protection:

  • Laravel 11+: Add ->validateCsrfTokens(except: ['mpesa/callback']) in bootstrap/app.php.
  • Laravel 6–10: Add 'mpesa/callback' to $except in app/Http/Middleware/VerifyCsrfToken.php.

Quickstart: Standalone PHP

require_once __DIR__ . '/vendor/autoload.php';

use Safaricom\Daraja\DarajaClient;

$daraja = new DarajaClient([
    'consumerKey'    => 'your_consumer_key',
    'consumerSecret' => 'your_consumer_secret',
    'passkey'        => 'your_passkey',
    'shortCode'      => '174379',
    'environment'    => 'sandbox',
]);

// 1. Trigger STK Push:
$response = $daraja->stkPush([
    'phone'       => '0712345678',
    'amount'      => 100,
    'reference'   => 'INV-001',
    'description' => 'Service payment',
    'callbackUrl' => 'https://example.com/callback.php',
]);

// 2. In callback.php:
$data = json_decode(file_get_contents('php://input'), true);
$result = $daraja->verifyCallback($data);

if ($result['success']) {
    echo "Paid! Receipt: " . $result['mpesaReceiptNumber'];
}

Environment Variables

Create a .env file in the Yii2 application root if your app uses dotenv-style environment loading.

Path from Yii2 basic app root: .env

Path from Yii2 advanced project root: .env

DARAJA_ENVIRONMENT=sandbox
DARAJA_CONSUMER_KEY=your_consumer_key
DARAJA_CONSUMER_SECRET=your_consumer_secret
DARAJA_SHORT_CODE=174379
DARAJA_PASSKEY=your_lipa_na_mpesa_passkey
DARAJA_INITIATOR_NAME=your_initiator_name
DARAJA_INITIATOR_PASSWORD=your_initiator_password
DARAJA_CALLBACK_BASE_URL=https://your-domain.example
DARAJA_IOT_API_KEY=your_iot_api_key
DARAJA_IOT_MSISDN=254700000000

Yii2 does not load .env files by default in every template. If your application already loads .env, getenv('DARAJA_CONSUMER_KEY') will work as shown above. If it does not, install and bootstrap a dotenv loader in the Yii2 application, or set these variables in your server environment.

If you choose the dotenv approach, install the loader in the Yii2 application:

composer require vlucas/phpdotenv

Example using vlucas/phpdotenv in a Yii2 basic app:

Path from Yii2 app root: web/index.php

require __DIR__ . '/../vendor/autoload.php';

if (class_exists('Dotenv\\Dotenv')) {
    $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
    $dotenv->safeLoad();
}

require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';

Example using vlucas/phpdotenv in a Yii2 advanced app:

Common entry files from project root:

  • Frontend: frontend/web/index.php
  • Backend: backend/web/index.php
  • Console: yii

Load .env before requiring common/config/bootstrap.php or before reading config files:

require __DIR__ . '/../../vendor/autoload.php';

if (class_exists('Dotenv\\Dotenv')) {
    $dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__, 2));
    $dotenv->safeLoad();
}

Do not hard-code real consumer keys, secrets, passkeys, initiator passwords, or API keys in code. The Postman collection may contain sample values; move all secrets to environment variables.

Basic Usage

Place these calls inside your own controller action, service class, console command, or model method. The MVC example below uses these paths:

  • Form model: models/StkPushForm.php
  • Controller: controllers/DarajaController.php
  • Optional payment view: views/daraja/stk-push.php

Generate an OAuth access token:

$tokenResponse = Yii::$app->daraja->generateAccessToken();
$accessToken = $tokenResponse['access_token'];

Most API calls do not need you to pass the token manually. The component automatically generates and refreshes the bearer token when consumerKey and consumerSecret are configured.

Use named helper methods where available:

$response = Yii::$app->daraja->stkPush($payload);
$response = Yii::$app->daraja->c2bRegisterUrl($payload);
$response = Yii::$app->daraja->accountBalance($payload);

Use the generic endpoint catalog for any endpoint:

use Safaricom\Daraja\EndpointCatalog;

$response = Yii::$app->daraja->request(EndpointCatalog::PULL_QUERY, [
    'ShortCode' => Yii::$app->params['daraja.shortCode'],
    'StartDate' => '2026-07-01 00:00:00',
    'EndDate' => '2026-07-16 23:59:59',
    'OffSetValue' => '0',
]);

Yii2 MVC Pattern

A clean Yii2 integration usually looks like this:

  • Model or form: validates phone numbers, amount, account reference, date ranges, and required business fields.
  • Controller: receives the user request, builds the Daraja payload, calls the component, and returns a Yii response.
  • Callback controller action: receives Safaricom result/confirmation/validation callbacks and stores them.
  • Service or ActiveRecord layer: saves payment requests, checkout request IDs, transaction IDs, and callback result codes.

Example Model: STK Push Form

Create the form model.

Path from Yii2 app root: models/StkPushForm.php

<?php

namespace app\models;

use Yii;
use yii\base\Model;

class StkPushForm extends Model
{
    public $phoneNumber;
    public $amount;
    public $accountReference;
    public $transactionDesc;

    public function rules()
    {
        return [
            [['phoneNumber', 'amount', 'accountReference'], 'required'],
            ['amount', 'number', 'min' => 1],
            [['accountReference', 'transactionDesc'], 'string', 'max' => 100],
            ['phoneNumber', 'match', 'pattern' => '/^2547[0-9]{8}$/', 'message' => 'Use format 2547XXXXXXXX.'],
        ];
    }

    public function send()
    {
        if (!$this->validate()) {
            return false;
        }

        $shortCode = Yii::$app->params['daraja.shortCode'];
        $passkey = Yii::$app->params['daraja.passkey'];
        $timestamp = date('YmdHis');

        return Yii::$app->daraja->stkPush([
            'BusinessShortCode' => $shortCode,
            'Password' => Yii::$app->daraja->generateStkPassword($shortCode, $passkey, $timestamp),
            'Timestamp' => $timestamp,
            'TransactionType' => 'CustomerPayBillOnline',
            'Amount' => $this->amount,
            'PartyA' => $this->phoneNumber,
            'PartyB' => $shortCode,
            'PhoneNumber' => $this->phoneNumber,
            'CallBackURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/stk-callback'),
            'AccountReference' => $this->accountReference,
            'TransactionDesc' => $this->transactionDesc ? $this->transactionDesc : 'Payment',
        ]);
    }
}

Example Controller

Create the controller.

Path from Yii2 app root: controllers/DarajaController.php

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\web\Response;
use app\models\StkPushForm;

class DarajaController extends Controller
{
    public $enableCsrfValidation = false;

    public function actionStkPush()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        $model = new StkPushForm();
        $model->load(Yii::$app->request->post(), '');

        if (!$model->validate()) {
            return ['ok' => false, 'errors' => $model->getErrors()];
        }

        try {
            return ['ok' => true, 'data' => $model->send()];
        } catch (\Exception $e) {
            Yii::error($e->getMessage(), __METHOD__);
            return ['ok' => false, 'message' => $e->getMessage()];
        }
    }

    public function actionStkCallback()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;

        $raw = Yii::$app->request->getRawBody();
        $payload = json_decode($raw, true);

        Yii::info($payload, 'daraja.stk.callback');

        /*
         * Save the callback to your database here.
         * Common fields:
         * $payload['Body']['stkCallback']['MerchantRequestID']
         * $payload['Body']['stkCallback']['CheckoutRequestID']
         * $payload['Body']['stkCallback']['ResultCode']
         * $payload['Body']['stkCallback']['ResultDesc']
         */

        return ['ResultCode' => 0, 'ResultDesc' => 'Accepted'];
    }
}

STK Push and Query

Start a Lipa na M-Pesa Online payment:

$timestamp = date('YmdHis');
$shortCode = Yii::$app->params['daraja.shortCode'];
$password = Yii::$app->daraja->generateStkPassword($shortCode, Yii::$app->params['daraja.passkey'], $timestamp);

$response = Yii::$app->daraja->stkPush([
    'BusinessShortCode' => $shortCode,
    'Password' => $password,
    'Timestamp' => $timestamp,
    'TransactionType' => 'CustomerPayBillOnline',
    'Amount' => 1,
    'PartyA' => '254700000000',
    'PartyB' => $shortCode,
    'PhoneNumber' => '254700000000',
    'CallBackURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/stk-callback'),
    'AccountReference' => 'INV-1001',
    'TransactionDesc' => 'Invoice payment',
]);

Query an STK payment using the CheckoutRequestID returned by Safaricom:

$response = Yii::$app->daraja->stkQuery([
    'BusinessShortCode' => $shortCode,
    'Password' => $password,
    'Timestamp' => $timestamp,
    'CheckoutRequestID' => 'ws_CO_...',
]);

C2B URL Registration and Simulation

Put the registration/simulation calls in a controller action, console command, or service class. For example:

  • Controller path from Yii2 app root: controllers/DarajaController.php
  • Console command path from Yii2 app root: commands/DarajaController.php

Register confirmation and validation URLs:

$response = Yii::$app->daraja->c2bRegisterUrl([
    'ShortCode' => Yii::$app->params['daraja.shortCode'],
    'ResponseType' => 'Completed',
    'ConfirmationURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/c2b-confirmation'),
    'ValidationURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/c2b-validation'),
]);

Sandbox C2B simulation:

$response = Yii::$app->daraja->c2bSimulate([
    'ShortCode' => Yii::$app->params['daraja.shortCode'],
    'CommandID' => 'CustomerPayBillOnline',
    'Amount' => '10',
    'Msisdn' => '254700000000',
    'BillRefNumber' => 'INV-1001',
]);

Callback examples can be added as methods inside the same web controller.

Path from Yii2 app root: controllers/DarajaController.php

public function actionC2bValidation()
{
    Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
    Yii::info(json_decode(Yii::$app->request->getRawBody(), true), 'daraja.c2b.validation');

    return ['ResultCode' => 0, 'ResultDesc' => 'Accepted'];
}

public function actionC2bConfirmation()
{
    Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
    Yii::info(json_decode(Yii::$app->request->getRawBody(), true), 'daraja.c2b.confirmation');

    return ['ResultCode' => 0, 'ResultDesc' => 'Accepted'];
}

B2C, B2B, and B2Pochi

Put these payout/request examples in your own service class, console command, or controller action.

Suggested paths from Yii2 app root:

  • Service class: components/DarajaService.php
  • Console command: commands/DarajaController.php
  • Web controller: controllers/DarajaController.php

Generate a security credential from your initiator password and Safaricom public certificate:

$credential = Yii::$app->daraja->generateSecurityCredential(
    Yii::$app->params['daraja.initiatorPassword'],
    Yii::getAlias(Yii::$app->params['daraja.certificatePath'])
);

B2C payment request:

$response = Yii::$app->daraja->b2cPayment([
    'InitiatorName' => Yii::$app->params['daraja.initiatorName'],
    'SecurityCredential' => $credential,
    'CommandID' => 'BusinessPayment',
    'Amount' => '100',
    'PartyA' => Yii::$app->params['daraja.shortCode'],
    'PartyB' => '254700000000',
    'Remarks' => 'Payout',
    'QueueTimeOutURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/timeout'),
    'ResultURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/result'),
    'Occasion' => 'Refund',
]);

B2B payment request:

$response = Yii::$app->daraja->b2bPayment([
    'Initiator' => Yii::$app->params['daraja.initiatorName'],
    'SecurityCredential' => $credential,
    'CommandID' => 'BusinessPayBill',
    'SenderIdentifierType' => '4',
    'RecieverIdentifierType' => '4',
    'Amount' => '100',
    'PartyA' => Yii::$app->params['daraja.shortCode'],
    'PartyB' => '600000',
    'AccountReference' => 'INV-1001',
    'Remarks' => 'Supplier payment',
    'QueueTimeOutURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/timeout'),
    'ResultURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/result'),
]);

B2Pochi payment request:

$response = Yii::$app->daraja->b2PochiPayment([
    'OriginatorConversationID' => uniqid('b2pochi-', true),
    'InitiatorName' => Yii::$app->params['daraja.initiatorName'],
    'SecurityCredential' => $credential,
    'CommandID' => 'BusinessPayment',
    'Amount' => '100',
    'PartyA' => Yii::$app->params['daraja.shortCode'],
    'PartyB' => '254700000000',
    'Remarks' => 'B2Pochi payment',
    'QueueTimeOutURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/timeout'),
    'ResultURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/result'),
    'Occasion' => 'Payment',
]);

Reversal, Transaction Status, and Account Balance

Put these examples in your own service class, console command, or controller action.

Suggested paths from Yii2 app root:

  • Service class: components/DarajaService.php
  • Console command: commands/DarajaController.php
  • Web controller: controllers/DarajaController.php

Reverse a transaction:

$response = Yii::$app->daraja->reversal([
    'Initiator' => Yii::$app->params['daraja.initiatorName'],
    'SecurityCredential' => $credential,
    'CommandID' => 'TransactionReversal',
    'TransactionID' => 'ABC123XYZ',
    'Amount' => '100',
    'ReceiverParty' => Yii::$app->params['daraja.shortCode'],
    'RecieverIdentifierType' => '4',
    'ResultURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/result'),
    'QueueTimeOutURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/timeout'),
    'Remarks' => 'Customer refund',
    'Occasion' => 'Refund',
]);

Query transaction status:

$response = Yii::$app->daraja->transactionStatus([
    'Initiator' => Yii::$app->params['daraja.initiatorName'],
    'SecurityCredential' => $credential,
    'CommandID' => 'TransactionStatusQuery',
    'TransactionID' => 'ABC123XYZ',
    'PartyA' => Yii::$app->params['daraja.shortCode'],
    'IdentifierType' => '4',
    'ResultURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/result'),
    'QueueTimeOutURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/timeout'),
    'Remarks' => 'Status query',
    'Occasion' => 'Status',
]);

Query account balance:

$response = Yii::$app->daraja->accountBalance([
    'Initiator' => Yii::$app->params['daraja.initiatorName'],
    'SecurityCredential' => $credential,
    'CommandID' => 'AccountBalance',
    'PartyA' => Yii::$app->params['daraja.shortCode'],
    'IdentifierType' => '4',
    'Remarks' => 'Balance query',
    'QueueTimeOutURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/timeout'),
    'ResultURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/result'),
]);

M-Pesa Ratiba Standing Orders

Put these examples in your own service class, console command, or controller action.

Suggested paths from Yii2 app root:

  • Service class: components/DarajaService.php
  • Console command: commands/DarajaController.php
  • Web controller: controllers/DarajaController.php

Create a standing order for Paybill:

$response = Yii::$app->daraja->ratibaCreatePaybill([
    'StandingOrderName' => 'Monthly fee',
    'BusinessShortCode' => '174379',
    'TransactionType' => 'Standing Order Customer Pay Bill',
    'Amount' => '100',
    'PartyA' => '254700000000',
    'ReceiverPartyIdentifierType' => '4',
    'CallBackURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/ratiba-callback'),
    'AccountReference' => 'ACC-1001',
    'TransactionDesc' => 'Monthly payment',
    'Frequency' => '1',
    'StartDate' => '20260716',
    'EndDate' => '20270716',
]);

Create a standing order for Buy Goods:

$response = Yii::$app->daraja->ratibaCreateBuyGoods([
    'StandingOrderName' => 'Merchant subscription',
    'BusinessShortCode' => '300584',
    'TransactionType' => 'Standing Order Customer Pay Merchant',
    'Amount' => '100',
    'PartyA' => '254700000000',
    'ReceiverPartyIdentifierType' => '2',
    'CallBackURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/ratiba-callback'),
    'AccountReference' => 'ACC-1001',
    'TransactionDesc' => 'Merchant payment',
    'Frequency' => '1',
    'StartDate' => '20260716',
    'EndDate' => '20270716',
]);

Lipa na Bonga

Put these examples in your own service class, console command, or controller action.

Suggested paths from Yii2 app root:

  • Service class: components/DarajaService.php
  • Console command: commands/DarajaController.php
  • Web controller: controllers/DarajaController.php

Redeem Bonga points to Paybill:

$response = Yii::$app->daraja->lipaNaBongaRedeemPaybill([
    'msisdn' => '254700000000',
    'amount' => 100,
    'bongaPoints' => 500,
    'conversionRate' => 0.2,
    'shortCode' => Yii::$app->params['daraja.shortCode'],
    'accountNumber' => 'ACC-1001',
]);

Calculate points:

$response = Yii::$app->daraja->lipaNaBongaCalculatePoints([
    'points' => '500',
]);

IMSI and SWAP CheckATI

Put this example in your own service class, console command, or controller action.

Suggested paths from Yii2 app root:

  • Service class: components/DarajaService.php
  • Console command: commands/DarajaController.php
  • Web controller: controllers/DarajaController.php
$response = Yii::$app->daraja->imsiCheckAti([
    'customerNumber' => '254700000000',
]);

$response = Yii::$app->daraja->swapCheckAti([
    'customerNumber' => '254700000000',
]);

Pull Transactions API

Put these examples in your own service class, console command, or controller action.

Suggested paths from Yii2 app root:

  • Service class: components/DarajaService.php
  • Console command: commands/DarajaController.php
  • Web controller: controllers/DarajaController.php

Register a callback URL:

$response = Yii::$app->daraja->pullRegister([
    'ShortCode' => Yii::$app->params['daraja.shortCode'],
    'RequestType' => 'Pull',
    'NominatedNumber' => '254700000000',
    'CallBackURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/pull-callback'),
]);

Query transactions:

$response = Yii::$app->daraja->pullQuery([
    'ShortCode' => Yii::$app->params['daraja.shortCode'],
    'StartDate' => '2026-07-01 00:00:00',
    'EndDate' => '2026-07-16 23:59:59',
    'OffSetValue' => '0',
]);

IoT SIM Portal APIs

Put these examples in your own service class, console command, or controller action.

Suggested paths from Yii2 app root:

  • Service class: components/DarajaService.php
  • Console command: commands/DarajaController.php
  • Web controller: controllers/DarajaController.php

The IoT SIM portal endpoints from the collection use the same request() engine, but they commonly need additional headers such as x-api-key, x-source-system, X-MSISDN, X-App, and X-MessageID. Use Daraja::iot($endpointKey, $data, $headers, $query).

use Safaricom\Daraja\EndpointCatalog;

$headers = [
    'x-correlation-conversationid' => uniqid('', true),
    'x-source-system' => 'web-portal',
    'x-api-key' => getenv('DARAJA_IOT_API_KEY'),
    'Accept-Language' => 'EN',
    'X-MSISDN' => getenv('DARAJA_IOT_MSISDN'),
    'X-App' => 'web-portal',
    'X-MessageID' => uniqid('msg-', true),
];

$response = Yii::$app->daraja->iot(
    EndpointCatalog::IOT_GET_ALL_MESSAGES,
    ['vpnGroup' => 'MY-GROUP'],
    $headers,
    ['pageNo' => 1, 'pageSize' => 10]
);

The package also exposes named IoT helpers such as iotSearchMessages(), iotSendSingleMessage(), iotAllSims(), and iotSuspendUnsuspendSub(). These helpers call the same endpoints as iot() and accept the same payload/header/query style where paging is needed.

Search messages:

$response = Yii::$app->daraja->iot(
    EndpointCatalog::IOT_SEARCH_MESSAGES,
    ['searchValue' => 'hello', 'vpnGroup' => 'MY-GROUP', 'username' => 'admin'],
    $headers,
    ['pageNo' => 1, 'pageSize' => 5]
);

Send one message:

$response = Yii::$app->daraja->iot(
    EndpointCatalog::IOT_SEND_SINGLE_MESSAGE,
    [
        'msisdn' => '254700000000',
        'message' => 'Test message',
        'vpnGroup' => 'MY-GROUP',
        'username' => 'admin',
    ],
    $headers
);

SIM activation:

$response = Yii::$app->daraja->iot(
    EndpointCatalog::IOT_SIM_ACTIVATION,
    ['msisdn' => '254700000000', 'vpnGroup' => 'MY-GROUP', 'username' => 'admin'],
    $headers
);

All Tools and Endpoints from the Collection

Import the constant class where you need generic access:

use Safaricom\Daraja\EndpointCatalog;
Tool / API from Postman Helper method Endpoint constant
OAuth access token generateAccessToken() EndpointCatalog::OAUTH_TOKEN
M-Pesa Ratiba Paybill standing order ratibaCreatePaybill($data) EndpointCatalog::RATIBA_CREATE_PAYBILL
M-Pesa Ratiba Buy Goods standing order ratibaCreateBuyGoods($data) EndpointCatalog::RATIBA_CREATE_BUY_GOODS
B2B payment request b2bPayment($data) EndpointCatalog::B2B_PAYMENT
B2C payment request b2cPayment($data) EndpointCatalog::B2C_PAYMENT
B2Pochi payment request b2PochiPayment($data) EndpointCatalog::B2POCHI_PAYMENT
C2B URL registration c2bRegisterUrl($data) EndpointCatalog::C2B_REGISTER_URL
C2B simulation c2bSimulate($data) EndpointCatalog::C2B_SIMULATE
STK Push process request stkPush($data) EndpointCatalog::STK_PUSH
STK Push query stkQuery($data) EndpointCatalog::STK_QUERY
Transaction reversal reversal($data) EndpointCatalog::REVERSAL
Transaction status query transactionStatus($data) EndpointCatalog::TRANSACTION_STATUS
Account balance query accountBalance($data) EndpointCatalog::ACCOUNT_BALANCE
Lipa na Bonga redeem Paybill lipaNaBongaRedeemPaybill($data) EndpointCatalog::LIPA_NA_BONGA_REDEEM_PAYBILL
Lipa na Bonga calculate points lipaNaBongaCalculatePoints($data) EndpointCatalog::LIPA_NA_BONGA_CALCULATE_POINTS
IMSI CheckATI imsiCheckAti($data) EndpointCatalog::IMSI_CHECK_ATI
SWAP CheckATI swapCheckAti($data) EndpointCatalog::SWAP_CHECK_ATI
Pull Transactions register URL pullRegister($data) EndpointCatalog::PULL_REGISTER
Pull Transactions query pullQuery($data) EndpointCatalog::PULL_QUERY
IoT search messages iotSearchMessages($data, $headers, $query) or iot() EndpointCatalog::IOT_SEARCH_MESSAGES
IoT filter messages iotFilterMessages($data, $headers, $query) or iot() EndpointCatalog::IOT_FILTER_MESSAGES
IoT delete message thread iotDeleteMessageThread($data, $headers) or iot() EndpointCatalog::IOT_DELETE_MESSAGE_THREAD
IoT get all messages iotGetAllMessages($data, $headers, $query) or iot() EndpointCatalog::IOT_GET_ALL_MESSAGES
IoT send single message iotSendSingleMessage($data, $headers) or iot() EndpointCatalog::IOT_SEND_SINGLE_MESSAGE
IoT delete message iotDeleteMessage($data, $headers) or iot() EndpointCatalog::IOT_DELETE_MESSAGE
IoT all SIMs iotAllSims($data, $headers, $query) or iot() EndpointCatalog::IOT_ALL_SIMS
IoT query lifecycle status iotQueryLifecycleStatus($data, $headers) or iot() EndpointCatalog::IOT_QUERY_LIFECYCLE_STATUS
IoT query customer info iotQueryCustomerInfo($data, $headers) or iot() EndpointCatalog::IOT_QUERY_CUSTOMER_INFO
IoT SIM activation iotSimActivation($data, $headers) or iot() EndpointCatalog::IOT_SIM_ACTIVATION
IoT get activation trends iotGetActivationTrends($data, $headers) or iot() EndpointCatalog::IOT_GET_ACTIVATION_TRENDS
IoT rename asset iotRenameAsset($data, $headers) or iot() EndpointCatalog::IOT_RENAME_ASSET
IoT get location info iotGetLocationInfo($data, $headers) or iot() EndpointCatalog::IOT_GET_LOCATION_INFO
IoT suspend / unsuspend subscriber iotSuspendUnsuspendSub($data, $headers) or iot() EndpointCatalog::IOT_SUSPEND_UNSUSPEND_SUB

Endpoint Paths

All paths use the configured base URL:

  • Sandbox: https://sandbox.safaricom.co.ke
  • Production: https://api.safaricom.co.ke
Constant Method Path
OAUTH_TOKEN GET /oauth/v1/generate?grant_type=client_credentials
RATIBA_CREATE_PAYBILL POST /standingorder/v1/createStandingOrderExternal
RATIBA_CREATE_BUY_GOODS POST /standingorder/v1/createStandingOrderExternal
B2B_PAYMENT POST /mpesa/b2b/v1/paymentrequest
B2C_PAYMENT POST /mpesa/b2c/v1/paymentrequest
B2POCHI_PAYMENT POST /mpesa/b2c/v1/paymentrequest
C2B_REGISTER_URL POST /mpesa/c2b/v1/registerurl
C2B_SIMULATE POST /mpesa/c2b/v1/simulate
STK_PUSH POST /mpesa/stkpush/v1/processrequest
STK_QUERY POST /mpesa/stkpushquery/v1/query
REVERSAL POST /mpesa/reversal/v1/request
TRANSACTION_STATUS POST /mpesa/transactionstatus/v1/query
ACCOUNT_BALANCE POST /mpesa/accountbalance/v1/query
LIPA_NA_BONGA_REDEEM_PAYBILL POST /v1/lipa/na/bonga/redeem-paybill
LIPA_NA_BONGA_CALCULATE_POINTS POST /v1/lipa/na/bonga/calculator-points
IMSI_CHECK_ATI POST /imsi/v1/checkATI
SWAP_CHECK_ATI POST /imsi/v2/checkATI
PULL_REGISTER POST /pulltransactions/v1/register
PULL_QUERY POST /pulltransactions/v1/query
IOT_SEARCH_MESSAGES POST /simportal/v1/searchmessages
IOT_FILTER_MESSAGES POST /simportal/v1/filtermessages
IOT_DELETE_MESSAGE_THREAD POST /simportal/v1/deleteMessageThread
IOT_GET_ALL_MESSAGES POST /simportal/v1/getallmessages
IOT_SEND_SINGLE_MESSAGE POST /simportal/v1/sendsinglemessage
IOT_DELETE_MESSAGE POST /simportal/v1/deletemessage
IOT_ALL_SIMS POST /simportal/v1/allsims
IOT_QUERY_LIFECYCLE_STATUS POST /simportal/v1/queryLifeCycleStatus
IOT_QUERY_CUSTOMER_INFO POST /simportal/v1/querycustomerinfo
IOT_SIM_ACTIVATION POST /simportal/v1/simactivation
IOT_GET_ACTIVATION_TRENDS POST /simportal/v1/getactivationtrends
IOT_RENAME_ASSET POST /simportal/v1/renameasset
IOT_GET_LOCATION_INFO POST /simportal/v1/getlocationinfo
IOT_SUSPEND_UNSUSPEND_SUB POST /simportal/v1/suspend_unsuspend_sub

Callback and Result URL Notes

Callback actions belong in a web controller because Safaricom calls them over HTTPS.

Suggested path from Yii2 app root: controllers/DarajaController.php

Safaricom sends many responses asynchronously. Any payload with ResultURL, QueueTimeOutURL, CallBackURL, ConfirmationURL, or ValidationURL must point to a publicly reachable HTTPS URL.

Use the component helper when building callback payload fields:

'CallBackURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/stk-callback'),
'ResultURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/result'),
'QueueTimeOutURL' => Yii::$app->daraja->buildCallbackUrl('/daraja/timeout'),

For a deployed housing application, you can omit callbackBaseUrl when Yii sees the correct public host. If the app runs behind a proxy or a queue/console command builds the payload, configure it explicitly:

DARAJA_CALLBACK_BASE_URL=https://your-public-url.example

localhost cannot receive Safaricom callbacks directly because it is only reachable from your own machine. For local development, expose the Yii app through a secure public HTTPS tunnel, then use that tunnel URL as DARAJA_CALLBACK_BASE_URL.

Always store the raw callback JSON before transforming it. This makes reconciliation much easier when Safaricom sends unexpected fields.

Error Handling

Failed HTTP responses throw Safaricom\Daraja\DarajaException.

try {
    $response = Yii::$app->daraja->accountBalance($payload);
} catch (\Safaricom\Daraja\DarajaException $e) {
    Yii::error($e->getMessage(), 'daraja');
    throw $e;
}

Testing

Run the package tests:

vendor/bin/phpunit

The included tests check the endpoint catalog and component behavior. Real API calls require valid Safaricom credentials and publicly reachable callback URLs.

Notes

  • The extension does not hard-code credentials from the Postman collection.
  • Put credentials in environment variables or Yii application params.
  • Keep callback actions CSRF-exempt because Safaricom will not send a Yii CSRF token.
  • Keep your production and sandbox credentials separate.
  • Store IDs returned by Safaricom, especially MerchantRequestID, CheckoutRequestID, ConversationID, OriginatorConversationID, and transaction IDs.
]]>
0
[extension] mspirkov/yii2-phpstan-rules Wed, 02 Sep 2026 13:30:16 +0000 https://www.yiiframework.com/extension/mspirkov/yii2-phpstan-rules https://www.yiiframework.com/extension/mspirkov/yii2-phpstan-rules max-s-lab max-s-lab

993323

Yii2 PHPStan rules

  1. Support
  2. Installation
  3. Configuration
  4. Rules at a glance
  5. Rule reference

A set of PHPStan rules for Yii2 projects that I put together for my own day-to-day work. Yii2 leans heavily on loosely-typed config arrays and magic properties/methods that PHPStan can't see through on its own, and on conventions — like keeping business logic and database access out of controllers and views — that are easy to drift from without anyone noticing. These rules catch both: they validate Yii2-specific config and structure statically, and they enforce the architectural boundaries and other code-quality checks I try to keep in a codebase. In my experience they help keep a Yii2 codebase a bit cleaner and more maintainable, but they're just my opinions turned into checks, not a universal standard — use what's useful, ignore or disable the rest.

PHP Yii2 PHPStan Total Downloads Tests Coverage PHPStan Level Max

Support

If you like this project, give it a ⭐ on GitHub — it helps others discover it.

Installation

[!IMPORTANT]

It works better with the latest versions of PHP, Yii2, and PHPStan. The more up-to-date the versions are, the more accurate the analysis is.

php composer.phar require --dev mspirkov/yii2-phpstan-rules

If your project uses phpstan/extension-installer, the rules are picked up automatically — nothing else to do.

Otherwise, include them manually in your phpstan.neon:

includes:
    - vendor/mspirkov/yii2-phpstan-rules/rules.neon

Configuration

All rules are on by default. Turn the whole set off, turn off just one of the two rule groups, or tune individual rules, under parameters.mspirkovYii2Rules:

parameters:
    mspirkovYii2Rules:
        # Master switch — false disables every rule below
        enableAllRules: false

        # Covers just the `*Validation` rules (config/shape checks like modelRulesValidation,
        # componentBehaviorsValidation, activeQueryWithValidation, ...) — defaults to
        # enableAllRules, so setting it only makes sense when it should differ. Here it keeps
        # static config validation on while the `no*` code-quality rules stay off.
        enableValidationRules: true

        # Component IDs treated as "the database" by the DB-access rules
        yiiAppDbProperties:
            - db

        # Classes to skip in config-array validation (unknown option / wrong option type
        # checks) — shared by every rule that validates a class against a config array:
        # baseObjectInstantiationValidation, yiiCreateObjectValidation,
        # componentBehaviorsValidation, controllerActionsValidation,
        # widgetPropertiesValidation, and modelRulesValidation. Useful when a class's
        # constructor consumes some config keys itself instead of leaving them for
        # Yii::configure() to apply to a public, checkable property.
        # Omit "attributes" to skip the class entirely; list them to skip only those options.
        baseObjectConfigValidation:
            skippedClasses:
                -
                    class: app\payment\PaymentGateway
                    attributes:
                        - retryPolicy
                -
                    class: app\sdk\ThirdPartySdkClient

        # Thresholds for the complexity rules — exceeding any one flags the method
        actionComplexity:
            ifCount: 3
            foreachCount: 0
            forCount: 0
            whileCount: 0
            doWhileCount: 0
            switchCount: 0
            matchCount: 0
            ternaryCount: 1
            tryCatchCount: 1

        # Yii application properties allowed to be read anywhere (e.g. request-agnostic settings)
        noForbiddenYiiAppProperties:
            allowedProperties:
                - id
                - name
                - charset
                - language
                - timeZone

        # Project-specific model validator aliases
        modelRulesValidation:
            customValidators:
                slug: app\validators\SlugValidator

        # Disable a single rule without touching the rest
        noDynamicQueryWhere:
            enabled: false

Rules at a glance

Validation rules

Statically validate Yii2's loosely-typed config arrays and array-driven conventions — shapes PHPStan can't check on its own because they only take effect at runtime. Toggle all of them at once with enableValidationRules.

Rule Catches
activeFormFieldValidation ActiveForm::field() calls targeting an attribute that is missing, read-only, or write-only on the given model
activeQueryWithValidation with() / joinWith() / innerJoinWith() calls referencing a relation that doesn't exist on the queried ActiveRecord model
activeRecordConditionValidation findOne() / findAll() / deleteAll() / updateAll() / updateAllCounters() WHERE conditions with an unknown attribute or a mismatched value type
activeRecordRelationValidation Invalid hasOne() / hasMany() link properties that do not exist on the current or related ActiveRecord model
activeRecordUpdateValuesValidation updateAll() / updateAllCounters() attribute or counter values with an unknown attribute or a mismatched value type
baseObjectInstantiationValidation new on a yii\base\BaseObject subclass whose last constructor argument is a $config array, with bad config keys and bad option types
behaviorAttributesValidation TimestampBehavior/BlameableBehavior/SluggableBehavior/AttributeTypecastBehavior/DateTimeBehavior options naming an unknown model attribute
componentBehaviorsValidation Malformed or invalid behaviors() in yii\base\Component — unknown behavior classes, bad config keys, and bad option types
controllerActionsValidation Malformed or invalid actions() in yii\base\Controller — unknown action classes, bad config keys, and bad option types
htmlActiveAttributeValidation Html::activeInput() / activeTextInput() / etc. calls referencing an attribute that does not exist on the given model
modelAttributeHintsValidation attributeHints() entries in yii\base\Model that target attributes that don't exist, or use an empty attribute name
modelAttributeLabelsValidation attributeLabels() entries in yii\base\Model that target attributes that don't exist, or use an empty attribute name
modelRulesValidation Malformed or invalid rules() in yii\base\Model — unknown validators, missing required options, bad regexes, unknown attributes, and more
modelScenariosValidation scenarios() entries in yii\base\Model with an empty name, a non-array attribute list, or an unknown attribute
queryConditionValidation where() / andWhere() / orWhere() operator-format conditions (in, between, like, etc.) with the wrong number of operands
uploadedFileInstanceValidation UploadedFile::getInstance() / getInstances() calls referencing an attribute that does not exist on the given model
widgetPropertiesValidation Unknown or mistyped option keys and bad option types in Widget::begin() / Widget::widget() config arrays
yiiCreateObjectValidation Yii::createObject() config arrays missing class/__class, bad config keys, and bad option types
Code quality rules

Catch architectural drift, complexity, and other code-quality issues that are easy to miss without anyone noticing — business logic and database access staying out of controllers and views, actions calling other actions directly, superglobals, dynamic SQL, an Application object that anything can read from or write to, and calls that are provably redundant.

Rule Catches
noComplexActionClasses Standalone yii\base\Action classes with too much branching/looping — logic that belongs in a service
noComplexControllerActions The same, for controller actions
noControllerActionCallsViaThis $this->actionFoo() inside a controller instead of a redirect or shared method
noDbQueriesInActions Direct DB/ActiveRecord access in Action classes
noDbQueriesInControllers Direct DB/ActiveRecord access in controllers
noDbQueriesInViews Direct DB/ActiveRecord access in view files
noDirectSuperglobals Direct use of $_GET, $_POST, $_SESSION, etc.
noDynamicQueryWhere String-concatenated conditions passed to Query::where() / andWhere() / orWhere()
noForbiddenYiiAppProperties Reads of arbitrary yii\base\Application components, including Yii::$app->*
noRedundantExistenceCheck Query::one() !== null / Query::count() compared against 0 or 1 where Query::exists() suffices
noRedundantHtmlEncode Html::encode() calls whose argument is always a numeric-string
noYiiAppPropertyMutation Writes to yii\base\Application properties, including setComponents()

Rule reference

Validation rules
Active Form field validation

ActiveForm::field($model, $attribute) binds an editable input to the attribute: it reads the current value to render the input, and writes the submitted value back to the model on load(). This rule checks that the attribute is both readable and writable — a declared (non-readonly) property, a PHPDoc @property, or a matching getter/setter pair — and reports it whether it's missing entirely or only exists as read-only or write-only. yii\base\DynamicModel instances (and subclasses) are skipped entirely, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.

/**
 * @property string $email
 * @property-read string $fullName
 */
final class ContactModel extends Model
{
    public $name;

    public function getPhone(): string { /* ... */ }

    public function setPhone(string $phone): void { /* ... */ }
}
/** @var ContactModel $model */

$form = ActiveForm::begin();

echo $form->field($model, 'name');     // ✓ declared property
echo $form->field($model, 'email');    // ✓ declared via @property
echo $form->field($model, 'phone');    // ✓ has both getPhone() and setPhone()
echo $form->field($model, 'fullName'); // ✗ read-only — declared via @property-read, nothing to write the submitted value back to
echo $form->field($model, 'nickname'); // ✗ typo — "nickname" is not a property on ContactModel

ActiveForm::end();
ActiveQuery with() validation

with(), joinWith(), and innerJoinWith() take relation names as plain strings, so a typo (or a relation that got renamed) silently returns no related data instead of failing. This rule checks that every relation name passed to these methods — including a joinWith()/innerJoinWith() alias ('orders o' or 'orders AS o') and a dotted sub-relation path ('orders.items') — resolves to an actual relation (a getXxx() method returning something compatible with yii\db\ActiveQueryInterface) on the queried model.

Validating a sub-relation requires knowing which model the parent relation points to. This rule can work that out two ways: from the relation getter's own @return ActiveQuery<T> PHPDoc, or from a @property-read T / @property-read T[] PHPDoc property of the same name on the model (the same resolution activeFormFieldValidation and friends already rely on). A relation whose target model can't be determined either way is still checked for existence at its own level, but any further sub-relation path past it is left unchecked rather than guessed at.

/**
 * @property-read Address $address
 */
class Customer extends ActiveRecord
{
    /** @return ActiveQuery<Order> */
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }

    public function getAddress()
    {
        return $this->hasOne(Address::class, ['id' => 'address_id']);
    }
}

class Order extends ActiveRecord
{
    /** @return ActiveQuery<Item> */
    public function getItems()
    {
        return $this->hasMany(Item::class, ['order_id' => 'id']);
    }
}

class Address extends ActiveRecord
{
    /** @return ActiveQuery<Country> */
    public function getCountry()
    {
        return $this->hasOne(Country::class, ['id' => 'country_id']);
    }
}

class Item extends ActiveRecord { /* ... */ }
class Country extends ActiveRecord  { /* ... */ }
Customer::find()->with('orders')->all();           // ✓
Customer::find()->with('orders.items')->all();     // ✓ Order declares its own "items" relation
Customer::find()->with('address.country')->all();  // ✓ related model resolved via @property-read
Customer::find()->joinWith('orders o')->all();     // ✓ alias is stripped before the relation is checked
Customer::find()->with('oders')->all();             // ✗ typo — no such relation on Customer
Customer::find()->with('orders.oops')->all();       // ✗ typo — no such relation on Order
Active Record condition validation

findOne(), findAll(), and deleteAll() take a plain array condition (['attribute' => value], with an array value matched as an IN (...) condition) — and so does the second, condition argument of updateAll() / updateAllCounters(). Like attributeLabels() and scenarios(), this is never checked against the model until the query actually runs. This rule checks that every attribute name in a condition array exists on the queried ActiveRecord model (the same @property-aware resolution as activeRecordRelationValidation) and that its value's type is compatible with the attribute's declared type. Only array literals with a resolvable string key are checked; primary-key-only lookups (findOne(1), findOne([1, 2])) and dynamically-built condition arrays are left alone. A value implementing yii\db\ExpressionInterface (e.g. new Expression('NOW()')) is accepted for any attribute regardless of its declared type — yii\db\conditions\HashConditionBuilder builds it as raw SQL instead of type-casting it, and does so per-value inside an IN (...) array too.

/**
 * @property int $id
 * @property int $status
 * @property string $updated_at
 */
final class Customer extends ActiveRecord { /* ... */ }

Customer::findOne(1);                                         // ✓ primary key lookup, not a condition hash
Customer::findOne(['status' => 1]);                           // ✓
Customer::findOne(['status' => [1, 2]]);                      // ✓ IN (1, 2)
Customer::findOne(['updated_at' => new Expression('NOW()')]); // ✓ raw SQL, not type-checked
Customer::findOne(['statuss' => 1]);                          // ✗ typo — unknown attribute
Customer::findOne(['status' => '1']);                         // ✗ wrong type — int expected
Customer::deleteAll(['statuss' => 1]);                        // ✗ typo — unknown attribute
Customer::updateAll(['status' => 1], ['idd' => 5]);           // ✗ typo — unknown attribute in the condition
Active Record relations validation

hasOne() and hasMany() relation links are plain string arrays: the array keys belong to the related AR class, and the values belong to the current AR class. This rule checks that those properties exist, including properties declared through PHPDoc @property.

/**
 * @property int $id
 * @property int $customer_id
 * @property int $shipping_address_id
 */
final class Order extends ActiveRecord
{
    public function getShippingAddress(): ActiveQuery
    {
        // ✗ missing property "uuid" on Address
        return $this->hasOne(Address::class, ['uuid' => 'shipping_address_id']);
    }

    public function getItems(): ActiveQuery
    {
        // ✗ missing property "order_uuid" on Order
        return $this->hasMany(OrderItem::class, ['order_id' => 'order_uuid']);
    }

    public function getCustomer(): ActiveQuery
    {
        // ✓
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

/**
 * @property int $id
 */
final class Customer extends ActiveRecord { /* ... */ }

/**
 * @property int $id
 */
final class Address extends ActiveRecord { /* ... */ }

/**
 * @property int $id
 * @property int $order_id
 */
final class OrderItem extends ActiveRecord { /* ... */ }
Active Record update values validation

updateAll()'s attribute values and updateAllCounters()'s counter values are the other plain array these two methods take — the values written into the row, as opposed to the WHERE condition activeRecordConditionValidation checks. This rule checks that every attribute name exists on the ActiveRecord model and that its value's type is compatible with the attribute's declared type; unlike a condition, these values are written as-is, so (unlike activeRecordConditionValidation) an array value is not treated as an IN (...) shorthand and is always a type mismatch. As with a condition, a value implementing yii\db\ExpressionInterface is accepted for any attribute regardless of its declared type — yii\db\QueryBuilder::prepareUpdateSets() builds it as raw SQL instead of type-casting it.

/**
 * @property int $id
 * @property int $status
 * @property int $age
 * @property string $updated_at
 */
final class Customer extends ActiveRecord { /* ... */ }

Customer::updateAll(['status' => 1], ['id' => 5]);              // ✓
Customer::updateAll(['updated_at' => new Expression('NOW()')]); // ✓ raw SQL, not type-checked
Customer::updateAll(['statuss' => 1]);                          // ✗ typo — unknown attribute
Customer::updateAll(['status' => 'active']);                    // ✗ wrong type — int expected
Customer::updateAllCounters(['age' => 1]);                      // ✓
Customer::updateAllCounters(['agee' => 1]);                     // ✗ typo — unknown attribute
BaseObject instantiation validation

yii\base\BaseObject::__construct($config = []) applies $config via Yii::configure($this, $config), the same mechanism Yii::createObject() uses to apply its own config array — so a typo'd key or wrong-typed value in a plain new SomeObject([...]) call is just as invisible to PHPStan as it is in a createObject() config array. This rule checks a new call the same way yiiCreateObjectValidation checks Yii::createObject(): config keys against the target class's writable properties, and literal values against their declared types. It only looks at classes extending yii\base\BaseObject, and only at a literal array passed as the constructor's last argument when that argument is exactly the one named $config — the Yii2 convention for opting into array-config construction. A subclass whose last parameter isn't named config (or is variadic) doesn't follow that convention, so its last argument is left alone.

$countQuery = Article::find()->where(['status' => 1]);

new Pagination(['totalCount' => $countQuery->count()]);  // ✓
new Pagination(['totalCoutn' => 100]);                   // ✗ typo — unknown option "totalCoutn"
new Pagination(['defaultPageSize' => '20']);             // ✗ wrong type — int expected
Behavior attributes validation

TimestampBehavior, BlameableBehavior, SluggableBehavior, AttributeTypecastBehavior, and mspirkov/yii2-db's DateTimeBehavior all fill in specific model attributes on their own — createdAtAttribute/updatedAtAttribute, createdByAttribute/updatedByAttribute, attribute/slugAttribute, attributeTypes, and the attributes event map every AttributeBehavior subclass inherits — and none of that is checked against the model until the behavior actually runs. This rule checks that every attribute name these options reference (a literal string, or an array of them) actually exists on the model declaring behaviors(), the same @property-aware resolution modelRulesValidation and modelAttributeLabelsValidation use. yii\base\DynamicModel instances are skipped, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.

/**
 * @property string $title
 * @property string $slug
 * @property string $created_at
 */
final class Post extends ActiveRecord
{
    public function behaviors(): array
    {
        return [
            [
                'class' => TimestampBehavior::class,
                'createdAtAttribute' => 'created_at',   // ✓
                'updatedAtAttribute' => 'udpated_at',   // ✗ typo — unknown attribute
            ],
            [
                'class' => SluggableBehavior::class,
                'attribute' => 'titel',                 // ✗ typo — unknown attribute
                'slugAttribute' => 'slug',              // ✓
            ],
            [
                'class' => AttributeTypecastBehavior::class,
                'attributeTypes' => [
                    'created_at' => AttributeTypecastBehavior::TYPE_STRING, // ✓
                ],
            ],
        ];
    }
}

TimestampBehavior, BlameableBehavior, SluggableBehavior, and DateTimeBehavior all extend yii\behaviors\AttributeBehavior, so instead of (or alongside) their own shorthand options, any of them can be configured directly through the inherited attributes event map — and this rule checks that map's attribute names the same way, on whichever of the four (or a custom AttributeBehavior subclass) it appears on:

/**
 * @property string $created_at
 * @property string|null $updated_at
 */
final class Article extends ActiveRecord
{
    public function behaviors(): array
    {
        return [
            [
                'class' => TimestampBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], // ✓
                ],
            ],
            [
                'class' => BlameableBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_INSERT => ['created_by', 'updated_by'], // ✗ neither attribute exists
                ],
            ],
            [
                'class' => SluggableBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_VALIDATE => 'slug',     // ✗ unknown attribute
                ],
            ],
            [
                'class' => DateTimeBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_UPDATE => 'updated_at', // ✓
                ],
            ],
        ];
    }
}
Component behaviors validation

Component::behaviors() uses Yii object configs, so typos usually wait until runtime. This rule checks statically visible behavior definitions on yii\base\Component subclasses, including models: classes that do not extend yii\base\Behavior, bad config keys, unknown config options, and option value types inferred from public properties or setters.

public function behaviors(): array
{
    return [
        'timestamp' => [
            'class' => TimestampBehavior::class,
            'createdAtAtribute' => 'created_at',     // ✗ typo — unknown option
        ],
        'typecast' => [
            'class' => AttributeTypecastBehavior::class,
            'attributeTypes' => [
                'views_count' => AttributeTypecastBehavior::TYPE_INTEGER,
                'is_published' => AttributeTypecastBehavior::TYPE_BOOLEAN,
            ],
            'typecastAfterValidate' => 1,            // ✗ bool expected
        ],
        'invalid' => stdClass::class,                // ✗ not a yii\base\Behavior

        'slug' => [
            'class' => SluggableBehavior::class,
            'attribute' => 'title',                  // ✓
        ],
    ];
}
Controller actions validation

Controller::actions() shares the same object-config shape as Component::behaviors() — this rule checks statically visible action definitions on yii\base\Controller subclasses: classes that do not extend yii\base\Action, an empty action ID, bad config keys, unknown config options, and option value types inferred from public properties or setters.

public function actions(): array
{
    return [
        'error' => [
            'class' => ErrorAction::class,
            'vieww' => 'error',            // ✗ typo — unknown option
        ],
        'captcha' => [
            'class' => CaptchaAction::class,
            'fixedVerifyCode' => 1,        // ✗ string expected
        ],
        'invalid' => stdClass::class,      // ✗ not a yii\base\Action

        'download' => [
            'class' => DownloadAction::class,
            'path' => '@app/uploads',      // ✓
        ],
    ];
}
Html active attribute validation

Html::activeInput(), activeTextInput(), and the rest of the active*() family (activeHiddenInput, activePasswordInput, activeFileInput, activeTextarea, activeRadio, activeCheckbox, activeDropDownList, activeListBox, activeCheckboxList, activeRadioList, activeLabel, activeHint) all take a model and a plain attribute-name string, the same as ActiveForm::field(). This rule checks that the attribute exists on the given model, the same @property-aware resolution used by activeFormFieldValidation and uploadedFileInstanceValidation. yii\base\DynamicModel instances are skipped, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.

/**
 * @property string $email
 */
final class ContactModel extends Model
{
    public $name;
}

/** @var ContactModel $model */

echo Html::activeLabel($model, 'name');    // ✓ declared property
echo Html::activeInput('text', $model, 'email');  // ✓ declared via @property
echo Html::activeTextInput($model, 'nema');       // ✗ typo — "nema" is not a property on ContactModel
echo Html::activeHint($model, 'nickname');        // ✗ typo — "nickname" is not a property on ContactModel
Model attribute hints validation

Model::attributeHints() is just as easy to get wrong as attributeLabels() — a typo'd key silently means the hint is never shown for the intended attribute. This rule checks that every key is an existing property on the model (as a declared property or a PHPDoc @property, same resolution as modelRulesValidation) and isn't left empty — though the existence check alone is skipped for yii\base\DynamicModel subclasses, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically:

/**
 * @property string $email
 */
final class ContactModel extends Model
{
    public $name;

    public function attributeHints(): array
    {
        return [
            'name' => 'Your full name',
            'emial' => 'We will reply here',   // ✗ typo — "emial" is not a property on ContactModel
            'email' => 'We will reply here',   // ✓ declared via @property
        ];
    }
}
Model attribute labels validation

Model::attributeLabels() is just as easy to get wrong as rules() — a typo'd key silently falls back to the default humanized attribute name instead of showing your label. This rule checks that every key is an existing property on the model (as a declared property or a PHPDoc @property, same resolution as modelRulesValidation) and isn't left empty — though the existence check alone is skipped for yii\base\DynamicModel subclasses, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically:

/**
 * @property string $email
 */
final class ContactModel extends Model
{
    public $name;

    public function attributeLabels(): array
    {
        return [
            'name' => 'Name',
            'emial' => 'E-mail',   // ✗ typo — "emial" is not a property on ContactModel
            'email' => 'E-mail',   // ✓ declared via @property
        ];
    }
}
Model validation rules validation

Model::rules() is just a plain array — PHP will never tell you that you forgot a validator's required option, wrote an invalid regex, misconfigured one of its options, or targeted an attribute that doesn't even exist. For every rule entry the validator type resolves to (a built-in alias like required/string/number/compare/date/match/in/unique/exist/file/image/ip/url, a custom Validator subclass, a configured project alias, or an inline closure/method), this rule statically checks the option array against what that validator actually accepts and requires. A validator name it can't resolve is reported as an error; add project-specific aliases under modelRulesValidation.customValidators:

public function rules(): array
{
    return [
        ['email', 'string', 'lenght' => 255],             // ✗ typo — unknown option "lenght" for StringValidator
        ['code', 'match', 'pattern' => '/[/'],            // ✗ invalid regular expression
        ['ip', 'ip', 'ipv4' => false, 'ipv6' => false],   // ✗ disables both protocols
        ['message', 'string', 'max' => 'invalid'],        // ✗ 'max' must be int|null
        ['status', 'someUnregisteredAlias'],              // ✗ unknown validator

        ['name', 'string', 'max' => 255],                 // ✓
    ];
}

This rule also checks that the attribute names at index 0 of each rule (including array lists of attributes) actually exist on the model, the same way activeRecordRelationValidation checks relation links — as a declared property or a PHPDoc @property. It only reports on attribute names it can resolve to a literal or constant string; anything built dynamically at runtime is left alone. This check alone is skipped for yii\base\DynamicModel subclasses, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.

/**
 * @property string $email
 */
final class ContactModel extends Model
{
    public $name;

    public function rules(): array
    {
        return [
            ['name', 'required'],
            ['emial', 'required'],   // ✗ typo — "emial" is not a property on ContactModel
            ['email', 'string'],     // ✓ declared via @property
        ];
    }
}
Model scenarios validation

Model::scenarios() maps scenario names to the attributes active in them, and PHP won't tell you that a scenario name is empty, an attribute list isn't actually an array, or an attribute doesn't exist on the model — the same way modelAttributeLabelsValidation checks attributeLabels(). An attribute prefixed with ! (Yii's "unsafe" marker) is checked under its unprefixed name. The attribute-existence check alone is skipped for yii\base\DynamicModel subclasses, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.

final class ContactModel extends Model
{
    public $name;
    public $email;

    public function scenarios(): array
    {
        return [
            'create' => ['name', 'email'],
            'update' => ['name', '!emial'],  // ✗ typo — "emial" is not a property on ContactModel
            '' => ['name'],                  // ✗ empty scenario name
            'delete' => 'name',              // ✗ must be an array of attribute names
        ];
    }
}
Query condition validation

Query::where() / andWhere() / orWhere() accept an "operator format" array ([operator, operand1, operand2, ...]), and Yii only discovers a missing operand at query-build time — each yii\db\conditions\*Condition::fromArrayDefinition() throws an InvalidArgumentException if its required operands aren't present. This rule checks the operand count against those same rules: not, between / not between, in / not in, like and its variants, and exists / not exists each need a specific minimum (or, for not, an exact) number of operands, and the standard comparison operators (=, !=, <>, >, >=, <, <=) need exactly 2, Yii's documented "arbitrary operator" case. and / or operands are recursed into, since they typically wrap further operator-format sub-conditions; yii\db\conditions\ConjunctionCondition itself never validates their count, but a zero-operand and/or can never produce a meaningful condition, so this rule still requires at least one. Any other operator string — a genuinely custom one registered via QueryBuilder::setConditionClasses() — is left unchecked rather than guessed at, and so is anything built dynamically or in hash format (['status' => 1], never operator-format to begin with).

$query->where(['in', 'status']);                                    // ✗ missing the values operand — needs 2
$query->andWhere(['between', 'age', 18]);                           // ✗ missing the upper bound — needs 3
$query->orWhere(['not', ['in', 'status']]);                         // ✗ same as above, nested inside "not"
$query->where(['>=', 'age', 18, 30]);                               // ✗ arbitrary operator, extra operand — needs exactly 2
$query->where(['and']);                                             // ✗ empty "and" — needs at least 1 operand

$query->where(['in', 'status', [1, 2]]);                            // ✓
$query->andWhere(['between', 'age', 18, 65]);                       // ✓
$query->orWhere(['and', ['status' => 1], ['in', 'type', [1, 2]]]);  // ✓
UploadedFile instance validation

UploadedFile::getInstance($model, $attribute) and getInstances($model, $attribute) build the file input's name from $model and a plain attribute-name string, the same way ActiveForm::field() does — so a typo silently returns null (or an empty array) instead of the uploaded file. This rule checks that the attribute exists on the given model, the same @property-aware resolution used elsewhere (e.g. activeFormFieldValidation, modelAttributeLabelsValidation). yii\base\DynamicModel instances are skipped, since their attributes are defined at runtime via defineAttribute() and can't be resolved statically.

final class UploadForm extends Model
{
    public $imageFile;
    public $imageFiles;

    public function rules(): array
    {
        return [[['imageFile', 'imageFiles'], 'file']];
    }
}

/** @var UploadForm $model */

$model->imageFile = UploadedFile::getInstance($model, 'imageFile');    // ✓
$model->imageFiles = UploadedFile::getInstances($model, 'imageFiles'); // ✓
$model->imageFile = UploadedFile::getInstance($model, 'imagefile');    // ✗ typo — "imagefile" is not a property on UploadForm
$files = UploadedFile::getInstances($model, 'imagefiles');             // ✗ typo — "imagefiles" is not a property on UploadForm
Widget properties validation

Widget::begin($config) / Widget::widget($config) configs are just arrays, like behaviors(), so a typo'd key or a wrong-typed value only fails once the widget renders. This rule checks config keys against the called widget's writable properties and literal values against their declared types.

ActiveForm::begin([
    'method' => 'get',             // ✓ declared property
    'metod' => 'get',              // ✗ typo — unknown option "metod"
    'encodeErrorSummary' => 'yes', // ✗ wrong type — bool expected, string given
]);

ActiveForm::end();
Yii::createObject() validation

Yii::createObject()'s class / __class config array is declared as an open, all-optional PHPStan array shape (array{class?: class-string<T>, __class?: class-string<T>, ...}), so PHPStan itself already flags an unknown or wrong-typed class value — but it stays silent about a missing class/__class key entirely (just an unhelpful "unable to resolve the template type" note) and about every other key in the array, since ... accepts anything. This rule fills exactly those two gaps on calls to createObject() on Yii (or any class extending yii\BaseYii): a clear "must specify class or __class" message, plus config keys checked against the resolved class's writable properties and value types, the same way componentBehaviorsValidation checks behaviors. Callables (a Closure, or a [$target, 'method'] array) are left alone, since they are not object configs.

Yii::createObject([
    'traceLevel' => 3,        // ✗ missing "class" or "__class"
]);

Yii::createObject([
    'class' => Logger::class,
    'traceLevel' => '3',      // ✗ wrong type — int expected
    'flushInteval' => 1000,   // ✗ typo — unknown option (should be "flushInterval")
]);

Yii::createObject([
    'class' => Logger::class,
    'traceLevel' => 3,        // ✓
]);
Code quality rules
Complexity limits

noComplexActionClasses and noComplexControllerActions count if, foreach, for, while, do-while, switch, match, ternaries, and try/catch blocks inside a controller action or Action::run(). Cross any configured threshold and the rule fires, pointing at the exact construct that pushed it over:

// ✗ flagged: 4 `if` statements against a default limit of 3
public function actionCheckout(): string
{
    if ($this->cart->isEmpty()) { /* ... */ }
    if (!$this->cart->hasPaymentMethod()) { /* ... */ }
    if ($this->cart->hasOutOfStockItems()) { /* ... */ }
    if ($this->cart->hasExpiredCoupon()) { /* ... */ }

    return $this->render('checkout', ['cart' => $this->cart]);
}

// ✓ the decision tree moves to a service, the action just orchestrates
public function actionCheckout(): string
{
    return $this->render('checkout', $this->checkoutService->process($this->cart));
}
No calling actions via $this
// ✗ flagged: bypasses the action-resolution pipeline (filters, events, results)
public function actionEdit(int $id): Response
{
    return $this->actionView($id);
}

// ✓ redirect, or extract the shared part into a private method / service
public function actionEdit(int $id): Response
{
    return $this->redirect(['view', 'id' => $id]);
}
No database access outside repositories

Fires on self::find()/findOne()/save(), Yii::$app->db, Yii::$app->db->createCommand(), creating or configuring a Query, transactions, and friends — wherever they turn up in a controller, an Action, or a view file.

// ✗ flagged in a view: queries the database instead of just rendering data
<?php foreach (Post::find()->where(['status' => 1])->all() as $post): ?>

// ✓ the controller/action fetches the data, the view only renders it
<?php foreach ($posts as $post): ?>

noDbQueriesInActions / noDbQueriesInControllers push the same query building into a repository or service instead. Query builder setup counts too: new Query(), $query->where(), and dynamic calls on a Query object are all treated as direct database access in these layers.

No raw superglobals

Covers $_GET, $_POST, $_REQUEST, $_SESSION, $_COOKIE, $_FILES, and $_SERVER, each pointing at the matching yii\web\Request / Session / UploadedFile API.

// ✗ flagged, with the fix suggested in the error message
$id = $_GET['id'];

// ✓ read through the injected yii\web\Request instead
$id = $this->request->get('id');
No dynamic SQL strings

Applies to where(), andWhere(), and orWhere() alike. The check is purely structural — it flags any interpolated or concatenated string passed as the condition, regardless of what the string contains (an IN (...) list is just as flagged as a plain = comparison) — and leaves the array condition syntax, including its ['in', 'column', $values] operator form, untouched.

// ✗ flagged: string-built condition, one step from SQL injection
$query->where("status = $status");
$query->andWhere('status = ' . $status);
$query->orWhere("id IN ($ids)");

// ✓ array condition syntax — parameterized, and PHPStan can see the shape
$query->where(['status' => $status]);
$query->andWhere(['in', 'id', $ids]);
No forbidden Yii::$app properties

Checks any expression typed as yii\base\Application, not just Yii::$app directly. A short allowlist (id, name, charset, language, timeZone by default) stays available everywhere since those are effectively static configuration, not injectable services.

// ✗ arbitrary component access
$cache = Yii::$app->cache;

// ✓ inject the component instead
public function __construct(private CacheInterface $cache) {}
No redundant existence check

Query::exists() runs a lighter SELECT EXISTS(...) query instead of fetching a row (one()) or counting every matching row (count()). This rule catches the common ways a record-existence check like this ends up written as one of those instead, on any expression typed as yii\db\QueryInterface / yii\db\ActiveQueryInterface — the comparison can be written with the query call on either side, and count() > 0 / count() !== 0 (or, negated, count() < 1 / count() === 0) are flagged the same way as one() !== null / one() === null:

// ✗ flagged: fetches a full row just to test whether one is there
public function emailIsTaken(string $email): bool
{
    return User::find()->where(['email' => $email])->one() !== null;
}

// ✗ flagged: counts every matching row just to test whether one is there
public function emailIsAvailable(string $email): bool
{
    return User::find()->where(['email' => $email])->count() < 1;
}

// ✓ exists() only asks the database whether a row is there
public function emailIsTaken(string $email): bool
{
    return User::find()->where(['email' => $email])->exists();
}

// ✓ same, negated
public function emailIsAvailable(string $email): bool
{
    return !User::find()->where(['email' => $email])->exists();
}
No redundant Html::encode()

PHPStan already flags most nonsensical Html::encode() calls on its own (wrong argument types and the like). The one gap it doesn't cover is a numeric-string argument: a value PHPStan can already prove only ever holds digits, so escaping it can't do anything — htmlspecialchars() never touches a plain number. This rule fires only in that narrow case, on yii\helpers\Html / BaseHtml and their subclasses:

/**
 * @var numeric-string $id
 * @var string $name
 */

echo Html::encode($id);   // ✗ flagged — $id can only ever be a numeric-string
echo Html::encode($name); // ✓ a plain string may still contain special characters
No Yii::$app property mutation

Checks the same yii\base\Application-typed expressions as noForbiddenYiiAppProperties, on the write side.

// ✗ mutation of properties
Yii::$app->params = [...];
Yii::$app->setComponents([...]);
]]>
0
[extension] tzabzlat/yii2-sentry Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/tzabzlat/yii2-sentry https://www.yiiframework.com/extension/tzabzlat/yii2-sentry tzabzlat tzabzlat

Yii2 Sentry

  1. Features
  2. Installation
  3. Configuration
  4. Built-in Collectors
  5. Usage
  6. How Collectors Work
  7. Creating a Custom Collector
  8. License

Latest Stable Version License PHP Version Require

Read this in other languages: English, Русский

Complete Sentry integration for Yii2 framework: logging, tracing and profiling.

Features

  • Tracking errors and exceptions through Yii2 logs
  • Database performance monitoring (slow queries, transactions)
  • HTTP request tracing (incoming and outgoing)
  • Manual spans for tracking performance of critical operations
  • Flexible data collection configuration through collector system
  • Sanitization of sensitive data (passwords, tokens, API keys)

Installation

Install the package via composer:

composer require tzabzlat/yii2-sentry

For using performance profiling features, you need to install the PHP Excimer extension.

Configuration

Basic Configuration

Add to your application configuration (not common):

'bootstrap' => ['sentry'],
'log'          => [
    'logger'  => 'tzabzlat\yii2sentry\Logger',
]
'components' => [
    'sentry' => [
        'class' => 'tzabzlat\yii2sentry\SentryComponent',
        'dsn' => 'https://your-sentry-dsn@sentry.io/project',
        'environment' => YII_ENV,
        // Sampling rate (percentage of requests for performance metrics collection)
        'tracesSampleRatePercent' => YII_ENV_PROD ? 5 : 100,
        // Additional tags for all events
        'tags' => [
            'application' => 'app-api',
            'app_version' => '1.0.0',
        ],
    ],
]

Built-in Collectors

The package includes four main collectors, each responsible for its own monitoring area:

1. LogCollector

Collects and sends Yii2 logs with error and warning levels to Sentry. Allows configuring which logs should be sent and which should be ignored.

2. DbCollector

Tracks SQL queries, measures their performance, and creates spans in Sentry for analysis. Automatically marks slow queries. Also tracks database transactions.

3. HttpClientCollector

Tracks outgoing HTTP requests made through Yii2 HttpClient. Measures response time, records response status, and creates spans for visualizing HTTP dependencies.

4. RequestCollector

Tracks incoming HTTP requests to your application. Creates the main transaction for each request and collects information about the controller, action, processing time, and response status.

Usage

Manual Spans for Custom Operations

To create spans manually, use the trace method:

// Simple span
Yii::$app->sentry->trace('Operation name', function() {
    // Your code here
    heavyOperation();
});

// With additional data
Yii::$app->sentry->trace(
    'Data import', 
    function() {
        // Import data
        return $result;
    }, 
    'custom.import', // Operation type
    [
        'source' => 'api',
        'records_count' => $count
    ]
);

// Span with exception handling
try {
    Yii::$app->sentry->trace('Critical operation', function() {
        // In case of an exception, the span will be marked as failed
        throw new \Exception('Error!');
    });
} catch (\Exception $e) {
    // The exception will be caught here
    // The span is already marked as failed in Sentry
}
Collector Configuration

You can configure each collector separately through the collectorsConfig parameter:

'sentry' => [
    'class' => 'tzabzlat\yii2sentry\SentryComponent',
    'dsn' => env('SENTRY_DSN', ''),
    'environment' => YII_ENV,
    'tracesSampleRatePercent' => YII_ENV_PROD ? 20 : 100,
    'collectorsConfig' => [
        // LogCollector configuration
        'logCollector' => [
            'targetOptions' => [
                'levels' => ['error', 'warning'], // Log levels to send
                'except' => ['yii\web\HttpException:404'], // Exceptions
                'exceptMessages' => [
                    '/^Informational message/' => true, // Exclude by pattern
                ],
            ],
        ],
        
        // DbCollector configuration
        'dbCollector' => [
            'slowQueryThreshold' => 100, // Threshold in ms for slow queries
        ],
        
        // HttpClientCollector with sensitive URL masking
        'httpClientCollector' => [
            'urlMaskPatterns' => [
                '|https://api\.telegram\.org/bot([^/]+)/|' => 'https://api.telegram.org/bot[HIDDEN]/',
            ],
        ],
        
        // RequestCollector configuration
        'requestCollector' => [
            'captureUser' => true, // Capture user ID
        ],
    ],
]
Disabling Collectors

To disable a specific collector, set its configuration to false:

'collectorsConfig' => [
    'dbCollector' => false, // Disables the database collector
    'httpClientCollector' => false, // Disables the HTTP client collector
],

How Collectors Work

LogCollector

Connects a special LogTarget that intercepts logs with specified levels and sends them to Sentry. Processes exceptions as a separate type of event. Also supports filtering by categories and message patterns.

DbCollector

Overrides the standard Yii2 DbCommand and connects to query profiling events. Measures the execution time of each SQL query, determines the query type (SELECT, INSERT, etc.), and creates spans for visualization in Sentry. Tracks transactions through Connection events.

HttpClientCollector

Subscribes to request sending events through HttpClient. For each request, it creates a span with details of URL, method, headers, and request body (with sanitization of sensitive data). Measures response time and adds information about the response status.

RequestCollector

Creates the main transaction for each incoming HTTP request. Collects information about the route, controller, action, request parameters, and response. Measures the total request processing time and peak memory usage.

Creating a Custom Collector

You can create your own collector by implementing the CollectorInterface or extending the BaseCollector class:

namespace app\components\sentry;

use tzabzlat\yii2sentry\collectors\BaseCollector;
use tzabzlat\yii2sentry\SentryComponent;
use Sentry\Breadcrumb;
use Sentry\State\Scope;
use Yii;

class MyCustomCollector extends BaseCollector
{
    // Collector configuration
    public $someOption = 'default';
    
    /**
     * Attaches the collector to Sentry
     */
    public function attach(SentryComponent $sentryComponent): bool
    {
        parent::attach($sentryComponent);
        
        // Connect to Yii2 events
        \yii\base\Event::on(SomeClass::class, SomeClass::EVENT_NAME, function($event) {
            $this->handleEvent($event);
        });
        
        return true;
    }
    
    /**
     * Sets additional tags
     */
    public function setTags(Scope $scope): void
    {
        $scope->setTag('custom_tag', 'value');
    }
    
    /**
     * Handles a custom event
     */
    protected function handleEvent($event)
    {
        // Create a span for tracking
        $span = $this->sentryComponent->startSpan(
            'My Custom Operation',
            'custom.operation',
            [
                'key' => 'value',
                'event_type' => get_class($event)
            ]
        );
        
        // Add a breadcrumb to the timeline
        $this->addBreadcrumb(
            'My Event Happened',
            ['data' => 'value'],
            Breadcrumb::LEVEL_INFO,
            'custom'
        );
        
        // Finish the span
        if ($span) {
            $this->sentryComponent->finishSpan($span, [
                'result' => 'success',
                'additional_data' => $someValue
            ]);
        }
    }
}

Then add your collector to the configuration:

'sentry' => [
    // ...
    'collectorsConfig' => [
        'myCustomCollector' => [
            'class' => 'app\components\sentry\MyCustomCollector',
            'someOption' => 'custom value',
        ],
    ],
],
Contributing

If you found a bug or have suggestions for improvement, feel free to:

  • Create an issue with a description of the problem or suggestion
  • Propose pull requests with fixes or new features

License

MIT

]]>
0
[news] Yii Bootstrap5 1.2 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/815/yii-bootstrap5-1-2 https://www.yiiframework.com/news/815/yii-bootstrap5-1-2 vjik vjik

Yii Bootstrap 5 version 1.2.0 was released.

In this version:

  • Add tabIndex() method to Button widget
  • Raise yiisoft/html version to ^3.13 || ^4.0
  • Fix Collapse re-encoding content already managed by Toggler
]]>
0
[extension] darktoolz/yii2-rexfilter Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/darktoolz/yii2-rexfilter https://www.yiiframework.com/extension/darktoolz/yii2-rexfilter darktoolz darktoolz

darktoolz/yii2-rexfilter

composer require darktoolz/yii2-rexfilter "@dev"
]]>
0
[extension] yiirocks/recaptcha Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/yiirocks/recaptcha https://www.yiiframework.com/extension/yiirocks/recaptcha Thoulah Thoulah

Yii3 reCAPTCHA

  1. Features
  2. Requirements
  3. Installation
  4. Documentation

Google reCAPTCHA v2 and v3 form field + server-side validator for Yii3.

Packagist Version PHP from Packagist Packagist Downloads GitHub License GitHub Workflow Status

Stats for Nerds

Coverage MSI Tests Assertions

Features

  • reCAPTCHA v2 — checkbox/invisible widget field with theme, size, and type options
  • reCAPTCHA v3 — score-based field that fetches its token on form submit (not page load), avoiding surprise challenge popups
  • Server-side validation — PHP attribute rules (RecaptchaV2Rule / RecaptchaV3Rule) verified against Google's siteverify endpoint
  • Score threshold + action matching — v3 rules can enforce a minimum score and an expected action name
  • Zero-config ergonomics — once site keys are set, fields and rules work out of the box via a static registry
  • i18n — validation and legal-notice messages are translated through Yii Translator

Requirements

  • PHP 8.3+
  • PSR-17 request + stream factories
  • PSR-18 HTTP client

Installation

composer require yiirocks/recaptcha

Documentation

The complete reference guide is available at Yii.Rocks.

]]>
0
[extension] yiirocks/voyti Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/yiirocks/voyti https://www.yiiframework.com/extension/yiirocks/voyti Thoulah Thoulah

Voyti — Yii3 User Management Extension

  1. Features
  2. Requirements
  3. Views Implementation
  4. Installation
  5. Documentation

войти
/vɐjˈtʲi/
verb

"to enter" or "to log in"

Highly customizable and extensible user management, authentication, and authorization extension for Yii3.

Originally ported from Usuario, Voyti has since been rebuilt around modern PSR standards and Yiisoft components. It has been extensively redesigned to provide a flexible, modular foundation that adapts to a wide range of authentication and authorization requirements.

Packagist Version PHP from Packagist Packagist GitHub GitHub Workflow Status

Stats for Nerds

Coverage MSI Tests Assertions

Features

  • User Management — Registration, email confirmation, login/logout with remember-me, password recovery, password expiration
  • Profile Management — User profiles with gravatar, timezone, bio, and a personal website link
  • Social Authentication — OAuth2 login via Google, GitHub, Facebook, and more
  • Two-Factor Authentication — Email codes, TOTP (authenticator app) with QR provisioning, or WebAuthn/passkeys, with enforced-per-permission support and one-time backup codes for account recovery
  • RBAC Management — Full admin UI for roles, permissions, and rules with parent-child hierarchy, assignment management, and filtering
  • Identity Switching — Admins can temporarily switch into another user's identity for support or debugging, then restore their own session with one click
  • Session Management — Session tracking and termination
  • GDPR Data Handling — Data export and account anonymization
  • Password Policies — Minimum complexity requirements, max age enforcement via middleware
  • Email Change Confirmation — Three modes: immediate, confirm new address, confirm both old and new
  • REST API: User Management — JSON user CRUD (Bearer-token auth), with optional per-user request throttling (429 responses, X-Rate-Limit-* headers)
  • REST API: Client Login & Self-Service — Credential login, account/session self-service, admin RBAC/audit-log, and dynamic 2FA/social-login/GDPR bridges over JSON, for a browser-based single-page application (SPA) or any other stateless client
  • Bot Protection — Google reCAPTCHA v2/v3 for registration and login forms
  • Brute-Force Protection — Exponential backoff delays for failed login and registration attempts, tracked per IP address
  • i18n — Built-in translations for multiple languages
  • Pluggable Views — View-agnostic core with Bootstrap 5 views available; alternative UI frameworks can implement the standard interface
  • Email Customization — Mail templates are independently overridable for complete control over transactional email content and styling
  • Toast Notifications — Native Bootstrap toast support with automatic fallback to flash messages

Requirements

  • PHP 8.3+
  • ext-intl
  • Various yiisoft packages (automatic installation via composer)

Views Implementation

Voyti's core is view-agnostic; you need a views implementation package to render any pages. yiirocks/voyti-views-bootstrap5 is the reference implementation using Bootstrap 5. You can substitute an alternative views package if you prefer a different UI framework, as long as it implements the yiirocks/voyti-views interface.

Installation

composer require yiirocks/voyti yiirocks/voyti-views-bootstrap5

Documentation

The complete reference guide is available at Yii.Rocks.

]]>
0
[news] New Yii3 Demo: Document Summarizer Fri, 05 Jun 2026 08:59:26 +0000 https://www.yiiframework.com/news/814/new-yii3-demo-document-summarizer https://www.yiiframework.com/news/814/new-yii3-demo-document-summarizer samdark samdark

We’ve added a new Yii3 demo application:

https://github.com/yiisoft/demo-summarizer

It started as a practical way to test yiisoft/queue together with AMQP and Redis/Valkey drivers in Yii3 application, but it turned into a useful demo on its own.

The app lets you upload documents, extract readable markdown, summarize content with a local OpenAI-compatible llama.cpp service, and track processing progress through Yii Queue workers. It supports multiple queue drivers, background workers, S3-compatible storage via Garage, retries, deletion, and clearing all stored data and pending jobs.

By default, it runs with AMQP protocol, two RabbitMQ workers, Kreuzberg extraction, Garage storage, and a small CPU-friendly Gemma model through llama.cpp.

It is useful if you want to see:

  • Yii3 app structure in practice.
  • Native yiisoft/queue worker usage.
  • AMQP and Redis/Valkey queue drivers.
  • Docker-based local development.
  • File upload validation and processing.
  • S3-compatible storage integration.
  • Local LLM integration through an OpenAI-compatible API.

Try it with:

make build
make up
make -- yii migrate:up -y

Then open http://127.0.0.1/

Feedback and improvements are welcome.

]]>
0
[news] Yii HTML 4.2 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/813/yii-html-4-2 https://www.yiiframework.com/news/813/yii-html-4-2 vjik vjik

Yii HTML version 4.2.0 was released. In this version:

  • Add beforeInput() and afterInput() methods to abstract BooleanInputTag, extended by Radio and Checkbox
  • Add beforeCheckbox() and afterCheckbox() methods to CheckboxList, and beforeRadio() and afterRadio() methods to RadioList
]]>
0
[news] Yii Runner RoadRunner 3.2 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/812/yii-runner-roadrunner-3-2 https://www.yiiframework.com/news/812/yii-runner-roadrunner-3-2 vjik vjik

Yii Runner RoadRunner version 3.2.0 was released. In this version:

  • Add PHP 8.5 support
  • Add spiral/roadrunner-http version ^v4.0.0 support
]]>
0
[news] Yii Validator 2.6.0 Tue, 02 Jun 2026 21:23:55 +0000 https://www.yiiframework.com/news/811/yii-validator-2-6-0 https://www.yiiframework.com/news/811/yii-validator-2-6-0 samdark samdark
  1. File validator
  2. SplFileInfo support in Image validator
  3. Other changes

Yii Validator version 2.6.0 was released.

This release adds a new File validator, extends Image validator input support, and includes translation, documentation, and internal code quality improvements.

File validator

The new File rule validates that a value is a file. It supports:

  • string file paths;
  • SplFileInfo instances;
  • PSR-7 UploadedFileInterface instances.

It can also check allowed extensions, MIME types, and file size.

use Yiisoft\Validator\Rule\File;
use Yiisoft\Validator\Validator;

$result = (new Validator())->validate(
    ['avatar' => $request->getUploadedFiles()['avatar'] ?? null],
    [
        'avatar' => new File(
            extensions: ['jpg', 'png', 'webp'],
            mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
            maxSize: 2_000_000,
        ),
    ],
);

For multiple files, use it together with Each:

use Yiisoft\Validator\Rule\Each;
use Yiisoft\Validator\Rule\File;

$rules = [
    'attachments' => new Each(
        new File(
            extensions: ['pdf', 'txt'],
            maxSize: 5_000_000,
        ),
    ),
];

Optional upload fields can use skipOnEmpty:

use Yiisoft\Validator\Rule\File;

$rule = new File(skipOnEmpty: true);

SplFileInfo support in Image validator

Image validator now accepts SplFileInfo values directly. This is useful when your application already works with filesystem objects instead of plain paths.

use SplFileInfo;
use Yiisoft\Validator\Rule\Image\Image;
use Yiisoft\Validator\Validator;

$result = (new Validator())->validate(
    new SplFileInfo(__DIR__ . '/avatar.jpg'),
    new Image(
        minWidth: 128,
        minHeight: 128,
        maxWidth: 2048,
        maxHeight: 2048,
	),
);

The Image validator was also fixed to handle unreadable streams correctly.

Other changes

  • Updated Polish translations.
  • Explicitly imported classes, functions, and constants in use sections.
  • Fixed translations, documentation grammar, incorrect imports, and a broken contributing guide link.
]]>
0
[news] ApiDoc extension version 4.0.0 released Sat, 30 May 2026 09:42:40 +0000 https://www.yiiframework.com/news/810/apidoc-extension-version-4-0-0-released https://www.yiiframework.com/news/810/apidoc-extension-version-4-0-0-released samdark samdark

We are very pleased to announce the release of the ApiDoc extension version 4.0.0.

This is a major release that modernizes ApiDoc for current PHP projects. The minimum PHP version is now 8.2, and dependency requirements were raised for nikic/php-parser, phpdocumentor/reflection, and phpdocumentor/type-resolver.

Version 4.0.0 adds support for intersection and nullable types, PHPStan/Psalm syntax, constants in interfaces and traits, inherited descriptions, improved method signatures, better inline @see and @link handling, and links to built-in PHP functions. It also includes a new template for custom projects.

A brief overview of major changes:

  • PHP 8.2 is now required.
  • Deprecated code was removed.
  • nikic/php-parser 5.0+ is now required.
  • phpdocumentor/reflection 7.0+ and phpdocumentor/type-resolver 2.0+ are now required.
  • FQSEN support in inline @link tags was removed.
  • Type extraction and rendering were significantly improved.
  • Constants, inheritance, and PHPDoc handling are more complete.

Most of this release is handled by Maksim Spirkov, with the new custom project template also based on work by jcherniak and cebe.

See the CHANGELOG for a full list of changes.

]]>
0
[news] Yii2 HTTP Client 2.0.17 Sat, 30 May 2026 09:41:52 +0000 https://www.yiiframework.com/news/809/yii2-http-client-2-0-17 https://www.yiiframework.com/news/809/yii2-http-client-2-0-17 terabytesoftw terabytesoftw

Yii2 HTTP Client version 2.0.17 was released. In this version:

  • Fixed TypeError: stream_get_contents(): Argument #1 ($stream) must be of type resource, bool given when PHP error reporting is turned off.
  • Fixed the PHP 8.5 deprecation for the predefined locally scoped $http_response_header variable.
  • Fixed curl_close() and curl_multi_close() deprecations in PHP 8.5.
  • Fixed PHPDoc annotations for Response::$statusCode and Response::getStatusCode().
  • Added additional keys to Message::getHeaders() to expose full HTTP status line details through Client::getHeaders().
  • Applied Yii2 coding standards.
  • Raised the minimum PHP version to 7.4.
]]>
0
[extension] ovargas/fluentrules Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/ovargas/fluentrules https://www.yiiframework.com/extension/ovargas/fluentrules Omar Vargas Omar Vargas ]]> 0 [news] Yii HTML 4.1 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/805/yii-html-4-1 https://www.yiiframework.com/news/805/yii-html-4-1 vjik vjik

Yii HTML version 4.1.0 was released. In this version:

  • Add test helper functions for controlling HTML ID generation
]]>
0
[news] Yii Active Record 1.1 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/804/yii-active-record-1-1 https://www.yiiframework.com/news/804/yii-active-record-1-1 vjik vjik

Yii Active Record version 1.1.0 was released. In this version:

  • Clarify $relations parameter type in JoinWith::__construct() from array<string|Closure> to array<string|callable(ActiveQueryInterface):void>
  • Optimize performance of ActiveRecord::get() method
  • Remove check for empty string in AbstractActiveRecord::markPropertyChanged() method
  • Add default config for yiisoft/config plugin
  • Relation query should be created by related class, not primary model class
  • Fix SoftDelete with initiated custom date
  • Fix ActiveRecordInterface::upsert() with $updateProperties = false
  • Fix ActiveRecordInterface::upsert() to prioritize passed associative values during updates
  • Fix properties with hooks
]]>
0
[news] Yii RBAC PHP File Storage 2.1 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/803/yii-rbac-php-file-storage-2-1 https://www.yiiframework.com/news/803/yii-rbac-php-file-storage-2-1 vjik vjik

Yii RBAC PHP File Storage version 2.1.0 was released. In this version:

  • Change PHP constraint in composer.json to 8.1 - 8.5
  • Bump yiisoft/rbac version to ^2.1
  • Apply code style fixes
  • Explicitly import functions and constants in "use" section
]]>
0
[news] Yii 2.0.55 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/news/802/yii-2-0-55 https://www.yiiframework.com/news/802/yii-2-0-55 samdark samdark

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

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

In this release:

  • Security fix for CVE-2026-39850: internal variables in View::renderPhpFile() and ErrorHandler::renderFile() are now isolated to prevent parameter collisions from overriding included file paths.
  • Continued PHPStan/Psalm and PHPDoc annotation improvements, including generics, conditional types, and more accurate return and parameter types.
  • yii\grid\GridView's default filterSelector can now be overridden and may use Closures.
  • Better compatibility with newer PHP versions, including PHP 8.6-related test adjustments.
  • Removal of obsolete code paths for PHP versions older than the current Yii 2 minimum.
  • Bug fixes.

The Yii 2 basic and advanced application templates received refreshed CI, Docker, static analysis, documentation, and project template updates. Note that the framework itself still supports PHP 7.4+, while the updated application templates target newer PHP versions.

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 continued taking 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
[wiki] Using Redis Cache in Yii 1.x (Production Setup + Tips) Tue, 21 Apr 2026 17:25:32 +0000 https://www.yiiframework.com/wiki/2716/using-redis-cache-in-yii-1-x-production-setup-tips https://www.yiiframework.com/wiki/2716/using-redis-cache-in-yii-1-x-production-setup-tips AftabHussainSharSukkur AftabHussainSharSukkur

Hey everyone,

I’m working on a Yii 1.x–based ERP/POS system and recently implemented Redis caching for performance optimization. Thought I’d share my setup and a few lessons learned in case it helps someone still maintaining Yii 1.x apps.

My Setup
'components'=>array(
    'cache'=>array(
        'class'=>'CRedisCache',
        'hostname'=>'127.0.0.1',
        'port'=>6379,
        'database'=>1,
        'keyPrefix'=>'database',
    ),
),
Why Redis Instead of FileCache?
  • Much faster read/write (RAM-based)
  • Great for high-traffic POS/ERP environments
  • Reduces DB load significantly
  • Works well for session + query caching
✅ Where I’m Using Cache
  • Product listing queries (heavy joins)
  • Dashboard stats (sales, stock, reports)
  • API responses for branch sync
  • Session handling (planning to shift fully to Redis)
Things to Watch Out For
  1. Key Prefix is Important If you’re running multiple apps on same Redis instance, always use keyPrefix to avoid conflicts.

  2. Cache Invalidation Yii 1.x doesn’t auto-handle this well. You need to manually clear cache when:

    • product updates
    • stock changes
    • price updates
  3. Persistence Redis is in-memory. Make sure:

    • RDB or AOF is enabled (depending on your setup)
    • Otherwise you risk data loss on restart
  4. Production Deployment Don’t keep Redis on default config:

    • bind to private IP
    • use password (requirepass)
    • firewall the port
Example Usage
$key = 'product_list';

$data = Yii::app()->cache->get($key);

if ($data === false) {
    $data = Product::model()->findAll();
    Yii::app()->cache->set($key, $data, 300); // cache for 5 minutes
}
Question for Community

For those still on Yii 1.x:

  • Are you using Redis for sessions as well?
  • Any best practices for automatic cache invalidation?

Would love to hear how others are optimizing legacy Yii apps in production.

Thanks!

]]>
0
[extension] bestyii/yii2-tabler Tue, 07 Apr 2026 04:04:05 +0000 https://www.yiiframework.com/extension/bestyii/yii2-tabler https://www.yiiframework.com/extension/bestyii/yii2-tabler ezsky ezsky

bestyii/yii2-tabler

  1. 产品定位
  2. 当前交付面
  3. 适合什么项目
  4. 和应用的边界
  5. 安装
  6. 快速开始
  7. 组件契约
  8. 资源策略
  9. 产品目标
  10. 稳定性与兼容原则
  11. 质量与可维护性
  12. 文档入口
  13. 选型结论

CI

bestyii/yii2-tabler 是一个面向 Yii2 后台、运营平台和数据管理界面的 Tabler 组件包。 它不是单纯的 CSS 主题封装,而是把 Tabler 的视觉语言、常见后台部件和前端插件整合成可复用的 Yii2 Widget 与 Asset Bundle。 从产品目标上,它不应只是一个“Tabler 版补充包”,而应逐步成为 yiisoft/yii2-bootstrap5 的上位替代:既覆盖 Bootstrap 常用能力,也提供更丰富的后台组件和更优的视觉表达。

产品定位

这个包解决的是 Yii2 后台项目里最常见的三个问题:

  1. 设计系统已经选定 Tabler,但团队不希望在视图里手写大量碎片化 HTML。
  2. 项目需要的不只是按钮、弹窗、导航,还包括图表、地图、富文本、拖拽上传、分段导航、状态指示、时间线、运营型表格等后台高频组件。
  3. 团队希望把前端插件接入、资源发布、组件文档和测试门禁,统一收敛在包级别,而不是散落在每个业务应用里。

因此,bestyii/yii2-tabler 的目标不是替代 Yii2 本身,而是为 Yii2 提供一层偏产品化、偏后台场景的 Tabler 组件基座,并逐步补齐 yii2-bootstrap5 已有的核心能力。

当前交付面

基于当前仓库快照,这个包已经提供:

  • 62 个顶层类,覆盖 Bootstrap 常用组件、Tabler 后台组件和表单基座。
  • 31 个 Asset Bundle,用于统一管理 Tabler 及其配套前端依赖。
  • 59 篇组件文档,位于 docs/components
  • 包级 phpunitphpstanecs 三条质量门禁。

代表性组件包括:

  • Bootstrap 核心层:ActiveFormActiveFieldBreadcrumbsButtonDropdownButtonToolbarLinkPagerNavBarPopoverToggleButtonGroup
  • 基础界面:ButtonAlertBadgeCardModalOffcanvasTabsToast
  • 导航与页头:NavDropdownDropdownMenuPageHeaderPaginationNavSegmented
  • 状态与运营表达:StatusStatusDotStatusIndicatorRibbonStepsTimelineTrackingTrending
  • 内容与后台块:AvatarAvatarListEmptyStatePaymentTag
  • 数据与交互:TableAdvancedTableRangeRating
  • 插件型组件:ChartDatepickerDropzoneFullcalendarSelectSignatureTypedVectorMapWysiwyg

适合什么项目

适合:

  • SaaS 后台
  • CRM / ERP / OA
  • 数据分析与运营平台
  • 需要较强视觉完成度的内部管理系统
  • 已经采用 Tabler 作为统一视觉系统的 Yii2 项目

不适合:

  • 只需要最基础 Bootstrap 组件的轻量页面
  • 主要面向营销落地页而非后台工作台的项目
  • 必须严格沿用 yiisoft/yii2-bootstrap5 既有命名、示例和资源层约定,不接受迁移适配的项目

和应用的边界

这个包当前聚焦在“组件层”和“资源层”:

  • 提供 Widget、Asset Bundle、组件文档和测试
  • 不内置应用级模块、站点路由或布局系统
  • 不伪装成完整后台脚手架

也就是说,它更像一个可持续维护的 UI 组件包,而不是一个整站模板。

安装

composer require bestyii/yii2-tabler

当前要求:

  • PHP 8.2 - 8.4
  • Yii2 ~2.0.32

官方支持策略和兼容承诺见 docs/support-policy.md

快速开始

1. 渲染一个按钮
use bestyii\tabler\Button;

echo Button::primary(
    'Open Preview',
    icon: 'eye',
    url: ['/preview'],
);
2. 渲染一个标准页头
use bestyii\tabler\Badge;
use bestyii\tabler\PageHeader;

echo PageHeader::widget([
    'preTitle' => 'Operations',
    'title' => 'Hybrid Validation Board',
    'content' => Badge::green('Ready', lite: true),
]);

对于高频场景,组件现在提供了更易写的静态 helper,例如 Badge::secondary('Draft')Button::primary('Save')Alert::success('Done')Progress::success(72, label: '72%')。底层的 ::widget([...]) API 仍然保留,适合更完整的配置数组。

如果颜色或类型是在业务代码里动态算出来的,可以进一步使用类型化的 make()

use bestyii\tabler\Badge;
use bestyii\tabler\Button;

echo Badge::make(color: 'orange', text: 'Review queue', lite: true);
echo Button::make(color: 'danger', label: 'Delete', outline: true, icon: 'trash');

当前已支持语法糖的组件和预设范围如下:

  • Badgeprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • Buttonprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • Alertprimarysecondarysuccessinfowarningdanger
  • Progressprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • Tagprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • Statusprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • StatusDotprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • StatusIndicatorprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • Ribbonprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • Spinnerbordergrow
  • ButtonDropdownprimarysecondarysuccessinfowarningdangerblueazureindigopurplepinkredorangeyellowlimegreentealcyandark
  • Offcanvasleftrighttopbottom
  • Popoverautotopbottomleftright
3. 渲染一个后台表格卡片
use bestyii\tabler\AdvancedTable;

echo AdvancedTable::widget([
    'title' => 'Release backlog',
    'description' => 'Searchable table for local-first delivery lanes.',
    'searchPlaceholder' => 'Search backlog',
    'pageSize' => 10,
    'columns' => [
        ['attribute' => 'owner', 'label' => 'Owner', 'format' => AdvancedTable::FORMAT_TEXT],
        ['attribute' => 'lane', 'label' => 'Lane', 'format' => AdvancedTable::FORMAT_TEXT],
    ],
    'rows' => [
        ['owner' => 'Alice Wong', 'lane' => 'Mirror routing'],
        ['owner' => 'Ben Yu', 'lane' => 'Widget validation'],
    ],
]);

组件契约

为了让组件在复杂后台页面里可长期维护,这个包现在遵循一套统一内容契约:

  • 文本属性默认安全。像 titlelabelsubtitle 这类字段,默认按文本处理。
  • 原始 HTML 要显式表达。新代码优先使用 contentHtmlheaderHtmlfooterHtml 这类命名,而不是模糊地把 HTML 放进普通字符串属性。
  • 列表和表格用 format 明确语义。TableAdvancedTable 的列配置优先使用 format => Table::FORMAT_TEXT|FORMAT_HTML,旧的 encode 仅作为兼容桥保留。
  • 缓冲式 begin()/end() 输出视为 HTML 插槽,因为这类用法本身就是为了拼接 widget 或标记。

详细约定见 docs/component-contracts.md

资源策略

bestyii/yii2-tabler 采用的是“组件 + 资源包”双层模型:

  • TablerAsset 负责注册 Tabler 核心样式与脚本
  • 插件组件各自依赖对应 Asset Bundle,例如 ApexChartsAssetDropzoneAssetFullcalendarAsset
  • 包内统一处理资源路径、发布行为和依赖声明,业务应用只关心 Widget 调用

扩展资源现在按最小边界拆分:

  • Flag 会自动注册 TablerFlagsAsset
  • Payment 会自动注册 TablerPaymentsAsset
  • 如果你直接在页面里使用原始 Tabler 扩展 class,而不是通过 widget 输出,请显式注册对应资源包:
    • TablerSocialsAsset
    • TablerMarketingAsset
    • TablerThemeAsset

例如:

use bestyii\tabler\assets\TablerSocialsAsset;
use bestyii\tabler\assets\TablerThemeAsset;

TablerSocialsAsset::register($this);
TablerThemeAsset::register($this);

如果主题切换器是应用级能力,而不是单个 widget 的局部交互,优先在 layout 里注册 TablerThemeAsset;像社交图标这类页面级 class,则只在实际使用该 class 的视图里注册对应资源。

TablerExtrasAsset 仍然保留为兼容聚合包,但新代码应优先注册最小匹配的资源,而不是一次性加载全部 extras。

这让项目可以在保留 Yii2 视图体系的同时,把前端插件接入成本控制在组件包内部。

产品目标

从长期方向看,bestyii/yii2-tabler 应该满足两层目标:

  • 第一层,用 Tabler 风格完整承接 yii2-bootstrap5 的核心用户态组件能力。当前这一层已经覆盖到 ActiveFormActiveFieldNavBarButtonDropdownButtonToolbarLinkPagerPopoverToggleButtonGroup 等高频部件。
  • 第二层,在此基础上继续提供 CardPageHeaderAdvancedTableChartDropzoneVectorMapWysiwyg 等更偏后台产品场景的组件。

换句话说,yii2-tabler 的定位不是“和 yii2-bootstrap5 做功能切分”,而是“以 Tabler 风格重做并扩展 Yii2 的 Bootstrap 组件层”。

稳定性与兼容原则

  • 包的官方支持线是 PHP 8.2 - 8.4,面向现代 Yii2 团队,而不是极旧环境。
  • yii2-bootstrap5 高度同构的组件优先追求稳定和一致性,不做为了“更优雅”而引入的新概念拆分。
  • 更强的内容契约、资产归属和后台产品能力,优先落在 CardAdvancedTablePopover 这类产品层组件里。

详细规则见 docs/parity-policy.mddocs/support-policy.md

质量与可维护性

当前包级交付标准包括:

  • 组件文档与源码同仓维护
  • PHPUnit 验证渲染结果与资产发布
  • PHPUnit 覆盖率配置已启用,CI 会在单独的 coverage job 里产出 runtime/coverage 报告工件
  • PHPStan 验证静态分析
  • ECS 保持代码风格一致

本地常用命令:

composer tests
composer static
composer cs
XDEBUG_MODE=coverage composer coverage

说明:

  • composer tests 默认带 --no-coverage,用于日常快速回归。
  • composer coverage 需要 pcovXdebug 覆盖率驱动;如果使用 Xdebug,请显式带上 XDEBUG_MODE=coverage
  • 覆盖率报告会写入 runtime/coverage/,其中包含 clover.xmlcobertura.xml 和 HTML 报告。

最近一次补强还加入了 Asset Bundle 一致性测试,用于直接检查:

  • sourcePath 是否有效
  • 本地资源文件是否真实存在
  • 资源是否能被 Yii 的 AssetManager 正常发布

这类测试的目的,是避免“组件渲染测试通过,但前端资源实际上不可发布”的隐性回归。

文档入口

选型结论

从产品方向上,bestyii/yii2-tabler 应该是 yiisoft/yii2-bootstrap5 的超集,而不是平行替代。

当前如果你的项目目标是“用 Tabler 风格统一后台界面,并把 Bootstrap 常用基础能力也收进同一个组件层”,bestyii/yii2-tabler 已经可以作为主包承接。更细的覆盖矩阵与选型参考,见 docs/compare-with-yii2-bootstrap5.md

]]>
0
[extension] chinaphp/yii2-ide-helper Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/chinaphp/yii2-ide-helper https://www.yiiframework.com/extension/chinaphp/yii2-ide-helper chinaphp chinaphp

Yii2 IDE Helper

  1. 功能特性
  2. 安装
  3. 使用
  4. PhpStorm Meta 文件详解
  5. PhpStorm 配置
  6. 生成文件示例
  7. 测试
  8. 贡献
  9. 许可证
  10. 致谢

为 Yii2 框架提供 PhpStorm 智能代码补全支持,灵感来源于 barryvdh/laravel-ide-helper

Latest Stable Version Total Downloads License

功能特性

  • ✅ 为 Yii2 组件生成完整的 PHPDoc 类型提示
  • ✅ 为 ActiveRecord 模型生成属性和方法文档
  • ✅ 为 ActiveQuery 生成查询构建器提示
  • ✅ 生成 PhpStorm Meta 文件支持高级 IDE 特性(DI 容器、类型推断)
  • ✅ 支持自定义配置路径
  • ✅ 完整的 CLI 命令支持
  • ✅ 支持多数据库(MySQL、PostgreSQL、SQLite)

安装

要求
  • PHP >= 7.4
  • Yii2 >= 2.0.43
  • PhpStorm 2018.2 或更高版本
使用 Composer 安装
composer require --dev chinaphp/yii2-ide-helper
配置

console/config/main-local.php 中添加以下配置(仅本地开发环境需要):

<?php

$config = [
    'controllerMap' => [
        'ide-helper' => 'Chinaphp\Yii2IdeHelper\Console\Controller',
    ],
    'components' => [
        'ide-helper' => [
            'class' => 'Chinaphp\Yii2IdeHelper\Config\ConfigProvider',
            'output_dir' => dirname(__DIR__),
            'filename' => '_ide_helper.php',
            'meta_filename' => '.phpstorm.meta.php',
            'config_paths' => [
                '@app/config/web.php',
                '@app/config/console.php',
            ],
        ],
    ],
    ],
];

使用

生成组件类型提示
php yii ide-helper/generate

这将生成 _ide_helper.php 文件,包含所有 Yii2 组件的类型提示。

生成的内容:

  • Yii 组件的 @property 注解(如 $db, $cache, $session
  • 组件 getter 方法的 @method 注解(如 getDb(), getCache()
  • 完整的命名空间和类定义
生成 ActiveRecord 模型类型提示
php yii ide-helper/models

这将扫描 ActiveRecord 模型并生成属性和关系类型提示。

生成的内容:

  • 数据库字段的 @property 注解(包含类型和默认值)
  • 关系方法的 @property-read 注解
  • 魔术查询方法的 @method 注解(where*(), orWhere*(), andWhere*()
  • 自定义 ActiveQuery 类的完整实现
生成 PhpStorm Meta 文件
php yii ide-helper/meta

这将生成 .phpstorm.meta.php 文件,为依赖注入容器提供类型推断。

生成的内容:

  • DI 容器绑定(Yii::$container->get() 类型推断)
  • 组件属性映射(Yii::$app->get() 类型提示)
  • ActiveRecord 模式(find(), hasMany(), hasOne() 返回类型)
  • 对象创建模式(Yii::createObject() 类型推断)
查看帮助
php yii help

PhpStorm Meta 文件详解

.phpstorm.meta.php 文件为 PhpStorm 提供高级类型推断能力,以下是它支持的功能:

1. DI 容器类型推断
override(\Yii::$app->get('db'), type(\yii\db\Connection));

这个配置告诉 PhpStorm,Yii::$app->get('db') 的返回类型是 \yii\db\Connection

使用示例: `php $db = Yii::$app->get('db'); $result = $db->createCommand('SELECT * FROM users')->queryAll(); // 有完整的类型提示 `

2. 组件属性类型提示
override(\Yii::$app->db, type(\yii\db\Connection));

这个配置告诉 PhpStorm,Yii::$app->db 属性的类型是 \yii\db\Connection

使用示例: `php $result = Yii::$app->db->createCommand('SELECT * FROM users')->queryAll(); // 有完整的类型提示 `

3. ActiveRecord 查询类型
override(\yii\db\ActiveRecord::find(), type(\yii\db\ActiveQuery));

这个配置告诉 PhpStorm,ActiveRecord::find() 返回 ActiveQuery 类型。

使用示例: `php $query = User::find(); // PhpStorm 知道返回的是 ActiveQuery $query->where(['status' => 1]); // 有完整的类型提示 `

4. 自定义绑定

你可以在代码中添加自定义绑定:

$generator = new MetaGenerator($config);
$generator->addBinding('custom', ['Custom\Class']);
$generator->save();

配置文件示例: `php // console/config/main.php 'components' => [

'ide-helper' => [
    'class' => 'Chinaphp\Yii2IdeHelper\Config\ConfigProvider',
    'output_dir' => dirname(__DIR__),
    'filename' => '_ide_helper',
    'meta_filename' => '.phpstorm.meta.php',
    'config_paths' => [
        '@app/config/web.php',
        '@app/config/console.php',
    ],
],

], `

PhpStorm 配置

  1. 在 PhpStorm 中打开项目
  2. 导航到 Settings > PHP > Include paths
  3. 添加 _ide_helper.php 文件路径
  4. 如果有 .phpstorm.meta.php,确保它在项目根目录
  5. 重新索引项目(File > Invalidate Caches / Restart)

生成文件示例

_ide_helper.php
namespace Yii {
    class App {
        public static $app;
    }
}

namespace {
    class Yii extends \Yii\BaseYii {
        /**
         * @var \yii\db\Connection
         */
        public $db;
        
        /**
         * @var \yii\caching\Cache
         */
        public $cache;
    }
}
_ide_helper_models.php
namespace app\models {
    /**
     * @property int $id
     * @property string $title
     * @property-read \app\models\User $user
     * @property-read \app\models\Comment[] $comments
     */
    class Post extends \yii\db\ActiveRecord {
    }
}
.phpstorm.meta.php
<?php
namespace PHPSTORM_META {
    // DI 容器绑定
    override(\Yii::$app->get('db'), type(\yii\db\Connection));
    override(\Yii::$app->db, type(\yii\db\Connection));

    override(\Yii::$app->get('cache'), type(\yii\caching\FileCache));
    override(\Yii::$app->cache, type(\yii\caching\FileCache));

    // ActiveRecord 类型推断
    override(\yii\db\ActiveRecord::find(), type(\yii\db\ActiveQuery));
    override(\yii\db\ActiveRecord::hasMany(), type(\yii\db\ActiveQuery));
    override(\yii\db\ActiveRecord::hasOne(), type(\yii\db\ActiveQuery));
}

Meta 文件优势:

  • ✅ 为 Yii::$app->get() 提供准确的类型推断
  • ✅ 为 Yii::$app->component 属性提供类型提示
  • ✅ 为 ActiveRecord 查询方法提供类型安全
  • ✅ 支持依赖注入容器的类型推断
  • ✅ 增强代码自动补全和重构能力

测试

运行测试套件:

composer test

运行代码规范检查:

composer lint

贡献

欢迎提交 Pull Request!

许可证

MIT License

致谢

本项目灵感来源于 barryvdh/laravel-ide-helper

]]>
0
[extension] crenspire/yii3-react-starter Tue, 24 Mar 2026 15:25:42 +0000 https://www.yiiframework.com/extension/crenspire/yii3-react-starter https://www.yiiframework.com/extension/crenspire/yii3-react-starter akshaypjoshi akshaypjoshi ]]> 0 [extension] neoacevedo/yii2-chartjs-widget Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/extension/neoacevedo/yii2-chartjs-widget https://www.yiiframework.com/extension/neoacevedo/yii2-chartjs-widget NestorAcevedo NestorAcevedo

ChartJs Widget

  1. Installation
  2. Usage
  3. Further Information
  4. Contributing
  5. Credits
  6. License

Este paquete es un fork de 2amigos/yii2-chartjs-widget, el cual se encuentra en modo de solo lectura. Este fork fue creado para mantener vivo el paquete y continuar su mantenimiento.

Latest Stable Version Total Downloads Latest Unstable Version License PHP Version Require

Renders a ChartJs plugin widget.

Installation

The preferred way to install this extension is through composer. This requires the composer-asset-plugin, which is also a dependency for yii2 – so if you have yii2 installed, you are most likely already set.

Either run

composer require neoacevedo/yii2-chartjs-widget:dev-main

or add

"neoacevedo/yii2-chartjs-widget" : "dev-main"

to the require section of your application's composer.json file.

Usage

The following types are supported:

  • Line
  • Bar
  • Radar
  • Polar
  • Pie
  • Doughnut
  • Bubble
  • Scatter
  • Area
  • Mixed

The following example is using the Line type of chart. Please, check ChartJs plugin documentation for the different types supported by the plugin.

use dosamigos\chartjs\ChartJs;

<?= ChartJs::widget([
    'type' => 'line',
    'options' => [
        'height' => 400,
        'width' => 400
    ],
    'data' => [
        'labels' => ["January", "February", "March", "April", "May", "June", "July"],
        'datasets' => [
            [
                'label' => "My First dataset",
                'backgroundColor' => "rgba(179,181,198,0.2)",
                'borderColor' => "rgba(179,181,198,1)",
                'pointBackgroundColor' => "rgba(179,181,198,1)",
                'pointBorderColor' => "#fff",
                'pointHoverBackgroundColor' => "#fff",
                'pointHoverBorderColor' => "rgba(179,181,198,1)",
                'data' => [65, 59, 90, 81, 56, 55, 40]
            ],
            [
                'label' => "My Second dataset",
                'backgroundColor' => "rgba(255,99,132,0.2)",
                'borderColor' => "rgba(255,99,132,1)",
                'pointBackgroundColor' => "rgba(255,99,132,1)",
                'pointBorderColor' => "#fff",
                'pointHoverBackgroundColor' => "#fff",
                'pointHoverBorderColor' => "rgba(255,99,132,1)",
                'data' => [28, 48, 40, 19, 96, 27, 100]
            ]
        ]
    ]
]);
?>

Plugins usage example (displaying percentages on the Pie Chart): ` echo ChartJs::widget([

'type' => 'pie',
'id' => 'structurePie',
'options' => [
    'height' => 200,
    'width' => 400,
],
'data' => [
    'radius' =>  "90%",
    'labels' => ['Label 1', 'Label 2', 'Label 3'], // Your labels
    'datasets' => [
        [
            'data' => ['35.6', '17.5', '46.9'], // Your dataset
            'label' => '',
            'backgroundColor' => [
                    '#ADC3FF',
                    '#FF9A9A',
                'rgba(190, 124, 145, 0.8)'
            ],
            'borderColor' =>  [
                    '#fff',
                    '#fff',
                    '#fff'
            ],
            'borderWidth' => 1,
            'hoverBorderColor'=>["#999","#999","#999"],                
        ]
    ]
],
'clientOptions' => [
    'legend' => [
        'display' => false,
        'position' => 'bottom',
        'labels' => [
            'fontSize' => 14,
            'fontColor' => "#425062",
        ]
    ],
    'tooltips' => [
        'enabled' => true,
        'intersect' => true
    ],
    'hover' => [
        'mode' => false
    ],
    'maintainAspectRatio' => false,

],
'plugins' =>
    new \yii\web\JsExpression('
    [{
        afterDatasetsDraw: function(chart, easing) {
            var ctx = chart.ctx;
            chart.data.datasets.forEach(function (dataset, i) {
                var meta = chart.getDatasetMeta(i);
                if (!meta.hidden) {
                    meta.data.forEach(function(element, index) {
                        // Draw the text in black, with the specified font
                        ctx.fillStyle = 'rgb(0, 0, 0)';

                        var fontSize = 16;
                        var fontStyle = 'normal';
                        var fontFamily = 'Helvetica';
                        ctx.font = Chart.helpers.fontString(fontSize, fontStyle, fontFamily);

                        // Just naively convert to string for now
                        var dataString = dataset.data[index].toString()+'%';

                        // Make sure alignment settings are correct
                        ctx.textAlign = 'center';
                        ctx.textBaseline = 'middle';

                        var padding = 5;
                        var position = element.tooltipPosition();
                        ctx.fillText(dataString, position.x, position.y - (fontSize / 2) - padding);
                    });
                }
            });
        }
    }]')

]) `

Further Information

ChartJs has lots of configuration options. For further information, please check the ChartJs plugin website.

Contributing

Please see CONTRIBUTING for details.

Credits

License

The BSD License (BSD). Please see License File for more information.

2amigOS!
Custom Software | Web & Mobile Software Development
www.2amigos.us

]]>
0
[extension] crenspire/yii3-inertia Tue, 24 Feb 2026 12:55:34 +0000 https://www.yiiframework.com/extension/crenspire/yii3-inertia https://www.yiiframework.com/extension/crenspire/yii3-inertia akshaypjoshi akshaypjoshi ]]> 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

Yii2 AI Boost - MCP Server for Yii2 Applications

  1. Features
  2. Quick Start
  3. Installation
  4. Usage
  5. Guidelines & Editor Integration
  6. What is MCP?
  7. Available Tools
  8. Core Tools Architecture
  9. Tools Roadmap
  10. MCP Protocol
  11. Guidelines
  12. Troubleshooting
  13. Requirements
  14. Development Timeline
  15. License
  16. Contributing
  17. Support & Feedback

Version License Yii2

Yii2 AI Boost is a Model Context Protocol (MCP) server that provides AI assistants (like Claude Code) with comprehensive tools and guidelines for faster Yii2 application development.

Features

  • 16 MCP Tools - Database inspection and queries, config access, route analysis, component introspection, model and validation inspection, console command discovery, migration inspection, widget inspection, performance profiling, PHP tinker, environment inspection, logging, and FTS5-powered semantic search
  • Semantic Search - BM25-ranked full-text search over bundled guidelines + Yii2 definitive guide (section-level results, not full files)
  • Framework Guidelines - Comprehensive Yii2 patterns covering controllers, models, migrations, caching, auth, and more
  • IDE Integration - Works with Claude Code, Cursor, Zed, and other MCP-compatible editors

Quick Start

For experienced developers:

# 1. Install
composer require codechap/yii2-ai-boost:^1.3 --dev

# 2. Run installation
php yii boost/install

# 3. (Optional) Sync guidelines to your editor (Cursor/Zed)
php yii boost/sync-rules

That's it! Claude Code and other AI tools now have access to your application context.

Installation

Step 1: Require the Package
cd /path/to/yii2/application

composer require codechap/yii2-ai-boost:^1.3 --dev
Step 2: Run Installation Wizard
php yii boost/install

The installation runs automatically and:

  • Detects your Yii2 environment
  • Generates configuration files (.mcp.json, boost.json)
  • Copies framework guidelines to .ai/guidelines/

If installation fails, please open an issue or reach out on X.

Step 3: Connect Claude Code and or your IDE

Claude Code integration

After running php yii boost/install, a .mcp.json file will be generated in your project root. Claude Code will automatically detect and use this configuration to connect to the MCP server.

Codex CLI Configuration @todo

Gemini CLI Configuration @todo

Zed Configuration

For Zed create or open your settings file in .zed/settings.json

{
    "context_servers": {
      "yii2-ai-boost": {
        "enabled": true,
        "command": "php",
        "args" : [
            "yii", "boost/mcp"
        ]
      }
    }
}
Generated Files

After installation, you'll have:

  • .mcp.json - MCP server configuration for Claude Code
  • boost.json - Package configuration and tool list
  • .ai/guidelines/ - Framework and ecosystem guidelines (Markdown)
  • .cursor/rules/yii2-boost.mdc - (Optional) Generated rules for Cursor
  • .rules - (Optional) Generated rules for Zed

Usage

View Yii2 information
php yii boost/info

Displays:

  • Package version and configuration
  • List of available MCP tools
  • Status of guidelines and configuration files
Sync Editor Rules
php yii boost/sync-rules

Automatically generates:

  • Cursor: .cursor/rules/yii2-boost.mdc
  • Zed: .rules (in project root)

These files contain the core Yii2 guidelines and structural references, giving your AI editor "X-Ray vision" into Yii2 best practices without manual prompting.

Start MCP Server (Manual Testing)
php yii boost/mcp

⚠️ Note: This command is invoked automatically by Cluade Code or your editor. You don't need to run it manually.

The server listens on STDIN for JSON-RPC requests and outputs responses to STDOUT.

Update Guidelines
php yii boost/update

Updates guidelines, downloads Yii2 guide from GitHub, and rebuilds the FTS5 search index.

Guidelines & Editor Integration

Yii2 AI Boost comes with a rich library of "Context Anchors" in .ai/guidelines/. These are Markdown files that define exact structures for Yii2 components (Controllers, Models, Migrations, etc.), preventing AI hallucinations.

1. Active Search (MCP Tool)

The MCP server includes a semantic_search tool powered by SQLite FTS5. AI agents (like Claude or Gemini) can use this to "look up" how to do things in Yii2.

  • User: "How do I create a migration?"
  • AI: Calls semantic_search(query="migration") -> Gets BM25-ranked sections -> Writes perfect code.
2. Passive Context (Editor Rules)

Run php yii boost/sync-rules to bake these guidelines directly into your editor's context.

  • Cursor: Creates a .mdc rule file.
  • Zed: Creates a .rules file.

This means when you open a file in Zed or Cursor, the AI already knows it should use yii\web\Controller and not Illuminate\Routing\Controller.

What is MCP?

The Model Context Protocol (MCP) is an open standard that enables AI assistants to interact with tools and data sources. MCP allows Claude Code and other AI tools to securely access your application's context—database schemas, configuration, routes, and logs—without exposing sensitive data.

Yii2 AI Boost implements MCP v2025-11-25 using JSON-RPC 2.0 over STDIO transport. This means Claude Code communicates with your application through standard input/output, with no need for network configuration.

Learn more about MCP

Available Tools

1. application_info - Application Info

Get comprehensive information about your Yii2 application:

  • Yii2 and PHP versions
  • Application environment and debug status
  • Installed modules and extensions
2. database_schema - Database Schema

Inspect your database structure:

  • List all tables with row counts
  • View detailed table schemas (columns, types, constraints)
  • Discover Active Record models
  • View indexes and foreign keys
3. database_query - Database Query

Execute SQL queries against your database:

  • Run SELECT queries with automatic row limiting
  • Support for bound parameters
  • Returns execution time and row count
  • Works with any configured database connection
4. config_access - Config Access

Access application configuration safely:

  • Component configurations
  • Module configurations
  • Application parameters (with sensitive data redaction)
5. route_inspector - Route Inspector

Analyze your application routes:

  • URL rules and patterns
  • Module routes with prefixes
  • Controller and action mappings
  • RESTful API endpoints
6. component_inspector - Component Inspector

Introspect application components:

  • List all registered components
  • View component classes and configurations
  • Check singleton vs new instance behavior
  • Inspect component properties
7. log_inspector - Log Inspector

Inspect application logs from all configured sources:

  • Read logs from FileTarget (text files)
  • Read logs from DbTarget (database table)
  • Access in-memory logs (current request)
  • Filter by log level (error, warning, info, trace, profile)
  • Filter by category with wildcard patterns
  • Search logs by keywords
  • Filter by time range
  • View stack traces (for in-memory logs)
8. semantic_search - Semantic Search (FTS5)

Search Yii2 guidelines and documentation with full-text search:

  • BM25-ranked results using SQLite FTS5 (replaces grep-based search)
  • Section-level results (relevant sections, not full files)
  • Supports phrases ("active record"), boolean (migration AND database), prefix (migrat*)
  • Indexes bundled guidelines + Yii2 definitive guide from GitHub
  • Porter stemming ("migrating" matches "migration")
  • Grep fallback if FTS5 index not built yet
9. model_inspector - Model Inspector

Inspect Active Record models at runtime:

  • Attributes with database types, labels, and hints
  • Relations (hasOne/hasMany) with link details and junction tables
  • Attached behaviors with class names and properties
  • Scenarios with active and safe attributes
  • Fields and extra fields for API serialization
  • Automatic model discovery from @app/models
10. validation_rules - Validation Rules

Inspect model validation rules and constraints:

  • All validation rules with parameters and scenario filters
  • Built-in vs custom validator classification
  • Error messages per validator grouped by attribute
  • Constraint summary (required, unique, string length, number range, email, etc.)
  • Safe attributes per scenario
  • Supports filtering by specific scenario
11. console_command_inspector - Console Command Inspector

Discover and inspect Yii2 console commands (./yii commands):

  • List all discoverable console controllers with class and description
  • Inspect individual commands with actions, options, and help text
  • Drill into specific actions for arguments, types, and defaults
  • Discovers from controllerMap, namespace directory, and modules
  • Option aliases and PHPDoc-based help extraction
12. migration_inspector - Migration Inspector

Inspect database migrations and their status:

  • Status summary with applied/pending counts and last applied migration
  • Applied migration history with timestamps (sorted most recent first)
  • Pending migration discovery from configured migration paths
  • View individual migration source code and apply status
  • Supports @app/migrations and additional configured paths
13. widget_inspector - Widget Inspector

Discover and inspect Yii2 widgets:

  • List available widgets grouped by source (framework core, grid, application)
  • Inspect widget properties with types, defaults, and PHPDoc descriptions
  • Public methods with parameter signatures and return types
  • Event constants (EVENT_*) with declaring class
  • Class hierarchy chain up to yii\base\Widget
  • Short name resolution (e.g., "ActiveForm" resolves to yii\widgets\ActiveForm)
  • Discovers widgets from @app/widgets/, @app/components/, and modules//widgets/ + modules//components/
  • Note: Yii2 allows widgets anywhere in the codebase. Auto-discovery scans the directories above; widgets in other locations can still be inspected by passing the full class name (e.g., widget: "app\\custom\\MyWidget")
14. performance_profiler - Performance Profiler

Analyze query performance and index coverage:

  • EXPLAIN query plans with driver-specific formatting (MySQL, PostgreSQL, SQLite)
  • Automatic detection of full table scans, missing index usage, filesort, and temporary tables
  • Per-table index analysis with foreign key column coverage
  • Missing index detection for FK-like columns (*_id naming convention)
  • Table statistics (row counts, data/index sizes for MySQL, scan counts for PostgreSQL)
  • Overview mode with per-table summary and missing-index report across all tables
  • Supports bound parameters for parameterized query analysis
15. tinker - Tinker

Execute arbitrary PHP code in the Yii2 application context:

  • Run any PHP expression or statement with full access to \Yii::$app
  • Automatic return value capture (tries expression first, falls back to statement)
  • Output capture for echo/print statements
  • Configurable timeout (default 5s, max 30s)
  • Dangerous function blocking (exit, die, shell_exec, system, exec, etc.)
  • Return values formatted with VarDumper for objects
  • Output truncated at 100KB, sensitive data automatically redacted
16. env_inspector - Environment Inspector

Inspect environment variables, PHP extensions, and system configuration:

  • Environment variables with automatic sensitive value redaction
  • Prefix filter for environment variables (e.g., "DB", "APP")
  • Loaded PHP extensions sorted alphabetically with count
  • Key PHP configuration values (memory_limit, max_execution_time, upload sizes, etc.)
  • System info: OS, architecture (32/64-bit), working directory
  • Configurable sections via include parameter

Core Tools Architecture

All 16 tools provide deep introspection into your Yii2 application. They follow a consistent architecture based on the BaseTool abstract class, which provides:

  • Automatic Sanitization: Sensitive data (passwords, tokens, keys) is automatically redacted from all tool outputs
  • Database Discovery: Tools automatically detect and access configured database connections
  • JSON Schema Validation: Input parameters are validated against defined schemas
  • Error Handling: Graceful error responses without exposing sensitive details
How the Log Inspector Works

The Log Inspector features a multi-reader architecture supporting three log storage methods:

Reader Source Best For Features
InMemoryLogReader Current request logs (Yii::getLogger()->messages) Real-time debugging during development Full stack traces, microsecond timestamps
FileLogReader FileTarget text logs (@runtime/logs/app.log) Reviewing logs from previous requests/sessions Efficient file handling (5MB+ files), auto-detects rotation
DbLogReader DbTarget database table ({{%log}}) Production logging & log aggregation Fast indexed queries, precise time-range filtering

Tools Roadmap

Phase Tool Status Description
1 application_info ✓ Complete Yii2 version, environment, modules, extensions
1 database_schema ✓ Complete Tables, columns, indexes, models, foreign keys
1 config_access ✓ Complete Component, module, and parameter configurations
1 route_inspector ✓ Complete URL rules, routes, REST endpoints
1 component_inspector ✓ Complete Component listing, classes, configurations
1 log_inspector ✓ Complete File, database, and in-memory logs with filtering
1 semantic_search ✓ Complete FTS5-powered search over guidelines + Yii2 guide
1 database_query ✓ Complete Execute database queries (limited rows)
2 model_inspector ✓ Complete Active Record model analysis, properties, relations
2 validation_rules ✓ Complete Model validation rules, error messages, constraints
2 console_command_inspector ✓ Complete Console command discovery, actions, options, arguments
3 migration_inspector ✓ Complete Migration status, history, pending, source viewing
3 widget_inspector ✓ Complete Available widgets, properties, methods, events, hierarchy
3 performance_profiler ✓ Complete EXPLAIN plans, index analysis, missing index detection
4 tinker ✓ Complete Execute arbitrary PHP code in Yii2 application context
4 env_inspector ✓ Complete Environment variables, PHP extensions, system configuration
5 semantic_search ✓ Complete SQLite FTS5 search over Yii2 guide + guidelines sourced from GitHub

MCP Protocol

Yii2 AI Boost implements the Model Context Protocol (MCP) v2025-11-25:

  • Transport: STDIO (local) - reads from stdin, writes to stdout
  • Format: JSON-RPC 2.0
  • Tools: Expose functionality to AI assistants
  • Resources: Provide static content (guidelines, configuration)
Example JSON-RPC Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "application_info",
    "arguments": {
      "include": ["version", "environment", "modules"]
    }
  }
}
Example Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "version": {
      "yii2_version": "2.0.45",
      "php_version": "8.1.2",
      "php_sapi": "cli"
    },
    "environment": {
      "yii_env": "dev",
      "yii_debug": true,
      "base_path": "/path/to/app",
      "runtime_path": "/path/to/app/runtime"
    },
    "modules": {
      "site": {
        "class": "app\\modules\\site\\Module",
        "basePath": "/path/to/app/modules/site"
      }
    }
  }
}

Response Structure:

  • jsonrpc: Always "2.0" per JSON-RPC spec
  • id: Echoes the request ID for request/response matching
  • result: The actual tool output (sensitive data automatically redacted)

Error Responses use error instead of result: `json { "jsonrpc": "2.0", "id": 1, "error": {

"code": -32603,
"message": "Internal error",
"data": "Error details here"

} } `

Guidelines

The package downloads comprehensive Yii2 development guidelines to .ai/guidelines/core/yii2-2.0.45.md. These cover application structure, controllers, models, views, components, security, performance, and console commands.

Including Guidelines in Your AI Workflow

To use these guidelines with Claude Code or other AI tools, add the following lines to your project's CLAUDE.md file:

@include .ai/guidelines/core/yii2-2.0.45.md

Your AI assistant can also search additional guidelines on-demand via the semantic_search MCP tool (database, cache, auth, validation, etc.).

This ensures AI assistants have access to framework best practices and patterns when working in your project.

Troubleshooting

Getting Help

If you encounter issues:

  1. Check the log files listed below for error details
  2. Open an issue: https://github.com/codechap/yii2-ai-boost/issues
  3. Reach out on X: https://x.com/codechap
Log Files

When debugging, check these log files:

  • Startup Log: @runtime/logs/mcp-startup.log — Server initialization and tool registration
  • Error Log: @runtime/logs/mcp-errors.log — PHP errors and exceptions
  • Request Log: @runtime/logs/mcp-requests.log — JSON-RPC requests and responses
  • Transport Log: /tmp/mcp-server/mcp-transport.log — Low-level STDIO communication
FAQ

This section will be expanded as common questions arise. For now, please reach out with issues or questions.

Requirements

Component Version Status
PHP 7.4, 8.0, 8.1, 8.2, 8.3 ✓ Tested
Yii2 2.0.45+ ✓ Compatible

Why PHP 7.4? While PHP 7.4 is EOL, Yii2 itself still supports it. As a Yii2 extension, we maintain the same baseline to ensure developers on older Yii2 installations aren't locked out. If your Yii2 app runs, this tool should too.

Why no caching? All introspection data is fetched fresh on every request. This is intentional - as a development tool, you need to see the current state of your application, not stale cached data. When you change a route, schema, or component, the tools should reflect that immediately.

Note: PHP 8.4 support pending. Report any compatibility issues on GitHub.

Development Timeline

Phase Goal Status Tools
1 Core MVP ✓ Complete 8 tools + guidelines + installer
2 Model & Command Introspection ✓ Complete +3 tools (model inspector, validation rules, console commands)
3 Extended Tools ✓ Complete +3 tools (migration inspector, widget inspector, performance profiler)
4 Advanced Tools ✓ Complete +2 tools (tinker, env inspector)
5 Semantic Search ✓ Complete SQLite FTS5 index, GitHub content pipeline, BM25-ranked search

Track progress and contribute at GitHub.

License

BSD 3-Clause License. See LICENSE file for details.

Contributing

Contributions are welcome! Here's how to get started:

  1. Fork the repository
  2. Clone your fork and create a branch (git checkout -b feature/my-feature)
  3. Install dependencies (composer install)
  4. Make your changes
  5. Test your changes (composer test)
  6. Check code style (composer cs-check) and fix if needed (composer cs-fix)
  7. Run static analysis (composer analyze)
  8. Commit with a clear message and push to your fork
  9. Open a Pull Request against master
Guidelines
  • Follow PSR-12 code style
  • Add tests for new functionality where practical
  • Keep changes focused - one feature/fix per PR
  • Update documentation if adding new tools or changing behavior
Areas Where Help is Appreciated
  • Additional test coverage (especially integration tests)
  • New tools from the roadmap
  • Documentation improvements
  • Bug reports with reproduction steps

Support & Feedback

Yii2 AI Boost - Making Yii2 development smarter and faster with AI assistants.

]]>
0
[extension] davidrnk/yii2-recurring-date Wed, 16 Sep 2026 00:10:52 +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
[wiki] Building Modern SPAs with Yii2 and Inertia.js Wed, 16 Sep 2026 00:10:52 +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
[wiki] Yii3 - How to start Thu, 03 Sep 2026 08:02:19 +0000 https://www.yiiframework.com/wiki/2581/yii3-how-to-start https://www.yiiframework.com/wiki/2581/yii3-how-to-start rackycz rackycz
  1. Intro
  2. Demo applications
  3. Git
  4. Docker
  5. PSR Standards by Framework Interoperability Group
  6. Dependency injection + container
  7. 135 packages by Yii
  8. invoke()
  9. Theory around __invoke():
  10. Hash annotations for class attributes
  11. ===== Yii3 - How to start ======
  12. .env files
  13. Running the demo application
  14. Disclaimer
  15. Adding DB into your project
  16. Enabling MariaDB (MySQL) and migrations
  17. Creating a migration
  18. Running the migrations
  19. Reading data from DB
  20. Seeding the database
  21. Using Repository and the Model class
  22. API login + access token
  23. Debugging in xDebug (xdebug.ini)
  24. php.ini
  25. Pjax
  26. GridView + CRUD for Users
  27. ActiveRecord
  28. JS client - Installable Vuejs3 PWA

Intro

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.

If you want to know how Yii2 works, check my other wiki and demo application:

Yii v2 snippet guide, part 1

Yii 2 Basic Project, Git

Demo applications

Yii3 offers more demo applications, but their list is a bit confusing.

In the Yii3 Guide there now are mentioned 3 demos, plus there are other 3 templates. Yii team makes a difference between word Demo and Template. Templates are simpler:

This article is based on the Web App template as in the past the best Demo Diary was not known to me. I will use it for inspiration.

Git

All my code is available in my GitHub repositories:

I will be using it as a boiler-plate for my future projects so it should be always up-to-date and working.

My app is based on the official demo

Docker

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 (if the framework also uses PSR).

Dependency injection + container

Check this YouTube video for explanation

135 packages by Yii

The Yii team split the functionalities if Yii3 into 135 packages which you should check in advance to know what is available. They are listed here or the same is also here.

For example login and RBAC, which can be stored in DB or in PHP files. And many more like "active-record", "form-model" or "boostrap5".

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']));

Theory around __invoke():

  • If a class implements the __invoke() method it is a callable- or invokable-object.
  • Its instance can be used as an anonymous function, callable or closure.
  • __invoke() implements the main (or the only) functionality of the object.

  • Why not to use $instance->myMethod() instead? You would need to implement an API and others would have to know it. Calling $instance() is a "universal anonymous API". Plus modern middleware or handlers often need to be passed as "callables".
  • Usual anonymous functions can only do a simple task. When you use $instance(), you are backed with a large object which can do much more. It can also use traits, state or OOP benefits.
  • Method __invoke($a ,$b) can take input parameters. But the application must know about them, which brings me back to interfaces. I am confused a little. So the invoke-params should probably be mostly provided by the DI I guess.
  • But you still can use method instead of invokation. For example in file config/common/routes.php you can use both:
    • ->action(Web\HomePage\Action::class) // __invoke() needed
    • ->action([Web\HomePage\Action::class, 'run'])
  • Invokable objects are often used for middleware as it fits naturally into dispatcher and pipeline systems. Middleware can then be stateful. But the same aplies to interface-based approaches, you only need to specify the method.

Summary:

Whenever a method requires a callable as the input parameter, you can supply "named function", "anonymous function" or "invokable object". It is up to you what you pick.

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.

You need to add special "cycle" dependencies to use the annotations. See composer.json in the deprecated yii3 demo:

===== Yii3 - How to start ======

Yii3 offers more basic applications: Web, Console, API. I will be using the WEB 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:

In this text I am mainly working with API endpoints, but UI will be presented later as well.

.env files

There are quite a lot of .env files which was confusing. But in short:

  • File /.env.example should be copied and renamed to /.env only if you are NOT using Docker.
  • Then there are the .env files in the docker folder. To understand, just study file ./Makefile. It is important to know that this file is here to call docker commands for you. Do not call docker manually. This is the important philosophy.
  • In the ./Makefile you will see command include docker/.env and below more compose.yml are loaded using the -f argument
  • So docker/.env is only visible in files compose.yml
  • If you need to inject any environment variable into your PHP application, you can place it into the .env files that are used inside (for example) docker/dev/compose.yml.

Running the demo application

You may be surprised that docker-compose.yml is missing in the root. Instead the "make up", "make stop" 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

  • http://localhost:80
  • If run via browser, XML is returned
  • If run via Postman or Ajax, JSON is returned

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.

Disclaimer

I am just learning Yii3. Its new architecture is not my cup of tea so I am trying my best to reach my goals. (I left world of Java+Spring because of these complex DI configurations and now it's back :-o )

If you find a better implementation, let me know. I am not an architect, but a real-life developer who solves daily problems in the world of business and industry so sometimes I may produce too straightforward or philosophically imperfect solutions.

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: yii3web_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: yii3web_db
    environment:
      MARIADB_ROOT_PASSWORD: root
      MARIADB_DATABASE: db
      MARIADB_USER: db
      MARIADB_PASSWORD: db
  adminer:
    image: adminer:latest
    container_name: yii3web_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: yii3web_php, yii3web_adminer, yii3web_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 use 4 different projects at the same time and all cannot run on port 80. Yes, you can technically remove those public (exposed) ports completely and use Caddy for routing, but it is level 2. This approach can be used on production

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 commands, you can use the "make command". Just prepend the composer commands with "make". Or you can use "docker exec -it {container_name} /bin/bash" to enter the container and you can freely use any commands Or type "docker exec -it {container_name} composer install" to directly call composer.

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 was not ready when I created this page. The solution was to use existing class Yiisoft\Db\Query\Query. (ActiveRecord will be shown later below.)

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

Debugging in xDebug (xdebug.ini)

It was a little tricky, but it is simple. Make sure your file docker/dev/.env contains following. (tested on MacOs + Docker Desktop):

XDEBUG_MODE=develop,debug,coverage
XDEBUG_CONFIG="client_host=host.docker.internal start_with_request=yes"

Method xdebug_info() will still show xdebug.mode=develop, but Step Debugger will be enabled.

You can enhance it by appending idekey, but IDE setup then may require you to specify "IDE Key (session id)", the host (localhost:80) and set "path mapping" so that your local project-folder is mapped to "/app". All is done via IDE configuration.

XDEBUG_CONFIG="client_host=host.docker.internal start_with_request=yes idekey=yii3debug

If you want to see correct value xdebug.mode in xdebug_info(), or generally configure xdebug via xdebug.ini, do this:

docker/dev/xdebug.ini:

xdebug.mode=develop,debug,coverage
xdebug.client_host=host.docker.internal
xdebug.start_with_request=yes
xdebug.idekey=yii3debug

docker/Dockerfile:

COPY docker/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini

dockerignore:

!/docker/dev/xdebug.ini

php.ini

You can do the same as above (chapter about xDebug) with php.ini for example to set the timezone. I recommend only using UTC in PHP and DB. Only if user needs to display the datetime in their timezone, concert it in the UI.

[Date]
date.timezone = "UTC"

docker/Dockerfile:

COPY docker/dev/php.ini /usr/local/etc/php/conf.d/custom.ini

Pjax

Pjax does not exist any more, but you can use HTMX instead:

<script src="htmx.min.js"></script>

Your action:

<?php
declare(strict_types=1);
namespace App\Web\HomePage;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
final readonly class Htmx
{
    public function __invoke(
        ResponseFactoryInterface $responseFactory
    ): ResponseInterface
    {
        $response = $responseFactory->createResponse();
        $response
            ->getBody()
            ->write('You are at homepage.<div id="id2">Welcome</div>');
        return $response;
    }
}

And then a simple HTML

<h2>HTMX test</h2>
<div id="htmx">
    <p>This is the original text</p>
</div>
<button data-hx-get="/htmx"
        data-hx-trigger="click"
        data-hx-target="#htmx"
        data-hx-select="#id2"
        data-hx-swap="innerHTML">
    Click me (and watch the traffic in devtools)
</button>

GridView + CRUD for Users

Grid view is in package yiisoft/yii-dataview. The source code is extremely verbose and complex to me, so I am just linking my GitHub demo:

In the UpdateAction I am presenting the ActiveRecord

Below you can also see how I created the UpdateAction + form to enable editing users.

In Yii3, validation rules are not part of the model - special form-class must be created. In Yii2 you didn't need the form-class for model-driven (db-driven) forms.

ActiveRecord

Example of usage is in the following files. ActiveRecord combines 2 classes: Entity + Repository = data and DB manipulation.

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
[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 Wed, 16 Sep 2026 00:10:52 +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. Wed, 16 Sep 2026 00:10:52 +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 Wed, 16 Sep 2026 00:10:52 +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 Tue, 01 Sep 2026 19:46:54 +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. The repository
  3. Switching languages and Language in URL
  4. Search and replace
  5. Virtualization - Vagrant and Docker - why and how
  6. Running Yii project in Vagrant. (Simplified version)
  7. Running Yii project in Docker (Update: xDebug added below!)
  8. Enabling xDebug in Docker, yii demo application
  9. Docker - Custom php.ini
  10. How to enter Docker's bash (cli, command line)
  11. AdminLTE - overview & general research on the theme
  12. Creating custom Widget
  13. Tests - unit + functional + acceptance (opa) + coverage
  14. Microsoft Access MDB
  15. Migration batch insert csv

My articles

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

The repository

All that you see in this Wiki is used in my Yii2 demo application. Just clone it and you are ready to start. Feel free to use it.

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 Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/wiki/2566/how-to-redirect-all-emails-to-one-inbox-on-yii2-applications https://www.yiiframework.com/wiki/2566/how-to-redirect-all-emails-to-one-inbox-on-yii2-applications glpzzz glpzzz

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

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

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

Well, next there is a solution:

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

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

    return $mailer->generateMessageFileName();
}

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

  1. Attach a json file with the real recipients information so we can review it
  2. Set the recipient (TO) as the email address where we want to receive all the emails.
  3. Set the others recipients fields as null
  4. Deactivate useFileTransport
  5. Send the email
  6. Activate useFileTransport
  7. Return the defaut file name (datetime of the operation)

This way we both receive all the emails on the specified account and get them stored on @runtime/mail.

Pretty simple helper to review emails on Yii2 applications.

Originally posted on: https://glpzzz.github.io/2020/10/02/yii2-redirect-all-emails.html

]]>
0
[wiki] Api of Multiple File Uploading in Yii2 Tue, 05 Jul 2022 03:01:39 +0000 https://www.yiiframework.com/wiki/2565/api-of-multiple-file-uploading-in-yii2 https://www.yiiframework.com/wiki/2565/api-of-multiple-file-uploading-in-yii2 fezzymalek fezzymalek

After getting lot's of error and don't know how to perform multiple images api in yii2 finally I get it today

This is my question I asked on forum and it works for me https://forum.yiiframework.com/t/multiple-file-uploading-api-in-yii2/130519

Implement this code in model for Multiple File Uploading

public function rules()
    {
        return [
            [['post_id', 'media'], 'required'],
            [['post_id'], 'integer'],
            [['media'], 'file', 'maxFiles' => 10],//here is my file field
            [['created_at'], 'string', 'max' => 25],
            [['post_id'], 'exist', 'skipOnError' => true, 'targetClass' => Post::className(), 'targetAttribute' => ['post_id' => 'id']],
        ];
    }
    

You can add extension or any skiponempty method also in model.

And this is my controller action where I performed multiple file uploading code.

public function actionMultiple(){
        $model = new Media;
        $model->post_id = '2';
        if (Yii::$app->request->ispost) {
            $model->media = UploadedFile::getInstances($model, 'media');
            if ($model->media) {
                foreach ($model->media as $value) {
                    $model = new Media;
                    $model->post_id = '2';
                    $BasePath = Yii::$app->basePath.'/../images/post_images';
                    $filename = time().'-'.$value->baseName.'.'.$value->extension;
                    $model->media = $filename;
                    if ($model->save()) {
                        $value->saveAs($BasePath.$filename);
                    }
                }
                return array('status' => true, 'message' => 'Image Saved'); 
            }
        }
        return array('status' => true, 'data' => $model);
    }

If any query or question I will respond.

]]>
0
[wiki] How to email error logs to developer on Yii2 apps Wed, 16 Sep 2026 00:10:52 +0000 https://www.yiiframework.com/wiki/2564/how-to-email-error-logs-to-developer-on-yii2-apps https://www.yiiframework.com/wiki/2564/how-to-email-error-logs-to-developer-on-yii2-apps glpzzz glpzzz

Logging is a very important feature of the application. It let's you know what is happening in every moment. By default, Yii2 basic and advanced application have just a \yii\log\FileTarget target configured.

To receive emails with messages from the app, setup the log component to email (or Telegram, or slack) transport instead (or besides) of file transport:

'components' => [
    // ...
    'log' => [
         'targets' => [
             [
                 'class' => 'yii\log\EmailTarget',
                 'mailer' => 'mailer',
                 'levels' => ['error', 'warning'],
                 'message' => [
                     'from' => ['log@example.com'],
                     'to' => ['developer1@example.com', 'developer2@example.com'],
                     'subject' => 'Log message',
                 ],
             ],
         ],
    ],
    // ...
],

The \yii\log\EmailTarget component is another way to log messages, in this case emailing them via the mailer component of the application as specified on the mailer attribute of EmailTarget configuration. Note that you can also specify messages properties and which levels of messages should be the sent trough this target.

If you want to receive messages via other platforms besides email, there are other components that represents log targets:

Or you can implement your own by subclassing \yii\log\Target

]]>
0
[wiki] How to add Schema.org markup to Yii2 pages Fri, 11 Sep 2020 22:09:55 +0000 https://www.yiiframework.com/wiki/2560/how-to-add-schema-org-markup-to-yii2-pages https://www.yiiframework.com/wiki/2560/how-to-add-schema-org-markup-to-yii2-pages glpzzz glpzzz

https://schema.org is a markup system that allows to embed structured data on their web pages for use by search engines and other applications. Let's see how to add Schema.org to our pages on Yii2 based websites using JSON-LD.

Basically what we need is to embed something like this in our pages:

<script type="application/ld+json">
{ 
  "@context": "http://schema.org/",
  "@type": "Movie",
  "name": "Avatar",
  "director": 
    { 
       "@type": "Person",
       "name": "James Cameron",
       "birthDate": "1954-08-16"
    },
  "genre": "Science fiction",
  "trailer": "../movies/avatar-theatrical-trailer.html" 
}
</script>

But we don't like to write scripts like this on Yii2, so let's try to do it in other, more PHP, way.

In the layout we can define some general markup for our website, so we add the following snippet at the beginning of the@app/views/layouts/main.php file:

<?= \yii\helpers\Html::script(isset($this->params['schema'])
    ? $this->params['schema']
    : \yii\helpers\Json::encode([
        '@context' => 'https://schema.org',
        '@type' => 'WebSite',
        'name' => Yii::$app->name,
        'image' => $this->image,
        'url' => Yi::$app->homeUrl,
        'descriptions' => $this->description,
        'author' => [
            '@type' => 'Organization',
            'name' => Yii::$app->name,
            'url' => 'https://www.hogarencuba.com',
            'telephone' => '+5352381595',
        ]
    ]), [
    'type' => 'application/ld+json',
]) ?>

Here we are using the Html::script($content, $options) to include the script with the necessary type option, and Json::encode($value, $options) to generate the JSON. Also we use a page parameter named schema to allow overrides on the markup from other pages. For example, in @app/views/real-estate/view.php we are using:

$this->params['schema'] = \yii\helpers\Json::encode([
    '@context' => 'https://schema.org',
    '@type' => 'Product',
    'name' => $model->title,
    'description' => $model->description,
    'image' => array_map(function ($item) {
        return $item->url;
    }, $model->images),
    'category' => $model->type->description_es,
    'productID' => $model->code,
    'identifier' => $model->code,
    'sku' => $model->code,
    'url' => \yii\helpers\Url::current(),
    'brand' => [
        '@type' => 'Organization',
        'name' => Yii::$app->name,
        'url' => 'https://www.hogarencuba.com',
        'telephone' => '+5352381595',
    ],
    'offers' => [
        '@type' => 'Offer',
        'availability' => 'InStock',
        'url' => \yii\helpers\Url::current(),
        'priceCurrency' => 'CUC',
        'price' => $model->price,
        'priceValidUntil' => date('Y-m-d', strtotime(date("Y-m-d", time()) . " + 365 day")),
        'itemCondition' => 'https://schema.org/UsedCondition',
        'sku' => $model->code,
        'identifier' => $model->code,
        'image' => $model->images[0],
        'category' => $model->type->description_es,
        'offeredBy' => [
            '@type' => 'Organization',
            'name' => Yii::$app->name,
            'url' => 'https://www.hogarencuba.com',
            'telephone' => '+5352381595',
        ]
    ]
]);

Here we redefine the schema for this page with more complex markup: a product with an offer.

This way all the pages on our website will have a schema.org markup defined: in the layout we have a default and in other pages we can redefine setting the value on $this->params['schema'].

]]>
0