Live News for Yii Framework News, fresh extensions and wiki articles about Yii framework. Fri, 24 Apr 2026 04:17:00 +0000 Zend_Feed_Writer 2 (http://framework.zend.com) https://www.yiiframework.com/ [extension] ayunanadi Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/extension/ayunanadi https://www.yiiframework.com/extension/ayunanadi bittineme19-dev bittineme19-dev

Cara membuka blokir BWS Mobile dapat dilakukan melalui WhatsApp +62813-7777-021, BWS Call resmi 1500-012.

]]>
0
[news] Yii Log Email Target 4.2 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/801/yii-log-email-target-4-2 https://www.yiiframework.com/news/801/yii-log-email-target-4-2 vjik vjik

Yii Log Email Target version 4.2.0 was released. In this version:

  • Change PHP constraint in composer.json to 8.1 - 8.5
  • Raise the minimum yiisoft/mailer version to ^6.0 and adapt the code accordingly
  • Explicitly import classes in "use" section
]]>
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
[news] Yii3 FrankenPHP worker runner 1.0.0 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/800/yii3-frankenphp-worker-runner-1-0-0 https://www.yiiframework.com/news/800/yii3-frankenphp-worker-runner-1-0-0 samdark samdark

First stable version of FrankenPHP worker runner released. The runner allows using FrankenPHP in worker mode. In this mode application is initializer once per worker and is serving multiple reqests in this state resulting in signficiant performance gains.

Since the application does not "die" on each request, you should clean up state carefully. Yii3 has means for that via DI container state resetters.

Turning default application templates into worker-mode enabled ones is described well in the README.

]]>
0
[news] Yii3 application templates 1.4.0 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/799/yii3-application-templates-1-4-0 https://www.yiiframework.com/news/799/yii3-application-templates-1-4-0 samdark samdark

Both web template and API template got 1.4.0 release which adds expicit Caddyfile configs for production and development Docker environments.

These configs are in docker/Caddyfile for production image and docker/dev/Caddyfile for development image. After these are edited you need to rebild the image with make build to apply changes.

]]>
0
[news] Response Download 1.1.0 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/798/response-download-1-1-0 https://www.yiiframework.com/news/798/response-download-1-1-0 samdark samdark

Response download version 1.1.0 was released. In this version:

  • Add support for psr/http-message version ^2.0
  • Try to determine content MIME type automatically
]]>
0
[news] Yii API Application Template 1.3 Mon, 06 Apr 2026 22:05:50 +0000 https://www.yiiframework.com/news/797/yii-api-application-template-1-3 https://www.yiiframework.com/news/797/yii-api-application-template-1-3 samdark samdark

Yii3 API project template version 1.3.0 was released.

In this version:

  • Do not write logs to file since that's not needed for both Docker and ./yii serve
  • Add .env for development without Docker
  • Add "service update paused" case for swarm deployment log parsing
]]>
0
[news] Yii3 web application template 1.3.0 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/796/yii3-web-application-template-1-3-0 https://www.yiiframework.com/news/796/yii3-web-application-template-1-3-0 samdark samdark

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

  • Replace deprecated PHP CS Fixer rule set @PER-CS2.0 with @PER-CS
  • Do not write logs to file since that's not needed for both Docker and ./yii serve
  • Allow symfony/console 8
  • Remove yiisoft/data-response dependency
  • Add .env for development without Docker
  • Add grouping to make help output
  • Update composer dependencies and refactor to replace use of deprecated classes
  • Add "service update paused" case for swarm deployment log parsing
]]>
0
[news] Proxy 1.2.0 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/795/proxy-1-2-0 https://www.yiiframework.com/news/795/proxy-1-2-0 samdark samdark

Yii Proxy version 1.2 was released.

This version adds support for PHP 8.5.

]]>
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
[news] Yii DataView 1.1 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/794/yii-dataview-1-1 https://www.yiiframework.com/news/794/yii-dataview-1-1 vjik vjik

Yii DataView version 1.1.0 was released. In this version:

  • Add caption support for GridView
  • Explicitly import constants in "use" section
  • Bump minimal yiisoft/html version to 3.13 and add support for ^4.0
]]>
0
[news] Yii HTML 4 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/793/yii-html-4 https://www.yiiframework.com/news/793/yii-html-4 vjik vjik

Yii HTML version 4 was released. In this version:

  • Allow passing int, float and null as tag content
  • Throw LogicException in Tag::id() when id is empty string
  • Remove tag attributes sorting
  • Make all CheckboxItem and RadioItem properties required
  • Rename $options parameter to $attributes in Html::addCssClass(), Html::removeCssClass(), Html::addCssStyle() and Html::removeCssStyle() methods

See upgrading instructions with notes about upgrading package in your application to this major version.

]]>
0
[news] Yii HTML 3.13 Fri, 13 Mar 2026 13:09:24 +0000 https://www.yiiframework.com/news/792/yii-html-3-13 https://www.yiiframework.com/news/792/yii-html-3-13 samdark samdark

Yii HTML version 3.13.0 was released. In this version:

  • Added $attributes parameter to Html::li() method
  • Deprecateed static constructors such as TagName::tag() in favor of new TagName()
  • Enhanced RadioList::addRadioWrapClass() method for cleaner class addition
  • Explicitly imported classes and constants in "use" section
]]>
0
[news] Yii HTTP Middleware 1.2 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/791/yii-http-middleware-1-2 https://www.yiiframework.com/news/791/yii-http-middleware-1-2 vjik vjik

Yii HTTP Middleware version 1.2.0 was released. In this version:

  • Add RedirectMiddleware
  • Change PHP constraint in composer.json to 8.1 - 8.5
]]>
0
[news] Yii API Application Template 1.2 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/790/yii-api-application-template-1-2 https://www.yiiframework.com/news/790/yii-api-application-template-1-2 vjik vjik

Yii3 API project template 1.2.0

