Cara membuka blokir BWS Mobile dapat dilakukan melalui WhatsApp +62813-7777-021, BWS Call resmi 1500-012.
]]>Yii Log Email Target version 4.2.0 was released. In this version:
composer.json to 8.1 - 8.5yiisoft/mailer version to ^6.0 and adapt the code accordinglyHey 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.
'components'=>array(
'cache'=>array(
'class'=>'CRedisCache',
'hostname'=>'127.0.0.1',
'port'=>6379,
'database'=>1,
'keyPrefix'=>'database',
),
),
Key Prefix is Important
If you’re running multiple apps on same Redis instance, always use keyPrefix to avoid conflicts.
Cache Invalidation Yii 1.x doesn’t auto-handle this well. You need to manually clear cache when:
Persistence Redis is in-memory. Make sure:
Production Deployment Don’t keep Redis on default config:
$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
}
For those still on Yii 1.x:
Would love to hear how others are optimizing legacy Yii apps in production.
Thanks!
]]>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.
]]>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.
Response download version 1.1.0 was released. In this version:
psr/http-message version ^2.0Yii3 API project template version 1.3.0 was released.
In this version:
./yii serve.env for development without DockerYii3 web application template version 1.3.0 was released. In this version:
@PER-CS2.0 with @PER-CS./yii servesymfony/console 8yiisoft/data-response dependency.env for development without Dockermake help outputYii Proxy version 1.2 was released.
This version adds support for PHP 8.5.
]]>bestyii/yii2-tabler 是一个面向 Yii2 后台、运营平台和数据管理界面的 Tabler 组件包。
它不是单纯的 CSS 主题封装,而是把 Tabler 的视觉语言、常见后台部件和前端插件整合成可复用的 Yii2 Widget 与 Asset Bundle。
从产品目标上,它不应只是一个“Tabler 版补充包”,而应逐步成为 yiisoft/yii2-bootstrap5 的上位替代:既覆盖 Bootstrap 常用能力,也提供更丰富的后台组件和更优的视觉表达。
这个包解决的是 Yii2 后台项目里最常见的三个问题:
因此,bestyii/yii2-tabler 的目标不是替代 Yii2 本身,而是为 Yii2 提供一层偏产品化、偏后台场景的 Tabler 组件基座,并逐步补齐 yii2-bootstrap5 已有的核心能力。
基于当前仓库快照,这个包已经提供:
docs/components。phpunit、phpstan、ecs 三条质量门禁。代表性组件包括:
ActiveForm、ActiveField、Breadcrumbs、ButtonDropdown、ButtonToolbar、LinkPager、NavBar、Popover、ToggleButtonGroupButton、Alert、Badge、Card、Modal、Offcanvas、Tabs、ToastNav、Dropdown、DropdownMenu、PageHeader、Pagination、NavSegmentedStatus、StatusDot、StatusIndicator、Ribbon、Steps、Timeline、Tracking、TrendingAvatar、AvatarList、EmptyState、Payment、TagTable、AdvancedTable、Range、RatingChart、Datepicker、Dropzone、Fullcalendar、Select、Signature、Typed、VectorMap、Wysiwyg适合:
不适合:
yiisoft/yii2-bootstrap5 既有命名、示例和资源层约定,不接受迁移适配的项目这个包当前聚焦在“组件层”和“资源层”:
也就是说,它更像一个可持续维护的 UI 组件包,而不是一个整站模板。
composer require bestyii/yii2-tabler
当前要求:
8.2 - 8.4~2.0.32官方支持策略和兼容承诺见 docs/support-policy.md。
use bestyii\tabler\Button;
echo Button::primary(
'Open Preview',
icon: 'eye',
url: ['/preview'],
);
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');
当前已支持语法糖的组件和预设范围如下:
Badge:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkButton:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkAlert:primary、secondary、success、info、warning、dangerProgress:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkTag:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkStatus:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkStatusDot:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkStatusIndicator:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkRibbon:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkSpinner:border、growButtonDropdown:primary、secondary、success、info、warning、danger、blue、azure、indigo、purple、pink、red、orange、yellow、lime、green、teal、cyan、darkOffcanvas:left、right、top、bottomPopover:auto、top、bottom、left、rightuse 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'],
],
]);
为了让组件在复杂后台页面里可长期维护,这个包现在遵循一套统一内容契约:
title、label、subtitle 这类字段,默认按文本处理。contentHtml、headerHtml、footerHtml 这类命名,而不是模糊地把 HTML 放进普通字符串属性。format 明确语义。Table 与 AdvancedTable 的列配置优先使用 format => Table::FORMAT_TEXT|FORMAT_HTML,旧的 encode 仅作为兼容桥保留。begin()/end() 输出视为 HTML 插槽,因为这类用法本身就是为了拼接 widget 或标记。详细约定见 docs/component-contracts.md。
bestyii/yii2-tabler 采用的是“组件 + 资源包”双层模型:
TablerAsset 负责注册 Tabler 核心样式与脚本ApexChartsAsset、DropzoneAsset、FullcalendarAsset扩展资源现在按最小边界拆分:
Flag 会自动注册 TablerFlagsAssetPayment 会自动注册 TablerPaymentsAssetTablerSocialsAssetTablerMarketingAssetTablerThemeAsset例如:
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 应该满足两层目标:
yii2-bootstrap5 的核心用户态组件能力。当前这一层已经覆盖到 ActiveForm、ActiveField、NavBar、ButtonDropdown、ButtonToolbar、LinkPager、Popover、ToggleButtonGroup 等高频部件。Card、PageHeader、AdvancedTable、Chart、Dropzone、VectorMap、Wysiwyg 等更偏后台产品场景的组件。换句话说,yii2-tabler 的定位不是“和 yii2-bootstrap5 做功能切分”,而是“以 Tabler 风格重做并扩展 Yii2 的 Bootstrap 组件层”。
8.2 - 8.4,面向现代 Yii2 团队,而不是极旧环境。yii2-bootstrap5 高度同构的组件优先追求稳定和一致性,不做为了“更优雅”而引入的新概念拆分。Card、AdvancedTable、Popover 这类产品层组件里。详细规则见 docs/parity-policy.md 与 docs/support-policy.md。
当前包级交付标准包括:
runtime/coverage 报告工件本地常用命令:
composer tests
composer static
composer cs
XDEBUG_MODE=coverage composer coverage
说明:
composer tests 默认带 --no-coverage,用于日常快速回归。composer coverage 需要 pcov 或 Xdebug 覆盖率驱动;如果使用 Xdebug,请显式带上 XDEBUG_MODE=coverage。runtime/coverage/,其中包含 clover.xml、cobertura.xml 和 HTML 报告。最近一次补强还加入了 Asset Bundle 一致性测试,用于直接检查:
sourcePath 是否有效这类测试的目的,是避免“组件渲染测试通过,但前端资源实际上不可发布”的隐性回归。
docs/index.mddocs/support-policy.mddocs/parity-policy.mddocs/roadmap-1.0.mddocs/component-contracts.mddocs/componentsdocs/compare-with-yii2-bootstrap5.mdCONTRIBUTING.mdSECURITY.md从产品方向上,bestyii/yii2-tabler 应该是 yiisoft/yii2-bootstrap5 的超集,而不是平行替代。
当前如果你的项目目标是“用 Tabler 风格统一后台界面,并把 Bootstrap 常用基础能力也收进同一个组件层”,bestyii/yii2-tabler 已经可以作为主包承接。更细的覆盖矩阵与选型参考,见 docs/compare-with-yii2-bootstrap5.md。
Yii DataView version 1.1.0 was released. In this version:
yiisoft/html version to 3.13 and add support for ^4.0Yii HTML version 4 was released. In this version:
int, float and null as tag contentLogicException in Tag::id() when id is empty stringCheckboxItem and RadioItem properties required$options parameter to $attributes in Html::addCssClass(), Html::removeCssClass(), Html::addCssStyle() and Html::removeCssStyle() methodsSee upgrading instructions with notes about upgrading package in your application to this major version.
]]>Yii HTML version 3.13.0 was released. In this version:
$attributes parameter to Html::li() methodTagName::tag() in favor of new TagName()RadioList::addRadioWrapClass() method for cleaner class additionYii HTTP Middleware version 1.2.0 was released. In this version:
RedirectMiddlewarecomposer.json to 8.1 - 8.5Yii3 API project template 1.2.0
Yii API Application Template version 1.2.0 was released. In this version:
prod-deploy error handling so exact error is printed in case of rollbackMakefile default command help logicPresenterInterface and implementations for preparing data onlyC.UTF-8 in Dockerfilemake help outputBootstrap 5 version 1.1.0 was released.
In this version:
Nav::activateParents() that makes parent dropdown active when one of its child items is activeDropdown::togglerVariant() to be null, avoiding it setting a variant classvisible to Dropdown and DropdownItemnavId() method to NavBar widgetDropdown widget, and addDropdownClass() methodbtn-secondary class instead of btn-link classYii Mailer version 6.1.0 was released. In this version:
Minor version of Yii View Renderer were tagged.
WebViewRenderer instead of ViewRenderer which is marked as deprecated.Yii Data Response version 2.2.0 was released. In this version:
DataStreamFormatterInterface and implementations: HtmlFormatter, JsonFormatter, PlainTextFormatter, XmlFormatterDataResponseFactoryInterface and implementations: DataResponseFactory, FormattedResponseFactory, HtmlResponseFactory, JsonResponseFactory, PlainTextResponseFactory, XmlResponseFactoryXmlDataResponseMiddleware, HtmlDataResponseMiddleware, JsonDataResponseMiddleware, PlainTextDataResponseMiddleware and DataResponseMiddlewareContentNegotiatorResponseFactory and ContentNegotiatorDataResponseMiddlewareDataResponse, DataResponseFactory, DataResponseFactoryInterface, DataResponseFormatterInterface, ResponseContentTrait, HtmlDataResponseFormatter, JsonDataResponseFormatter, PlainTextDataResponseFormatter, XmlDataResponseFormatter, ContentNegotiator, FormatDataResponse, FormatDataResponseAsHtml, FormatDataResponseAsJson, FormatDataResponseAsPlainText, FormatDataResponseAsXmlYii3 support was added to Tideways profiler additionally to existing verison 1 and 2 support.
Happy profiling!
]]>Yii3 web application template version 1.2.0 was released. In this version:
prod-deploy error handling so exact error is printed in case of rollback.roave/security-advisories since Composer 2.9 handles security advisories natively.Makefile default command help logic.C.UTF-8 in Dockerfile.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:
Security is not a one-time action; it is a process, and we are committed to it.
Thanks, GitHub!
]]>为 Yii2 框架提供 PhpStorm 智能代码补全支持,灵感来源于 barryvdh/laravel-ide-helper
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 组件的类型提示。
生成的内容:
@property 注解(如 $db, $cache, $session)@method 注解(如 getDb(), getCache())php yii ide-helper/models
这将扫描 ActiveRecord 模型并生成属性和关系类型提示。
生成的内容:
@property 注解(包含类型和默认值)@property-read 注解@method 注解(where*(), orWhere*(), andWhere*())php yii ide-helper/meta
这将生成 .phpstorm.meta.php 文件,为依赖注入容器提供类型推断。
生成的内容:
Yii::$container->get() 类型推断)Yii::$app->get() 类型提示)find(), hasMany(), hasOne() 返回类型)Yii::createObject() 类型推断)php yii help
.phpstorm.meta.php 文件为 PhpStorm 提供高级类型推断能力,以下是它支持的功能:
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(); // 有完整的类型提示
`
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(); // 有完整的类型提示
`
override(\yii\db\ActiveRecord::find(), type(\yii\db\ActiveQuery));
这个配置告诉 PhpStorm,ActiveRecord::find() 返回 ActiveQuery 类型。
使用示例:
`php
$query = User::find(); // PhpStorm 知道返回的是 ActiveQuery
$query->where(['status' => 1]); // 有完整的类型提示
`
你可以在代码中添加自定义绑定:
$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',
],
],
],
`
_ide_helper.php 文件路径.phpstorm.meta.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;
}
}
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 {
}
}
<?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 属性提供类型提示运行测试套件:
composer test
运行代码规范检查:
composer lint
欢迎提交 Pull Request!
MIT License
本项目灵感来源于 barryvdh/laravel-ide-helper
]]>Supporting tools for testing version 3.2.0 was released.
In this version added StringStream, a test-specific implementation of PSR-7 stream.
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.
Renders a ChartJs plugin widget.
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.
The following types are supported:
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);
});
}
});
}
}]')
])
`
ChartJs has lots of configuration options. For further information, please check the ChartJs plugin website.
Please see CONTRIBUTING for details.
The BSD License (BSD). Please see License File for more information.
]]>
Custom Software | Web & Mobile Software Development
www.2amigos.us
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.
previous | next).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.
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.
The widget persists the configuration in JSON format with the following main structure (examples):
{"type": "no_expiration"}
{ "type": "interval", "value": 10, "unit": "days" }
{ "type": "monthly", "day": 31, "adjust": "previous" }
{ "type": "yearly", "day": 29, "month": 2, "adjust": "previous" }
{ "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).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';
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.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',
],
]);
adjust policy.adjust value is persisted in JSON and is considered by RecurringDateEngine::calculateExpiration.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.
Unit tests for PHP are included for the calculation logic (tests/RecurringDateEngineTest.php) and should be executed with:
vendor/bin/phpunit tests/RecurringDateEngineTest.php
recurrence_config), and use RecurringDateEngine::calculateExpiration to obtain the next expiration date when needed.adjust policy for your domain (by default the extension uses previous — clamp to the last valid day). This avoids surprises when calculating next dates.Yii::$app->language) to ensure the UI displays the desired translations.Pull requests and issues are welcome. For major changes, first open an issue describing the proposed change.
BSD-3-Clause — see LICENSE file.
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.
#### Sign In
#### Sign Up
#### Dashboard
#### Users Management
#### Settings
#### Forgot Password
#### Sign In
#### Sign Up
#### Dashboard
#### Users Management
#### Settings
#### Forgot Password
Before you begin, ensure you have the following installed on your system:
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.
Install all PHP dependencies using Composer:
composer install
This will install all required PHP packages including Yii2 framework and Inertia.js adapter.
Install all frontend dependencies:
npm install
This will install React, Inertia.js, Shadcn UI components, Tailwind CSS, and all other frontend dependencies.
CREATE DATABASE yii2basic CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
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).
Run the migrations to create all necessary database tables:
php yii migrate
This will create the following tables:
users - User accounts with soft deletespassword_reset_tokens - Password reset functionalityCreate an admin user for testing:
php yii seed/admin
This creates an admin user with:
admin@example.comadmin123The cookie validation key should be automatically generated during composer install. If it wasn't, you can generate it manually:
config/web.phpcookieValidationKey in the request component'request' => [
'cookieValidationKey' => 'your-random-32-character-string-here',
],
You can generate a random string using:
php -r "echo bin2hex(random_bytes(16));"
You need to run two servers simultaneously:
PHP Development Server (Backend):
`bash
php yii serve
`
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"
Open your browser and navigate to:
http://localhost:8080
You should see the home page. You can now:
/auth/register/auth/login (or use admin credentials if you seeded)/dashboard (requires login)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.
The application uses Yii2's environment configuration. You can set the environment by:
config/params.php and modifying as neededYII_ENV constant in web/index.php:YII_ENV_DEV - Development modeYII_ENV_PROD - Production modeconfig/web.php - Web application configurationconfig/console.php - Console application configurationconfig/db.php - Database configurationconfig/params.php - Application parametersNOTES:
runtime/ and web/assets/ directories are writable by the web serverconfig/web.phpweb/ directory as the document rootassets/ 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)
Tests are located in tests directory. They are developed with Codeception PHP Testing Framework.
By default, there are 3 test suites:
unitfunctionalacceptanceTests 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.
To execute acceptance tests do the following:
Rename tests/acceptance.suite.yml.example to tests/acceptance.suite.yml to enable suite configuration
Replace codeception/base package in composer.json with codeception/codeception to install full-featured
version of Codeception
Update dependencies with Composer
composer update
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
(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.
Start web server:
tests/bin/yii serve
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
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.
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?
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.
Install via Composer:
composer require crenspire/yii2-inertia
Add to your config/web.php:
'view' => [
'renderers' => [
'inertia' => \Crenspire\Yii2Inertia\ViewRenderer::class,
],
],
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>
use Crenspire\Yii2Inertia\Inertia;
class HomeController extends \yii\web\Controller
{
public function actionIndex()
{
return Inertia::render('Home', [
'title' => 'Welcome',
'users' => User::find()->all(),
]);
}
}
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} />);
},
});
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.
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).
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'));
});
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'] });
};
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]);
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>
);
}
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,
]);
}
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>
);
}
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,
],
]);
}
Use eager loading to prevent N+1 queries:
$users = User::find()
->with('posts', 'comments')
->all();
Only send necessary data:
// Instead of full models
return Inertia::render('Users/Index', [
'users' => User::find()
->select(['id', 'name', 'email'])
->asArray()
->all(),
]);
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);
}
}
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,
]);
}
}
If you're experiencing frequent full page reloads:
manifest.json exists and is readableInertia::version(function () {
$manifest = Yii::getAlias('@webroot/dist/manifest.json');
return file_exists($manifest) ? (string)filemtime($manifest) : '1';
});
Always use Inertia::location() instead of Yii::$app->response->redirect() for Inertia requests.
usePage() hook correctly:import { usePage } from '@inertiajs/inertia-react';
const { props } = usePage();
const { title, user } = props;
Share CSRF token globally:
Inertia::share('csrfToken', function () {
return Yii::$app->request->csrfToken;
});
Use in forms:
<input type="hidden" name="_token" value={props.csrfToken} />
examples/basic directory in the repositoryInertia.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.
]]>High-resolution cron-like job scheduler for Yii2 supporting:
{pid, host, ts}composer require ldkafka/yii2-scheduler
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',
],
],
];
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
],
],
];
php yii scheduler/run
php yii scheduler/daemon
{pid, host, ts} instead of bare PID. No migration needed; old locks will expire naturally (1-hour TTL).staleLockTtl and maxJobsPerTick from your scheduler config (not implemented).day key is now normalized to mday. Update job configs using day to use mday instead for clarity.EVERY_MINUTE, EVERY_HOUR, EVERY_DAY, EVERY_WEEK, EVERY_MONTHgetdate() keys: minutes, hours, mday (day of month), wday (weekday), mon, year* - 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)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:
{pid: int, host: string, ts: int} and are acquired atomically with cache->add().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.All logs use the scheduler category with production-appropriate levels:
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
],
],
],
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' => [ /* ... */ ],
],
jobBeforeRun): Job is about to executejobAfterRun): Job completed successfullyjobError): Job threw exceptionjobBlocked): Job prevented from running (e.g., single-instance lock)jobTimeout): Reserved for future useAll events use SchedulerJobEvent class with typed properties for monitoring integration.
Scheduler::finalizeRuntimeJob() to remove the job entry from the persistent run cache.pcntl for graceful signal handling (SIGINT/SIGTERM). On platforms without pcntl, the daemon stops when too many ticks are missed (configurable).BSD-3-Clause
]]>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.
final class, consistent helper methods| 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 |
composer require ldkafka/yii2-google-gemini
'components' => [
'gemini' => [
'class' => 'ldkafka\gemini\Gemini',
'apiKey' => 'YOUR_GEMINI_API_KEY',
'generationConfig' => [
'temperature' => 0.7,
'topP' => 0.95,
'maxOutputTokens' => 2048,
],
],
],
$gemini = Yii::$app->gemini;
$resp = $gemini->generateContent('gemini-2.5-flash', 'Explain quantum computing');
if ($resp['ok']) {
echo $gemini->extractText($resp['data']);
}
$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";
}
$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();
}
});
$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);
$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."
$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');
$gemini->systemInstruction = [
'parts' => [['text' => 'You are a helpful coding assistant who explains concepts simply.']]
];
$resp = $gemini->generateContent('gemini-2.5-flash', 'Explain recursion');
// 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');
// 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);
$tokens = $gemini->countTokens('gemini-2.5-flash', 'Your prompt text here');
echo "This prompt will use approximately $tokens tokens\n";
// 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";
| 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().
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
| 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 |
'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
]
| 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.
All methods return:
[
'ok' => true|false, // Success status
'status' => 200, // HTTP status code
'data' => [...], // Response data
'error' => null|string // Error message if failed
]
// 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]
$text = $gemini->extractText($resp['data']);
$reason = $gemini->getFinishReason($resp['data']);
$usage = $gemini->getUsageMetadata($resp['data']);
$gemini->cacheType = 'none';
$resp = $gemini->generateContent('gemini-2.5-flash', 'Hello');
// Each request is independent
$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
$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.
$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
}
'gemini' => [
'class' => 'ldkafka\gemini\Gemini',
'apiKey' => 'YOUR_KEY',
'httpConfig' => [
'timeout' => 60,
'transport' => 'yii\httpclient\CurlTransport',
],
],
$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)]],
]
]];
$gemini->safetySettings = [
['category' => 'HARM_CATEGORY_HARASSMENT', 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'],
['category' => 'HARM_CATEGORY_HATE_SPEECH', 'threshold' => 'BLOCK_ONLY_HIGH'],
];
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.
HARM_CATEGORY_HARASSMENT, HARM_CATEGORY_HATE_SPEECH, HARM_CATEGORY_SEXUALLY_EXPLICIT, HARM_CATEGORY_DANGEROUS_CONTENT, HARM_CATEGORY_VIOLENCE.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:
safetySettings is empty, no custom filters are applied (provider defaults may still apply).The package includes comprehensive test actions:
Ensure your API key is set in the component configuration or params.
Server caching requires:
Use client-side caching for shorter conversations.
Ensure your HTTP client supports Server-Sent Events (SSE). The default Yii2 HTTP client may need custom transport configuration.
429 & transient 5xx responses.countTokens().Yii::info([...], 'gemini') for observability.BSD-3-Clause (matches class header).
Open issues or PRs at: https://github.com/ldkafka/yii2-google-gemini
When reporting an issue, include:
Enjoy building with Gemini! Suggestions & improvements welcome.
]]>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\widgetsnamespace. Make sure to update your imports if you're upgrading from an older version.
wbraganca/yii2-dynamicform for creating dynamic forms.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.
If you don't want to use Composer, you can still install it manually:
src/ directorycommon/widgets/searchable_dep_drop/)src/assets/ directoryFor 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;
}
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');
The widget comes with built-in CSS that provides:
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;
}
| 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 |
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 |
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_wrapperwith your actualwidgetContainerclass fromDynamicFormWidget::begin().
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:
paramNames option is crucial for dependent dropdowns to work properlySearchableDepDrop.php to src/widgets/SearchableDepDrop.phpSearchableDepDropAsset.php to src/widgets/SearchableDepDropAsset.phpsrc/
├── widgets/
│ ├── SearchableDepDrop.php
│ └── SearchableDepDropAsset.php
└── assets/
├── css/
│ └── searchable-dep-drop.css
└── js/
└── searchable-dep-drop.js
If you're upgrading from a previous version:
composer update rft/yii2-searchable-depdropuse rft\searchabledepdrop\widgets\SearchableDepDrop;This project is licensed under the MIT License.
]]>A package of helper classes for working with web components in Yii2.
Run
php composer.phar require mspirkov/yii2-web
or add
"mspirkov/yii2-web": "^0.4"
to the require section of your composer.json file.
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.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),
),
],
],
...
];
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',
]);
}
}
This package contains 4 helpers:
MSpirkov\Yii2\Web\Html - a helper that extends yii\web\HtmlMSpirkov\Yii2\Web\Bootstrap3\Html - a helper that extends yii\bootstrap\HtmlMSpirkov\Yii2\Web\Bootstrap4\Html - a helper that extends yii\bootstrap4\HtmlMSpirkov\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.
A trait that extends the basic functionality of the \yii\helpers\Html helper.
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;
}
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'); ?>
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.
First, you need to replace the request component in the configuration:
use MSpirkov\Yii2\Web\Request;
return [
...
'components' => [
'request' => [
'class' => Request::class,
...
],
...
],
];
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 {}
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);
}
}
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'));
}
}
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
$requiredparameter.
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.use MSpirkov\Yii2\Web\RequestTrait;
class Request extends \yii\web\Request
{
use RequestTrait;
}
]]>A package of helper classes for working with databases in Yii2.
Run
php composer.phar require mspirkov/yii2-db
or add
"mspirkov/yii2-db": "^0.3"
to the require section of your composer.json file.
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.
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,
];
}
}
Behavior for ActiveRecord models that automatically fills the specified attributes with the current date and time.
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()'),
],
];
}
}
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.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),
],
],
...
];
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;
}
}
]]>(draft - all will be retested later)
In Yii3 it is not as easy to start as it was with Yii2. You have to install and configure basic things on your own. Yii3 uses the modern approach based on independent packages and dependency injection, but it makes it harder for newcomers. I am here to show all how I did it.
All my code is available in my GitHub repositories:
yii3web offers more)I will be using it as a boiler-plate for my future projects so it should be always up-to-date and working.
Instead of installing local WAMP- or XAMPP-server I will be using Docker. Do not forget about a modern IDE like PhpStorm, which comes bundled with all you will ever need.
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.
Check this YouTube video for explanation
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".
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']));
__invoke() method it is a callable- or invokable-object.__invoke() implements the main (or the only) functionality of the object.
$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".$instance(), you are backed with a large object which can do much more. It can also use traits, state or OOP benefits.__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.config/common/routes.php you can use both:->action(Web\HomePage\Action::class) // __invoke() needed->action([Web\HomePage\Action::class, 'run'])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.
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 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:
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:
... then the web will be available on URL
If you want to modify the data that was returned by the endpoint, just open the action-class src/Api/IndexAction.php and add one more element to the returned array.
You may be missing 'docker compose stop' or 'make stop', because 'make down' removes your containers and drops your DB. In that case you can add it to the Makefile in the root (see below). If you then type 'make help' you will see the new command.
ifeq ($(PRIMARY_GOAL),stop)
stop: ## Stop the dev environment
$(DOCKER_COMPOSE_DEV) stop
endif
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
Then run following commands:
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)
Now when your project contains MariaDB, you may wanna use it in the code ...
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:
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.
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);
When this is done, call "composer du" or "make composer du" and then try "make yii list". You should see the migration commands.
Run the command to create a migration:
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');
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:
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
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 = 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
IndexActionwith theQueryobject. Just useContainerInterface $containerin the constructor instead ofConnectionInterface $db. Then you can call$db = $this->container->get(ConnectionInterface::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));
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:
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".
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.
]]>Yii 2 extension for render views using Fenom template engine.
composer require ensostudio/yii2-fenom
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
]]>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
]]>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.
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.
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.
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.
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';
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.
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),
],
];
}
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:
admin-post.php WordPress endpoint.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.
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.
Originally posted on https://glpzzz.dev/2024/03/03/integrating-yii3-packages-into-wordpress.html
]]>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',
],
]);
]]>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.

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 intlextension to provide most of its I18N features, such as the date and number formatting of theyii\i18n\Formatterclass and the message formatting usingyii\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 installintlwhen I18N is needed.
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,
*/
];
/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'],
],
],
],
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>',
],
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',
...
This takes a number of steps.
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:
fror French Canada:fr-CAPortuguese:
ptor 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' => '中国的',
],
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'],
],
],
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 */
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',
],
/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.
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.
The following are optional but may help both you and/or the user.
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]),
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:
Yii2 Internationalization Tutorial
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."
]]>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)
Bank Account Number Validator
`php
preg_match("/^[0-9]{9,18}+$/", $model->$attribute)
`
Bank IFSC Code Validator
`php
preg_match("/^[A-Z]{4}0[A-Z0-9]{6}$/", $model->$attribute)
`
Pan Card Number Validator
`php
preg_match('/^([a-zA-Z]){5}([0-9]){4}([a-zA-Z]){1}?$/', $model->$attribute)
`
Pin Code Validator
`php
preg_match('/^[0-9]{6}+$/', $model->$attribute)
`
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
<?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
]]>GridView show sum of columns in footer
`PHP
use yii\grid\DataColumn;
/**
@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
]]>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;
}
}
]]>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');
}
}
}
]]>Hey Everyone, In this post I Just shared my Experience what most of interviewer ask in YII2 Interview.
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
]]>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...
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 :(
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.
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:

It allows us to send emails via Gmail SMTP server.
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',
],
],
],
];
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.
/**
* 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;
}
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.
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...
]]>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:
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.
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.
/auth/login endpoint: ¶In our actionLogin() method two things happens, if the credentials are correct:
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).
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.
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.
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.
https enabled site is required for the HttpOnly cookie to work cross-siteCREATE 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';
composer require sizeg/yii2-jwtAuthController.php. You can name it what you want.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;
/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.
],
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();
}
}
/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,
],
...
],
];
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;
}
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;
}
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();
}
}
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.');
}
}
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();
}
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);
}
user_refresh_tokens that belongs to the given user
and allow him to delete the ones he chooses.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.
]]>Articles are separated into more files as there is the max lenght for each file on wiki.
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.
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)
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.
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:
Once virtual is running, you can call also these:
In the Linux shell you can call any command you want.
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.
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.
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:
Run migrations using one of following commands:
Now go to Frontend and click "signup" in the right upper corner
Second way is to directly modify table in DB:
Now you have your account and you can log in to Backend
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.
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.
Navigate in command line to the folder of your docker-project and run command:
The last column of the list is NAMES. Pick one and copy its name. Then run command:
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 is one of available admin themes. It currently has 2 versions:
* 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
Also delete all SCRIPT and LINK tags. We will add them using the AssetBundle later.
We only need to create the Asset file to link all SCRIPTs and LINKs:
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',
];
}
This error can appear: "Headers already sent"
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
vendor\insolita\yii2-adminlte-widgets\CollapseBox.php
LteBox
<div class="overlay"><i class="fa fa-refresh fa-spin"></i></div>
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> 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.
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();
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".
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:
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:
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.
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);
}
}
]]>\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.
useFileTransportuseFileTransportThis 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
]]>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.
]]>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
]]>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'].
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">
]]>