Yii API Application Template version 1.2.0 was released. In this version:

  • Improve prod-deploy error handling so exact error is printed in case of rollback
  • Refactor Makefile default command help logic
  • Allow symfony/console 8
  • Remove mutation testing
  • Refactor PresenterInterface and implementations for preparing data only
  • Set locale C.UTF-8 in Dockerfile
  • Update composer dependencies and refactor to replace use of deprecated classes
  • Add grouping to make help output
  • Fix incorrect .env files used in Docker Compose for production
  • Fix psalm cache directory in configuration file
]]>
0
[news] Bootstrap5 1.1.0 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/789/bootstrap5-1-1-0 https://www.yiiframework.com/news/789/bootstrap5-1-1-0 samdark samdark

Bootstrap 5 version 1.1.0 was released.

In this version:

  • Add Nav::activateParents() that makes parent dropdown active when one of its child items is active
  • Allow Dropdown::togglerVariant() to be null, avoiding it setting a variant class
  • Add visible to Dropdown and DropdownItem
  • Add navId() method to NavBar widget
  • Explicitly import classes and functions in "use" section
  • Add PHP 8.5 support
  • Remove unnecessary files from Composer package
  • Update Bootstrap from 5.0.0-beta to 5.3.8
  • Add support for custom togglerClasses in Dropdown widget, and addDropdownClass() method
  • Link buttons get btn-secondary class instead of btn-link class
]]>
0
[news] Yii Mailer 6.1 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/788/yii-mailer-6-1 https://www.yiiframework.com/news/788/yii-mailer-6-1 vjik vjik

Yii Mailer version 6.1.0 was released. In this version:

  • Add debug collector
  • Explicitly import classes, functions, and constants in "use" section
  • Remove unnecessary files from Composer package
  • Add PHP 8.5 support
]]>
0
[news] Yii View Renderer 7.4 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/787/yii-view-renderer-7-4 https://www.yiiframework.com/news/787/yii-view-renderer-7-4 vjik vjik

Minor version of Yii View Renderer were tagged.

  • Introduce WebViewRenderer instead of ViewRenderer which is marked as deprecated.
  • Explicitly import classes, functions, and constants in "use" section.
]]>
0
[news] Yii Data Response 2.2 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/786/yii-data-response-2-2 https://www.yiiframework.com/news/786/yii-data-response-2-2 vjik vjik

Yii Data Response version 2.2.0 was released. In this version:

  • Add DataStream
  • Add FormatterInterface and implementations: HtmlFormatter, JsonFormatter, PlainTextFormatter, XmlFormatter
  • Add DataResponseFactoryInterface and implementations: DataResponseFactory, FormattedResponseFactory, HtmlResponseFactory, JsonResponseFactory, PlainTextResponseFactory, XmlResponseFactory
  • Add middlewares: XmlDataResponseMiddleware, HtmlDataResponseMiddleware, JsonDataResponseMiddleware, PlainTextDataResponseMiddleware and DataResponseMiddleware
  • Add ContentNegotiatorResponseFactory and ContentNegotiatorDataResponseMiddleware
  • Deprecate DataResponse, DataResponseFactory, DataResponseFactoryInterface, DataResponseFormatterInterface, ResponseContentTrait, HtmlDataResponseFormatter, JsonDataResponseFormatter, PlainTextDataResponseFormatter, XmlDataResponseFormatter, ContentNegotiator, FormatDataResponse, FormatDataResponseAsHtml, FormatDataResponseAsJson, FormatDataResponseAsPlainText, FormatDataResponseAsXml
  • Explicitly import classes, functions, and constants in "use" section
  • Remove unnecessary files from Composer package
]]>
0
[news] Yii3 support added to Tideways profiler Fri, 20 Feb 2026 15:30:59 +0000 https://www.yiiframework.com/news/785/yii3-support-added-to-tideways-profiler https://www.yiiframework.com/news/785/yii3-support-added-to-tideways-profiler samdark samdark

Yii3 support was added to Tideways profiler additionally to existing verison 1 and 2 support.

Happy profiling!

]]>
0
[news] Yii3 web application template 1.2.0 Fri, 24 Apr 2026 04:16:59 +0000 https://www.yiiframework.com/news/784/yii3-web-application-template-1-2-0 https://www.yiiframework.com/news/784/yii3-web-application-template-1-2-0 samdark samdark

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

  • Improved prod-deploy error handling so exact error is printed in case of rollback.
  • Removed mutation testing since that's likely not to be used in applications. Usually tests aren't covering 100% of the code.
  • Removed roave/security-advisories since Composer 2.9 handles security advisories natively.
  • Refactored Makefile default command help logic.
  • Set locale C.UTF-8 in Dockerfile.
]]>
0
[news] Security lessons Yii learned thanks to GitHub Secure Open Source Fund Tue, 17 Feb 2026 19:35:47 +0000 https://www.yiiframework.com/news/783/security-lessons-yii-learned-thanks-to-github-secure-open-source-fund https://www.yiiframework.com/news/783/security-lessons-yii-learned-thanks-to-github-secure-open-source-fund samdark samdark

Last year Yii was chosen to be in session 3 of the GitHub Secure Open Source Fund as a project that impacts a significant part of the PHP landscape.

We were in good company together with many wonderful projects such as LLVM, Node.js, CPython, curl, ImageMagick, webpack, and jQuery.

It was quite an experience. We've verified that we were already doing well and learned and adopted new things. As a result, Yii became an even more secure base for your projects.

What we've learned/adopted:

  • GitHub actions could be an attack vector. We've revised our actions.
  • Immutable releases are great. We've started to use these immediately.
  • The security report could be out of the framework scope to fix in case multiple practices recommended in the documentation are violated. It will help us to decide on issues faster and more precisely.
  • Creating public threat models could be beneficial for the project. We'll definitely try it in the future.
  • Security configurations are a great way to enforce particular rules for the whole GitHub organization. Very useful for Yii3, which has many packages.
  • Having an incident response plan is a good idea since when the time comes, it's better to follow the plan instead of making things up on the go.

Security is not a one-time action; it is a process, and we are committed to it.

Thanks, GitHub!

]]>
0
[extension] chinaphp/yii2-ide-helper Fri, 24 Apr 2026 04:17:00 +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
[news] Yii Test Support 3.2 Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/news/782/yii-test-support-3-2 https://www.yiiframework.com/news/782/yii-test-support-3-2 vjik vjik

Supporting tools for testing version 3.2.0 was released.

In this version added StringStream, a test-specific implementation of PSR-7 stream.

]]>
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 Fri, 24 Apr 2026 04:17:00 +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 ]]> 0 [extension] davidrnk/yii2-recurring-date Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/extension/davidrnk/yii2-recurring-date https://www.yiiframework.com/extension/davidrnk/yii2-recurring-date DavidRmz DavidRmz

yii2-recurring-date

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

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

Main Features

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

Installation

Install the extension with Composer:

composer require davidrnk/yii2-recurring-date

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

Usage

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

Usage with model (ActiveForm):

use davidrnk\RecurringDate\Widget\RecurringDate;

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

Usage without model:

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

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

JSON Format (persisted)

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

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

Relevant keys:

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

Calculation of the Next Expiration Date

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

use davidrnk\RecurringDate\Core\RecurringDateEngine;

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

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

Quick example:

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

Configuration and Customization

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

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

Example of label customization:

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

Validations and UX Behavior

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

Internationalization (i18n)

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

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

Tests

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

vendor/bin/phpunit tests/RecurringDateEngineTest.php

Best Practices and Notes

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

Contributing

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

License

BSD-3-Clause — see LICENSE file.

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

993323

Yii2 - Modern Starter Kit

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

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

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

Screenshots

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

Requirements

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

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

Installation

Step 1: Clone the Repository
git clone git@github.com:crenspire/yii2-react-starter.git
cd yii2-react-starter

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

Step 2: Install PHP Dependencies

Install all PHP dependencies using Composer:

composer install

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

Step 3: Install Node.js Dependencies

Install all frontend dependencies:

npm install

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

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

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

Step 5: Run Database Migrations

Run the migrations to create all necessary database tables:

php yii migrate

This will create the following tables:

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

Create an admin user for testing:

php yii seed/admin

This creates an admin user with:

  • Email: admin@example.com
  • Password: admin123
Step 7: Configure Cookie Validation Key

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

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

You can generate a random string using:

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

You need to run two servers simultaneously:

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

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

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

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

Open your browser and navigate to:

http://localhost:8080

You should see the home page. You can now:

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

Production Build

For production deployment, build the frontend assets:

npm run build

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

Configuration

Environment Configuration

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

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

NOTES:

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

Directory Structure

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

Testing

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

  • unit
  • functional
  • acceptance

Tests can be executed by running

vendor/bin/codecept run

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

Running acceptance tests

To execute acceptance tests do the following:

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

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

  3. Update dependencies with Composer

    composer update

  4. Download Selenium Server and launch it:

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

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

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

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

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

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

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

    tests/bin/yii migrate
    

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

  1. Start web server:

    tests/bin/yii serve

  2. Now you can run all available tests

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

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

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

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

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

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

]]>
0
[extension] crenspire/yii2-inertia Thu, 11 Dec 2025 12:26:03 +0000 https://www.yiiframework.com/extension/crenspire/yii2-inertia https://www.yiiframework.com/extension/crenspire/yii2-inertia akshaypjoshi akshaypjoshi ]]> 0 [wiki] Building Modern SPAs with Yii2 and Inertia.js Fri, 24 Apr 2026 04:17:00 +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
[extension] ldkafka/yii2-scheduler Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/extension/ldkafka/yii2-scheduler https://www.yiiframework.com/extension/ldkafka/yii2-scheduler ldk ldk

yii2-scheduler

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

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

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

Installation

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

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

use ldkafka\scheduler\ScheduledJob;

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

Usage

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

Upgrade notes

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

Release checklist

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

use ldkafka\scheduler\ScheduledJob;
use Yii;

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

Notes:

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

Logging

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

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

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

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

Monitoring Events

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

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

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

Cleanup and safety

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

Compatibility

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

License

BSD-3-Clause

]]>
0
[extension] ldkafka/yii2-google-gemini Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/extension/ldkafka/yii2-google-gemini https://www.yiiframework.com/extension/ldkafka/yii2-google-gemini ldk ldk

Yii2 Google Gemini Component (v2.0.0)

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

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

What’s New in 2.0.0

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

Feature Summary

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

Requirements

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

Installation

composer require ldkafka/yii2-google-gemini

Quick Start

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

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

Usage Examples

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

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

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

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

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

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

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

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

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

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

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

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

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

Caching Modes Deep Dive

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

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

Console Commands

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

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

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

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

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

Configuration Options

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

Supported Models (Snapshot)

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

See Gemini Models Documentation for full list.

Canonical Response Format

All methods return:

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

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

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

Helper Methods

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

Cache Modes (Summary)

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

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

Error Handling Pattern

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

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

Advanced Usage

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

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

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

Example JSON payload as sent to the API:

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

Notes:

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

Testing

The package includes comprehensive test actions:

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

Troubleshooting

"API key not configured"

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

"Failed to create server cache"

Server caching requires:

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

Use client-side caching for shorter conversations.

Streaming not working

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

Production Notes

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

Links

License

BSD-3-Clause (matches class header).

Support / Contributing

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

When reporting an issue, include:

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

Enjoy building with Gemini! Suggestions & improvements welcome.

]]>
0
[extension] rft/yii2-searchable-depdrop Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/extension/rft/yii2-searchable-depdrop https://www.yiiframework.com/extension/rft/yii2-searchable-depdrop zenjaku zenjaku

Searchable & Dependent Dropdown Widget for Yii2

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

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

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

Features

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

Installation

Via Composer (Recommended)

The preferred way to install this extension is through Composer.

composer require rft/yii2-searchable-depdrop

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

Manual Installation (Alternative)

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

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

Usage

1. Controller Action for Dependent Data

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

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

Example Controller Action:

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

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

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

    return $out;
}
2. View File Setup

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

A. Standalone Searchable Dropdown

use rft\searchabledepdrop\widgets\SearchableDepDrop;

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

B. Multiple Selection Dropdown

use rft\searchabledepdrop\widgets\SearchableDepDrop;

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

C. Dependent Dropdown

Example for a State → City dropdown setup:

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

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

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

Styling Features

The widget comes with built-in CSS that provides:

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

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

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

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

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

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

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

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

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

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

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

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

Configuration Options

The widget supports several configuration options:

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

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

Essential Widget Usage:

use rft\searchabledepdrop\widgets\SearchableDepDrop;

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

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

Required JavaScript for Dynamic Forms:

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

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

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

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

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

Multiple Selection Example

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

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

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

Important Notes:

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

Recent Changes (v1.0.1)

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

If you're upgrading from a previous version:

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

License

This project is licensed under the MIT License.

]]>
0
[extension] mspirkov/yii2-web Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/extension/mspirkov/yii2-web https://www.yiiframework.com/extension/mspirkov/yii2-web max-s-lab max-s-lab

993323

Yii2 Web Extension

  1. Installation
  2. Components

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

PHP Yii 2.0.x Tests PHPStan Coverage PHPStan Level Max

Installation

Run

php composer.phar require mspirkov/yii2-web

or add

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

to the require section of your composer.json file.

Components

CookieManager

A utility class for managing cookies.

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

It contains the following methods:

  • has - checks if a cookie with the specified name exists.
  • get - returns the cookie with the specified name.
  • add - adds a cookie to the response.
  • remove - removes a cookie.
  • removeAll - removes all cookies.
Usage example
Initialization

Add the definition to the container configuration in the definitions section:

use MSpirkov\Yii2\Web\CookieManagerInterface;
use MSpirkov\Yii2\Web\CookieManager;
use MSpirkov\Yii2\Web\Request;
use yii\web\Response;

return [
    ...
    'container' => [
        'definitions' => [
            CookieManagerInterface::class => static fn() => new CookieManager(
                Instance::ensure('request', Request::class),
                Instance::ensure('response', Response::class),
            ),
        ],
    ],
    ...
];
Usage
use MSpirkov\Yii2\Web\CookieManagerInterface;

final readonly class ExampleService
{
    public function __construct(
        private CookieManagerInterface $cookieManager,
    ) {}

    public function addCookie(): void
    {
        $this->cookieManager->add([
            'name' => 'someCookieName',
            'value' => 'someCookieValue',
        ]);
    }
}
Html

This package contains 4 helpers:

  1. MSpirkov\Yii2\Web\Html - a helper that extends yii\web\Html
  2. MSpirkov\Yii2\Web\Bootstrap3\Html - a helper that extends yii\bootstrap\Html
  3. MSpirkov\Yii2\Web\Bootstrap4\Html - a helper that extends yii\bootstrap4\Html
  4. MSpirkov\Yii2\Web\Bootstrap5\Html - a helper that extends yii\bootstrap5\Html

[!IMPORTANT]

To use Bootstrap helpers, you need to install the corresponding packages (yii2-bootstrap, yii2-bootstrap4, or yii2-bootstrap5)

All of them contain methods from the HtmlTrait and allow you to use its features without having to create your own basic helper.

HtmlTrait

A trait that extends the basic functionality of the \yii\helpers\Html helper.

Usage example
use MSpirkov\Yii2\Web\HtmlTrait;

final class Html extends \yii\helpers\Html
{
    use HtmlTrait;
}

You can also use this trait with other helpers that extends \yii\helpers\Html. For example:

use MSpirkov\Yii2\Web\HtmlTrait;

final class Html extends \yii\bootstrap5\Html
{
    use HtmlTrait;
}
Method singleButtonForm

Сreates a form as a single button with hidden inputs. This can be useful when you need to perform an action when you click a button, such as deleting an item. This allows you to easily perform a request without manually creating a form, hidden inputs, etc.

Usage example:

<?= Html::singleButtonForm(['product/delete'], ['id' => $product->id], 'Delete'); ?>
Request

A wrapper for \yii\web\Request that uses the capabilities of RequestTrait and allows you to use them without having to create your own basic Request.

Configuration

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

use MSpirkov\Yii2\Web\Request;

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

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

<?php

use yii\BaseYii;
use yii\web\Application as BaseWebApplication;
use yii\console\Application as BaseConsoleApplication;
use MSpirkov\Yii2\Web\Request;

final class Yii extends BaseYii
{
    /** @var __WebApplication|__ConsoleApplication  */
    public static $app;
}

/**
 * @property-read Request $request
 */
final class __WebApplication extends BaseWebApplication {}

final class __ConsoleApplication extends BaseConsoleApplication {}
Basic Controller (Optional)

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

use MSpirkov\Yii2\Web\Request;
use yii\web\Controller as BaseController;

/**
 * @property Request $request
 */
abstract class AbstractController extends BaseController
{
    public function init(): void
    {
        parent::init();

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

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

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

A trait for easier handling of GET and POST parameters.

[!IMPORTANT]

All parameter retrieval methods also allow you to mark parameters as required using the $required parameter.

It contains the following methods:

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

class Request extends \yii\web\Request
{
    use RequestTrait;
}
]]>
0
[extension] mspirkov/yii2-db Sun, 02 Nov 2025 12:46:55 +0000 https://www.yiiframework.com/extension/mspirkov/yii2-db https://www.yiiframework.com/extension/mspirkov/yii2-db max-s-lab max-s-lab

993323

Yii2 DB Extension

  1. Installation
  2. Components

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

PHP Yii 2.0.x Tests PHPStan Coverage PHPStan Level Max

Installation

Run

php composer.phar require mspirkov/yii2-db

or add

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

to the require section of your composer.json file.

Components

AbstractRepository

An abstract class for creating repositories that interact with ActiveRecord models.

Contains the most commonly used methods:

  • findOne - finds a single ActiveRecord model based on the provided condition.
  • findAll - finds all ActiveRecord models based on the provided condition.
  • save - saves an ActiveRecord model to the database.
  • delete - deletes an ActiveRecord model from the database.
  • updateAll - updates the whole table using the provided attribute values and conditions.
  • deleteAll - deletes rows in the table using the provided conditions.

It also has several additional methods:

  • findOneWith - finds a single ActiveRecord model based on the provided condition and eager loads the specified relations.
  • findAllWith - finds all ActiveRecord models based on the provided condition and eager loads the specified relations.
  • getTableSchema - returns the schema information of the DB table associated with current ActiveRecord class.
  • find - creates and returns a new ActiveQuery instance for the current ActiveRecord model.

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

Usage example

Create an interface based on RepositoryInterface:

use MSpirkov\Yii2\Db\ActiveRecord\RepositoryInterface;

/**
 * @extends RepositoryInterface<Product>
 */
interface ProductRepositoryInterface extends RepositoryInterface
{
    /**
     * @return Product[]
     */
    public function findForMainPage(int $limit): array
}

Next, create your repository:

use MSpirkov\Yii2\Db\ActiveRecord\AbstractRepository;

/**
 * @extends AbstractRepository<Product>
 */
final class ProductRepository extends AbstractRepository implements ProductRepositoryInterface
{
    public function __construct()
    {
        parent::__construct(Product::class);
    }

    public function findForMainPage(int $limit): array
    {
        return $this->find()
            ->where(['hidden' => 0])
            ->orderBy(['id' => SORT_DESC])
            ->limit($limit)
            ->all();
    }
}

After that, specify the implementation of the ProductRepositoryInterface interface in the container in the definitions section:

return [
    ...
    'container' => [
        'definitions' => [
            ProductRepositoryInterface::class => ProductRepository::class,
        ],
    ],
    ...
];

After that, you can use the repository as follows:

final readonly class MainService
{
    private const int PRODUCTS_LIMIT = 20;

    public function __construct(
        private ProductRepositoryInterface $productRepository,
    ) {}

    /**
     * @return array{
     *     products: Product[],
     * }
     */
    public function getMainData(int $id): array
    {
        $products = $this->productRepository->findForMainPage(self::PRODUCTS_LIMIT);

        return [
            'products' => $products,
        ];
    }
}
DateTimeBehavior

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

Usage example
use MSpirkov\Yii2\Db\ActiveRecord\DateTimeBehavior;

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

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

By default, this behavior will fill the created_at attribute with the date and time when the associated AR object is being inserted; it will fill the updated_at attribute with the date and time when the AR object is being updated. The date and time are determined relative to $timeZone.

If your attribute names are different or you want to use a different way of calculating the timestamp, you may configure the $createdAtAttribute, $updatedAtAttribute and $value properties like the following:

use MSpirkov\Yii2\Db\ActiveRecord\DateTimeBehavior;
use yii\db\Expression;

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

    public function behaviors(): array
    {
        return [
            [
                'class' => DateTimeBehavior::class,
                'createdAtAttribute' => 'create_time',
                'updatedAtAttribute' => 'update_time',
                'value' => new Expression('NOW()'),
            ],
        ];
    }
}
TransactionManager

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

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

It provides two main methods:

  • safeWrap - executes a callable within a transaction, safely handling exceptions and logging them.
  • wrap - executes a callable within a transaction.
Usage example
Initialization

Add the definition to the container configuration in the definitions section:

use MSpirkov\Yii2\Db\TransactionManagerInterface;
use MSpirkov\Yii2\Db\TransactionManager;

return [
    ...
    'container' => [
        'definitions' => [
            TransactionManagerInterface::class => static fn() => new TransactionManager(Yii::$app->db),
        ],
    ],
    ...
];
Usage
use MSpirkov\Yii2\Db\TransactionManagerInterface;

final readonly class ProductService
{
    public function __construct(
        private TransactionManagerInterface $transactionManager,
        private FilesystemInterface $filesystem,
        private ProductRepositoryInterface $productRepository,
    ) {}

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

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

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

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

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

        return $transactionResult;
    }
}
]]>
0
[extension] dacheng-php/yii2-swoole Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/extension/dacheng-php/yii2-swoole https://www.yiiframework.com/extension/dacheng-php/yii2-swoole dacheng-gao dacheng-gao ]]> 0 [extension] neoacevedo/yii2-fastly-cache Thu, 05 Mar 2026 22:46:25 +0000 https://www.yiiframework.com/extension/neoacevedo/yii2-fastly-cache https://www.yiiframework.com/extension/neoacevedo/yii2-fastly-cache NestorAcevedo NestorAcevedo ]]> 0 [wiki] Yii3 - How to start Thu, 23 Apr 2026 14:08:57 +0000 https://www.yiiframework.com/wiki/2581/yii3-how-to-start https://www.yiiframework.com/wiki/2581/yii3-how-to-start rackycz rackycz

(draft - all will be retested later)

Intro

  1. Git
  2. Docker
  3. PSR Standards by Framework Interoperability Group
  4. Dependency injection + container
  5. 135 packages by Yii
  6. invoke()
  7. Theory around __invoke():
  8. Hash annotations for class attributes
  9. Running the demo application
  10. Adding DB into your project
  11. Enabling MariaDB (MySQL) and migrations
  12. Creating a migration
  13. Running the migrations
  14. Reading data from DB
  15. Seeding the database
  16. Using Repository and the Model class
  17. API login + access token
  18. UI

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.

Git

All my code is available in my GitHub repositories:

  • Yii3 API demo (yii3api) - May be dropped in the future as yii3web offers more)
  • Yii3 WEB demo (yii3web) - I think I will use this one more

I will be using it as a boiler-plate for my future projects so it should be always up-to-date and working.

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 following these principles.

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" (which is now finished), "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 API application:

Clone it like this:

.. and follow the docker instructions in the documentation.

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

Running the demo application

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

  • make composer update
  • make up

... then the web will be available on URL

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

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

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

Adding DB into your project

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

In my case the resulting file looks like this:

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

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

  • DEV_PORT=9080
  • DEV_ADMINER_PORT=9081

Then run following commands:

  • make down
  • make build
  • make up

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

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

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

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

Enabling MariaDB (MySQL) and migrations

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

Installing composer packages

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

So you need to run following commands:

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

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

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

Other solutions:

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

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

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

Setting up composer packages

Follow their documentations. Quick links:

The documentations want you to create 2 files:

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

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

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

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

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

Creating a migration

Run the command to create a migration:

  • make yii migrate:create user

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

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

The down() method should contain this:

$b->dropTable('user');

Running the migrations

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

Then call:

  • make down
  • make build
  • make up

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

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

Reading data from DB

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

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

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

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

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

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

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

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

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

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

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

Seeding the database

Seeding = inserting fake data.

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

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

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

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

namespace App\Console;

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

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

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

        $faker = Factory::create();

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

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

        return ExitCode::OK;
    }
}

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

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

Using Repository and the Model class

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

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

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

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

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

API login + access token

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

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

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

Before you start, install dependency:

composer require yiisoft/security

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

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

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

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

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

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

The User model will need one more method:

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

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

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

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

UI

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>

JS client - Installable Vuejs3 PWA

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

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

Yii 2 extension for render views using Fenom template engine.

Installation

composer require ensostudio/yii2-fenom

Usage

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

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

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

]]>
0
[extension] sahmed237/yii2-admin-theme Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/extension/sahmed237/yii2-admin-theme https://www.yiiframework.com/extension/sahmed237/yii2-admin-theme s.ahmed s.ahmed ]]> 0 [extension] jonatas-sas/yii2-m2m-behavior Wed, 23 Apr 2025 14:03:03 +0000 https://www.yiiframework.com/extension/jonatas-sas/yii2-m2m-behavior https://www.yiiframework.com/extension/jonatas-sas/yii2-m2m-behavior jonatas-sas jonatas-sas ]]> 0 [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 Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/wiki/2575/how-to-create-and-use-validator-using-regular-expressions https://www.yiiframework.com/wiki/2575/how-to-create-and-use-validator-using-regular-expressions aayushmhu aayushmhu

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

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

See First Example 10 Digit Mobile Number Validation

<?php

namespace common\validators;

use yii\validators\Validator;

class MobileValidator extends Validator {

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

}

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

Step 2: How tO Use Validator

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

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


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


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

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

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

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

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

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

This is Other Type of Custom Validator

  1. 500 Word Validator for a String
<?php

namespace common\validators;

use yii\validators\Validator;

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

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

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

:) Thanks for Reading

]]>
0
[wiki] GridView show sum of columns in footer. Fri, 24 Apr 2026 04:17:00 +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 Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/wiki/2572/aadhar-number-validator https://www.yiiframework.com/wiki/2572/aadhar-number-validator shivam4u shivam4u

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

use yii\validators\Validator;

class TAadharNumberValidator extends Validator
{

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

    public function validateAttribute($model, $attribute)
    {
        if (preg_match($this->regExPattern, $model->$attribute)) {
            $model->addError($attribute, 'Not valid Aadhar Card Number');
        }
    }
}
]]>
0
[wiki] Interview Questions For YII2 Thu, 03 Apr 2025 17:20:29 +0000 https://www.yiiframework.com/wiki/2570/interview-questions-for-yii2 https://www.yiiframework.com/wiki/2570/interview-questions-for-yii2 aayushmhu aayushmhu

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

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

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

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

Searching the Answers of these Question Find on Dynamic Duniya

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

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

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

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

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

Thanks @Google :(

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

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

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

1. Setup a helper @gmail.com account

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

Gmail options

It allows us to send emails via Gmail SMTP server.

2. Add custom component in your configuration file

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

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

	...

	'components' => [

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

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

3. Add helper function

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4. Usage

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

5. Know the limits

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

6. Gmail is not your friend

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

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

How to implement JWT

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

The JWT Concept

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

The concept relies on two tokens:

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

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

  • RefreshToken - a long-lived, stored in database

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

Scenarios

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

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

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

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

Token expired:

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

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

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

My laptop got stolen:

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

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

Why do we trust the JWT blindly?

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

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

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

Implementation Steps

Prerequisites

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

Step-by-step setup

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

  • Disable CSRF validation on all your controllers:

Add this property: public $enableCsrfValidation = false;

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

use Yii;

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

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

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

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

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

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

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

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

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

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

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

			$this->generateRefreshToken($user);

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

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

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

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

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

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

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

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

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

Client-side examples

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


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

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

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

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

    if (status === API_STATUS_UNAUTHORIZED) {


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

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


  }
);

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

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

My articles

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

Switching languages and Language in URL

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

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

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

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

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

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

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

Search and replace

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

Yii::t()

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

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

URLs (in Notepad++)

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

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

====

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

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

PHP short tags

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

View usage

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

Virtualization - Vagrant and Docker - why and how

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

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

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

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

Running Yii project in Vagrant. (Simplified version)

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

Basic overview and Vagrant configuration:

List of all available OS images for Vagrant is here:

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

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

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

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

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


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

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

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

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

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

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

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

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

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

sudo service apache2 restart

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

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

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

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

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

To debug in PhpStorm check this video.

To connect to MySQL via PhpStorm check this comment by MilanG

Installing and using Vagrant:

First install Vagrant and VirtualBox, please.

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

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

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

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

Once virtual is running, you can call also these:

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

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

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

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

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

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

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

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

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

Now you will be able to open URLs:

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

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

Run migrations using one of following commands:

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

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

Second way is to directly modify table in DB:

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

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

Enabling xDebug in Docker, yii demo application

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

services:

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

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

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

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

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

Docker - Custom php.ini

Add into section "volumes" this line:

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

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

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

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

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

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

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

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

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

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

AdminLTE - overview & general research on the theme

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

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

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

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

There are also many other admin themes:

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

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

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

Manual integration of v2.4 - Asset File creation

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

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

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

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

This error can appear: "Headers already sent"

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

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

Insolita extension

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

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

Imperfections in the sources:

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

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

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

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

LteBox

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

Yiister

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

dmstr/yii2-adminlte-asset

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

Other enhancements

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

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

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

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

  ... grid view ...

  </div>
</div>

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

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

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

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

Creating custom Widget

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

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

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

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

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

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

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

php ./vendor/bin/codecept run

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

java -jar selenium-server-4.0.0.jar standalone

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

composer require codeception/module-webdriver --dev

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

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

Microsoft Access MDB

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

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

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

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

<?php

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

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

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

Then use this to query a table:

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

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

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

Note3: There are preinstalled applications in Windows 10 named:

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

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

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

In my case the Engine added following 64bit drivers:

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

And how about Linux ?

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

For Java there are Java JDBC, Jackcess and Ucanaccess.

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

Migration batch insert csv

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

<?php

namespace app\components;

use yii\db\Migration;

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

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

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

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

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

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

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

            $lineNumber++;
            $batch++;

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

        $this->batchInsert($table, $sqlColNames, $rows);
    }
}
]]>
0
[wiki] How to redirect all emails to one inbox on Yii2 applications Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/wiki/2566/how-to-redirect-all-emails-to-one-inbox-on-yii2-applications https://www.yiiframework.com/wiki/2566/how-to-redirect-all-emails-to-one-inbox-on-yii2-applications glpzzz glpzzz

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

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

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

Well, next there is a solution:

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

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

    return $mailer->generateMessageFileName();
}

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

  1. Attach a json file with the real recipients information so we can review it
  2. Set the recipient (TO) as the email address where we want to receive all the emails.
  3. Set the others recipients fields as null
  4. Deactivate useFileTransport
  5. Send the email
  6. Activate useFileTransport
  7. Return the defaut file name (datetime of the operation)

This way we both receive all the emails on the specified account and get them stored on @runtime/mail.

Pretty simple helper to review emails on Yii2 applications.

Originally posted on: https://glpzzz.github.io/2020/10/02/yii2-redirect-all-emails.html

]]>
0
[wiki] Api of Multiple File Uploading in Yii2 Tue, 05 Jul 2022 03:01:39 +0000 https://www.yiiframework.com/wiki/2565/api-of-multiple-file-uploading-in-yii2 https://www.yiiframework.com/wiki/2565/api-of-multiple-file-uploading-in-yii2 fezzymalek fezzymalek

After getting lot's of error and don't know how to perform multiple images api in yii2 finally I get it today

This is my question I asked on forum and it works for me https://forum.yiiframework.com/t/multiple-file-uploading-api-in-yii2/130519

Implement this code in model for Multiple File Uploading

public function rules()
    {
        return [
            [['post_id', 'media'], 'required'],
            [['post_id'], 'integer'],
            [['media'], 'file', 'maxFiles' => 10],//here is my file field
            [['created_at'], 'string', 'max' => 25],
            [['post_id'], 'exist', 'skipOnError' => true, 'targetClass' => Post::className(), 'targetAttribute' => ['post_id' => 'id']],
        ];
    }
    

You can add extension or any skiponempty method also in model.

And this is my controller action where I performed multiple file uploading code.

public function actionMultiple(){
        $model = new Media;
        $model->post_id = '2';
        if (Yii::$app->request->ispost) {
            $model->media = UploadedFile::getInstances($model, 'media');
            if ($model->media) {
                foreach ($model->media as $value) {
                    $model = new Media;
                    $model->post_id = '2';
                    $BasePath = Yii::$app->basePath.'/../images/post_images';
                    $filename = time().'-'.$value->baseName.'.'.$value->extension;
                    $model->media = $filename;
                    if ($model->save()) {
                        $value->saveAs($BasePath.$filename);
                    }
                }
                return array('status' => true, 'message' => 'Image Saved'); 
            }
        }
        return array('status' => true, 'data' => $model);
    }

If any query or question I will respond.

]]>
0
[wiki] How to email error logs to developer on Yii2 apps Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/wiki/2564/how-to-email-error-logs-to-developer-on-yii2-apps https://www.yiiframework.com/wiki/2564/how-to-email-error-logs-to-developer-on-yii2-apps glpzzz glpzzz

Logging is a very important feature of the application. It let's you know what is happening in every moment. By default, Yii2 basic and advanced application have just a \yii\log\FileTarget target configured.

To receive emails with messages from the app, setup the log component to email (or Telegram, or slack) transport instead (or besides) of file transport:

'components' => [
    // ...
    'log' => [
         'targets' => [
             [
                 'class' => 'yii\log\EmailTarget',
                 'mailer' => 'mailer',
                 'levels' => ['error', 'warning'],
                 'message' => [
                     'from' => ['log@example.com'],
                     'to' => ['developer1@example.com', 'developer2@example.com'],
                     'subject' => 'Log message',
                 ],
             ],
         ],
    ],
    // ...
],

The \yii\log\EmailTarget component is another way to log messages, in this case emailing them via the mailer component of the application as specified on the mailer attribute of EmailTarget configuration. Note that you can also specify messages properties and which levels of messages should be the sent trough this target.

If you want to receive messages via other platforms besides email, there are other components that represents log targets:

Or you can implement your own by subclassing \yii\log\Target

]]>
0
[wiki] How to add Schema.org markup to Yii2 pages Fri, 11 Sep 2020 22:09:55 +0000 https://www.yiiframework.com/wiki/2560/how-to-add-schema-org-markup-to-yii2-pages https://www.yiiframework.com/wiki/2560/how-to-add-schema-org-markup-to-yii2-pages glpzzz glpzzz

https://schema.org is a markup system that allows to embed structured data on their web pages for use by search engines and other applications. Let's see how to add Schema.org to our pages on Yii2 based websites using JSON-LD.

Basically what we need is to embed something like this in our pages:

<script type="application/ld+json">
{ 
  "@context": "http://schema.org/",
  "@type": "Movie",
  "name": "Avatar",
  "director": 
    { 
       "@type": "Person",
       "name": "James Cameron",
       "birthDate": "1954-08-16"
    },
  "genre": "Science fiction",
  "trailer": "../movies/avatar-theatrical-trailer.html" 
}
</script>

But we don't like to write scripts like this on Yii2, so let's try to do it in other, more PHP, way.

In the layout we can define some general markup for our website, so we add the following snippet at the beginning of the@app/views/layouts/main.php file:

<?= \yii\helpers\Html::script(isset($this->params['schema'])
    ? $this->params['schema']
    : \yii\helpers\Json::encode([
        '@context' => 'https://schema.org',
        '@type' => 'WebSite',
        'name' => Yii::$app->name,
        'image' => $this->image,
        'url' => Yi::$app->homeUrl,
        'descriptions' => $this->description,
        'author' => [
            '@type' => 'Organization',
            'name' => Yii::$app->name,
            'url' => 'https://www.hogarencuba.com',
            'telephone' => '+5352381595',
        ]
    ]), [
    'type' => 'application/ld+json',
]) ?>

Here we are using the Html::script($content, $options) to include the script with the necessary type option, and Json::encode($value, $options) to generate the JSON. Also we use a page parameter named schema to allow overrides on the markup from other pages. For example, in @app/views/real-estate/view.php we are using:

$this->params['schema'] = \yii\helpers\Json::encode([
    '@context' => 'https://schema.org',
    '@type' => 'Product',
    'name' => $model->title,
    'description' => $model->description,
    'image' => array_map(function ($item) {
        return $item->url;
    }, $model->images),
    'category' => $model->type->description_es,
    'productID' => $model->code,
    'identifier' => $model->code,
    'sku' => $model->code,
    'url' => \yii\helpers\Url::current(),
    'brand' => [
        '@type' => 'Organization',
        'name' => Yii::$app->name,
        'url' => 'https://www.hogarencuba.com',
        'telephone' => '+5352381595',
    ],
    'offers' => [
        '@type' => 'Offer',
        'availability' => 'InStock',
        'url' => \yii\helpers\Url::current(),
        'priceCurrency' => 'CUC',
        'price' => $model->price,
        'priceValidUntil' => date('Y-m-d', strtotime(date("Y-m-d", time()) . " + 365 day")),
        'itemCondition' => 'https://schema.org/UsedCondition',
        'sku' => $model->code,
        'identifier' => $model->code,
        'image' => $model->images[0],
        'category' => $model->type->description_es,
        'offeredBy' => [
            '@type' => 'Organization',
            'name' => Yii::$app->name,
            'url' => 'https://www.hogarencuba.com',
            'telephone' => '+5352381595',
        ]
    ]
]);

Here we redefine the schema for this page with more complex markup: a product with an offer.

This way all the pages on our website will have a schema.org markup defined: in the layout we have a default and in other pages we can redefine setting the value on $this->params['schema'].

]]>
0
[wiki] How to add Open Graph and Twitter Card tags to Yii2 website. Fri, 24 Apr 2026 04:17:00 +0000 https://www.yiiframework.com/wiki/2559/how-to-add-open-graph-and-twitter-card-tags-to-yii2-website https://www.yiiframework.com/wiki/2559/how-to-add-open-graph-and-twitter-card-tags-to-yii2-website glpzzz glpzzz

OpenGraph and Twitter Cards are two metadata sets that allow to describe web pages and make it more understandable for Facebook and Twitter respectively.

There a lot of meta tags to add to a simple webpage, so let's use TaggedView

This component overrides the yii\web\View adding more attributes to it, allowing to set the values on every view. Usually we setup page title with

$this->title = $model->title;

Now, with TaggedView we are able to set:

$this->title = $model->title;
$this->description = $model->abstract;
$this->image = $model->image;
$this->keywords = ['foo', 'bar'];

And this will generate the proper OpenGraph, Twitter Card and HTML meta description tags for this page.

Also, we can define default values for every tag in the component configuration that will be available for every page and just will be overriden if redefined as in previous example.

'components' => [
    //...
    'view' => [
        'class' => 'daxslab\taggedview\View',
        'site_name' => '',
        'author' => '',
        'locale' => '',
        'generator' => '',
        'updated_time' => '',
    ],
    //...
]

Some of this properties have default values assigned, like site_name that gets Yii::$app->name by default.

Result of usage on a website:

<title>¿Deseas comprar o vender una casa en Cuba? | HogarEnCuba, para comprar y vender casas en Cuba</title>
<meta name="author" content="Daxslab (https://www.daxslab.com)">
<meta name="description" content="Hay 580 casas...">
<meta name="generator" content="Yii2 PHP Framework (http://www.yiiframework.com)">
<meta name="keywords" content="HogarEnCuba, ...">
<meta name="robots" content="follow">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:description" content="Hay 580 casas...">
<meta name="twitter:image" content="https://www.hogarencuba.com/images/main-identifier_es.png">
<meta name="twitter:site" content="HogarEnCuba">
<meta name="twitter:title" content="¿Deseas comprar o vender una casa en Cuba?">
<meta name="twitter:type" content="website">
<meta name="twitter:url" content="https://www.hogarencuba.com/">
<meta property="og:description" content="Hay 580 casas...">
<meta property="og:image" content="https://www.hogarencuba.com/images/main-identifier_es.png">
<meta property="og:locale" content="es">
<meta property="og:site_name" content="HogarEnCuba">
<meta property="og:title" content="¿Deseas comprar o vender una casa en Cuba?">
<meta property="og:type" content="website">
<meta property="og:updated_time" content="10 sept. 2020 9:43:00">
]]>
0