Esta es una extensión primaria para [Yii framework 2.0](https://www.yiiframework.com). Encapsula componentes de [Material Design](https://m3.material.io/) en términos de Widgets Yii. **NOTA**: Material Web 3 no tiene los componentes `Card`, `Snackbar`, `TopAppBar` ni `NavigationRail`, así que se han creado desde 0 para intentar seguir los lineamientos del diseño de Material 3. Instalación ------------ La forma preferida de instalar esta extensión es a través de [composer](http://getcomposer.org/download/). Luego ejecute ``` php composer.phar require --prefer-dist neoacevedo/yii2-material3 ``` o agregue ```js "neoacevedo/yii2-material3": "*" ``` a la sección require de su archivo `composer.json`. Uso ---- ### Card e Icon Buttons ```php <?php ... $css = <<registerCss($css); ?> <?php \neoacevedo\yii2\material\widgets\Card::begin([ 'options' => [ 'class' => 'card', 'type' => Card::MD_CARD_TYPE_FILLED ], 'actions' => [ 'icons' => [ Html::iconButton(['icon' => 'dictionary']), Html::iconButton(['icon' => 'bookmark']) ] ] ]); ?>
Version 12.1 of Yii View package has been released. Here are the list of improvements and fixes included in the new version:
composer.json
to 8.1 - 8.4
;../
in the name of the view to refer to parent directory of the directory containing the view currently being rendered;Version 1.6 of Yii Hydrator package has been released. Here are the list of improvements included in the new version:
ObjectMap
class;composer.json
to 8.1 - 8.4
;HydratorInterface::create()
method.Version 5.1 of Yii Assets package was released. There a few changes:
composer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
;array_filter
call in AssetUtil::extractFilePathsForExport()
method;Version 3.0 of Yii Session package was released. There a few changes and fixes:
sid_length
and sid_bits_per_character
options;SessionException
as final;composer.json
to 8.0 - 8.4
;Minor version of Yii Definitions package was released.
composer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
Version 1.1 of Yii Security package was released. There are some improvements and changes:
composer.json
to 8.1 - 8.4
SensitiveParameter
attribute to mark sensitive parametersMajor versions of Yii Router and Yii Routere FastRoute Adapter adapter package were released.
Yii Router
UrlGeneratorInterface
contract: on URL generation all unused arguments must be moved to query parameters, if query parameter with such name doesn't existyiisoft/yii-debug
$hash
parameter to UrlGeneratorInterface
methods: generate()
, generateAbsolute()
and generateFromCurrent()
RouteCollectorInterface
methods addRoute()
and addGroup()
to single addRoute()
Route
, Group
and MatchingResult
dispatcher-independentcomposer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
UrlGeneratorInterface
FastRoute Adapter
composer.json
to 8.1 - 8.4
yiisoft/router
version ^4.0
UrlGenerator
host and scheme properties to package config paramsVersion 2.3 of Yii User package was released. There are some improvements and changes:
ApiAuth
authentication methodUserAuth
to WebAuth
, mark UserAuth
as deprecatedcomposer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
LogginMiddleware
Minor version of Yii HTTP Runner were tagged. In this version made several changes.
$temporaryErrorHandler
parameter to HttpApplicationRunner
constructor. Mark parameter $logger
and method withTemporaryErrorHandler()
as deprecated.composer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
.Version 2.2 of Yii Validator package has been released. Here is the list of changes included in the new version:
stopOnError
parameter to Each
rulecomposer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
yiisoft/strings
version to ^2.6
PostValidationHookInterface
when Nested
rule with specified rules is usedFilledAtLeast
ruleEmail
rule in edge case when enabled IDN, check DNS and used custom patternURL
handler for PHP 8.4 when value is empty stringYii2 extensions with pending releases were tagged:
Follow the links above to check individual changelogs. Big thanks to Yii community for pull requests.
]]>We are pleased to announce the release of Yii Framework version 2.0.52.
Please refer to the instructions at https://www.yiiframework.com/download/ to install or upgrade to this version.
This release fixes PHP 8.4 compatibility, resolves a number of bugs, and adds some minor enhancements:
yii\helpers\ArrayHelper::flatten()
.BackedEnum
support in AttributeTypecastBehavior
.Thanks to all Yii community members who contribute to the framework, translators who keep documentation translations up to date and community members who answer questions at forums.
There are many active Yii communities so if you need help or want to share your experience, feel free to join them.
A complete list of changes can be found in the CHANGELOG.
]]>Minor version of Yii Strings package was released.
composer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
StringHelper::parsePath()
for empty string returns path ['']
instead of []
before StringHelper
methods: trim()
, ltrim()
and rtrim()
Yii Error Handler package was updated with the following enhancements:
HtmlRenderer
settings in constructor. Mark $settings
parameter as deprecatedcomposer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
ErrorHandler
ErrorCatcher
middleware into separate ThrowableResponseFactory
classMinor version of Yii Config package was released. There are some new features and improvements:
yii-config-info
composer commandcomposer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
Options
and ProcessHelper
Minor version of Yii Arrays package is released.
composer.json
to ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
ArrayHelper::getObjectVars()
, ArrayableInterface
, ArrayableTrait
and ArrayAccessTrait
ArrayHelper::getValue()
returns default value on empty array keyArrayHelper::keyExists()
returns false on empty array keyVersion 2.2 of Yii Swagger package has been released. Here is the list of changes included in the new version:
yiisoft/assets
version of ^5.0
;Version 2.3 of Yii Console package was tagged.
--open
option for serve
commandserve
commandMinor version of Yii Strings package was released.
StringHelper::matchAnyRegex()
method as a facade for CombinedRegexp
.StringHelper::base64UrlEncode()
method.First stable version of Yii File Router was released. The package provides a convention-based router middleware that chooses controller based on its namespace and class name similar to how it was in worked in Yii2. It could be used either standalone or as a fallback for explicit router.
src
Controller
User
Profile
IndexController.php
BlogController.php
UserController.php
IndexController.php
Here's how it works:
GET /
→ IndexController::index()
GET /user
→ UserController::index()
POST /user
→ UserController::create()
GET /user/blog/view
→ User/BlogController::view()
GET /user/profile
→ User/Profile/IndexController::index()
As usual, the package has 100% unit test coverage, 100% MSI score and a good type coverage.
]]>Bootstrap 4 and 5 are expecting `
html`
like this to decorate validation error:
<input type="text" id="eventform-datetime" class="form-control is-invalid" name="EventForm[datetime]" aria-required="true">
<div class="invalid-feedback">Error message</div>
Element with `
div.invalid-feedbackis supposed to be on the same level with your
input.is-invalid`
.
But sometimes when we are using any widgets or custom template we get html like this:
<div class="some-plugin-wrapper">
<input type="text" id="eventform-datetime" class="form-control is-invalid" name="EventForm[datetime]" aria-required="true">
</div>
<div class="invalid-feedback">Error message</div>
, so our error message is not shown.
Of cource you can make `
div.invalid-feedback`
visible by css for this page.
But if that does not suit you, this library propose another solution.
We are adding special `
to a field template right before
{error}part. And we **synchronize** this
with the **input field** so it gets
.is-invalid`
class when input does
The preferred way to install this library is through composer.
Either run
composer require --prefer-dist mgrechanik/yii2-activefield-additional-error
or add
"mgrechanik/yii2-activefield-additional-error " : "~1.0.0"
to the require section of your composer.json
.
in your `
viewfile, say it is
_form.php`
use mgrechanik\additionalerror\AdditionalErrorBehavior;
<div class="event-form-form">
<?php $form = ActiveForm::begin([
'id' => 'event-create-form',
// Adding behavior
'as adderror' => [
'class' => AdditionalErrorBehavior::class,
]
]); ?>
<?= $form->field($model, 'datetime', [
// Adding this hidden span before error block
'template' => "{label}\n{input}\n{hint}\n" . $form->getAdditionalErrorSpan($model, 'datetime') . "\n{error}"
])->hint('Some hint')
->widget(/* Some complicated widget creates a wrapper for the {input} part... */)
It will work for both server and client side.
]]>When you are using Yii2 default GridView you might meet a problem that validation errors for filter model are not displayed properly, like this:
The preferred way to install this library is through composer.
Either run
composer require --prefer-dist mgrechanik/gridviewfilterfix
or add
"mgrechanik/gridviewfilterfix" : "~1.0.0"
to the require section of your composer.json
.
Add the following lines of code to your main configuration file:
1) For Bootstrap 4
`
php
'container' => [
'definitions' => [
\yii\grid\GridView::class => [
'dataColumnClass' => \mgrechanik\gridviewfilterfix\Bs4DataColumn::class
]
]
],
2) For Bootstrap 5
```php
'container' => [
'definitions' => [
\yii\grid\GridView::class => [
'dataColumnClass' => \mgrechanik\gridviewfilterfix\Bs5DataColumn::class
]
]
],
Solution:
'container' => [
'definitions' => [
\yii\widgets\LinkPager::class => \yii\bootstrap5\LinkPager::class,
],
],
There is a library to solve this problem
]]>Yii2-donate is a module for the Yii 2.0 PHP Framework to handle donations. It makes use of the payment service provider Mollie, which is mainly active in Western European countries.
Yii2-donate sports a widget, which can be placed on any page (or even all pages).
If a visitor selects an amount and presses the 'Donate'-button, she is
transfered to a Mollie payment page. If she successfully completes the
payment, she is redirected to the site's donate/thanks
page, where she
is rewarded with a joyful shower of confetti. If the visitor did supply
an email address, she receives a 'Thank you' mail. The 'thanks' page
also sports a button to resume her visit to the site.
If the visitor cancels the payment, she is redirected to the site's
donate/cancel
page, from where she can resume her surfing.
At any time, the site's administrator can get an overview of granted
donations on the donate
page.
You'll need a Mollie account. It's free, but depending on your country, you may need a valid registration as a (small) business. You'll get two API keys, one for testing purposes and one for the real work. One of the API keys is used tot initialize the module.
It is strongly advised that the app uses Pretty URLs.
Because Yii2-donate may send emails, the mailer
component of the application has to be up and running.
Be sure that the 'adminEmail'
parameter of the application has a sensible value. If you prefer, you may set
the 'supportEmail'
parameter as well; if set, Yii2-donate will use this.
Install Yii2-donate in the usual way with Composer.
Add the following to the require section of your composer.json
file:
"sjaakp/yii2-donate": "*"
or run:
composer require sjaakp/yii2-donate
You can manually install yii2-comus by downloading the source in ZIP-format.
Yii2-donate is a module
in the Yii2 framework. It has to be configured
in the main configuration file, usually called web.php
or main.php
in the config
directory. Add the following to the configuration array:
<?php
// ...
'modules' => [
'donate' => [
'class' => sjaakp\donate\Module::class,
// several options
],
],
// ...
The module has to be bootstrapped. Do this by adding the following to the application configuration array:
<php
// ...
'bootstrap' => [
'donate',
]
// ...
There probably already is a bootstrap
property in your configuration file; just
add 'donate'
to it.
Important: the module should also be set up in the same way in the console configuration (usually
called console.php
).
To complete the installation, a console command have to be run. This will create a database table for the donations:
yii migrate
The migration applied is called sjaakp\donate\migrations\m000000_000000_init
.
Placing the Donate widget in any view is trivial:
<?php
use sjaakp\donate\DonateWidget;
?>
...
<?= DonateWidget::widget() ?>
...
The small, collapsed variant is obtained by:
<?php
use sjaakp\donate\DonateWidget;
?>
...
<?= DonateWidget::widget([
'small' => true
]) ?>
...
The Donate module has a range of options. They are set in the application configuration like so:
<?php
// ...
'modules' => [
'donate' => [
'class' => sjaakp\donate\Module::class,
'description' => 'Please, buy me a drink!',
// ...
// ... more options ...
],
],
// ...
The options (most are optional) are:
string
One of the API keys obtained from Mollie.
Not optional, must be set.array
Amounts to select from. Keys are integers representing
the amounts in cents, values are textual representations.
Example: [ ..., 250 => '€2,50', 500 => '€5', ... ]
. Defaults: amounts of 5, 10, 25, 50, and 100.string|null
Text header appearing in the donate-widget. If
null
(default) no header is rendered.bool
Whether a 'friendly message' field is included in the widget.
Default: true
.bool
Whether confetti is shown on the 'thanks' page.
Default: true
.string|null
The textual description displayed on Mollie's
payment page. If null
(default), defaults to 'Donation for <site name>'
.string|null
The locale sent to the payment site. If null
,
defaults to site's language
property.array
Options for the app mailer.
Default: see source.bool|null
If true
, performs a dummy-payment on the
local system, useful for debugging and testing. If null
(default),
localTest is set to true
if YII_ENV === 'dev'
, in other words
if the site is in the development environment.array
The access rule
for the donations overview (donate
page). Default: only accessible to
authenticated visitors ([ 'allow' => true, 'roles' => ['@'] ]
). For most
sites, you'll want to refine this.All of Yii2-donate's utterances are translatable. The translations are
in the 'sjaakp\donate\messages'
directory.
You can override Yii2-donate's translations by setting the application's message source in the main configuration, like so:
<?php
// ...
'components' => [
// ... other components ...
'i18n' => [
'translations' => [
// ... other translations ...
'donate' => [ // override donate's standard messages
'class' => yii\i18n\PhpMessageSource::class,
'basePath' => '@app/messages', // this is a default
'sourceLanguage' => 'en-US', // this as well
],
],
],
// ... still more components ...
]
The translations should be in a file called 'donate.php'
.
If you want a single or only a few messages translated and use Yii2-donate's
translations for the main part, the trick is to set up 'i18n'
like above
and write your translation file something like:
<?php
// app/messages/nl/donate.php
$donateMessages = Yii::getAlias('@sjaakp/donate/messages/nl/donate.php');
return array_merge (require($donateMessages), [
'Amount' => 'Bedrag in euro', // your preferred translation
]);
At the moment, the only language implemented is Dutch. Agreed, it's only the world's 52th language, but it happens to be my native tongue. Please, feel invited to translate Yii2-donate in other languages. I'll be more than glad to include them into Yii2-donate's next release.
By default, the Module ID is 'donate'
. It is set in the module
configuration. If necessary (for instance if there is a conflict with
another module or application component), you may set the Module
ID to something different. Important: in that case, the moduleId
property of the Donate widget must be set to
this new value as well.
Can I change the layout for the Yii2-donate views?
Use the EVENT_BEFORE_ACTION
event.
One easy way is to incorporate it in the module setup, like so:
<?php
// ...
'modules' => [
'donate' => [
'class' => sjaakp\donate\Module::class,
'description' => 'Please, buy me a drink!',
'on beforeAction' => function ($event) {
$event->sender->layout = '@app/views/layouts/one_column';
},
// ... more options ...
],
],
// ...
A versatile QR code generation library that supports HTML, PNG, and SVG output formats.
The Duna QR Code Library is a QR code generation tool originally based on the QrCode
library bundled with Duna until v8.0, developed by Laurent Minguet. It is distributed under the LGPL license, providing a flexible and open-source solution for QR code generation.
To install the library, use Composer:
$ composer require duna/qrcode
Here's a quick guide to using the Duna QR Code Library:
First, include the necessary classes and create a QR code instance:
<?php
use Duna\Helpers\QrCode\QrCode;
use Duna\Helpers\QrCode\Output;
$qrCode = new QrCode('Lorem ipsum dolor sit amet');
To generate a PNG image of the QR code, specifying dimensions and colors, use:
// Create PNG output
$output = new Output\Png();
// Generate PNG data with a specified width, background color (white), and foreground color (black)
$data = $output->output($qrCode, 100, [255, 255, 255], [0, 0, 0]);
// Save the PNG data to a file
file_put_contents('file.png', $data);
For SVG output, which is useful for scalable vector graphics:
// Create SVG output
$output = new Output\Svg();
// Generate SVG data with a specified width, background color (white), and foreground color (black)
echo $output->output($qrCode, 100, 'white', 'black');
To display the QR code as an HTML table:
// Create HTML output
$output = new Output\Html();
// Generate HTML table representation of the QR code
echo $output->output($qrCode);
This library is provided under the GNU Lesser General Public License (LGPL) v3.0. See the LICENSE file for details.
Contributions are welcome! Please refer to our CONTRIBUTING guidelines for more information.
For issues and support, please refer to our issue tracker or reach out to the community.
]]>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
]]>A Yii 2 demo application to illustrate how Inertia.js works.
With Inertia you are able to build single-page apps using classic server-side routing and controllers, without building an API.
This application is a port of the original Ping CRM written in Laravel and based on the Yii 2 Basic Project Template.
Based on the application Ping CRM on Yii 2 github and yii extension.
Changes: Updated Vue to version 3, updated npm packages and composer. Converted Vue files to Composition API (script setup).
https://pingcrm-yii2.tebe.ch
Clone the repo locally:
git clone https://github.com/toatall/pingcrm-yii2-vue3 pingcrm-yii2-vue3
cd pingcrm-yii2-vue3
Install PHP dependencies:
composer install
Install NPM dependencies:
npm ci
Build assets:
npm run css-dev
npm run dev
Create an SQLite database. You can also use another database (MySQL, Postgres), simply update your configuration accordingly.
touch database/database.sqlite
Run database migrations:
php yii migrate
Run database seeder:
php yii db/seed
Run the dev server (the output will give the address):
php yii serve
You're ready to go! Visit Ping CRM in your browser, and login with:
To run the Ping CRM tests, run:
(to be done)
The following steps are required when extending this project with new features.
<?php
namespace app\controllers;
use tebe\inertia\web\Controller;
class CustomController extends Controller
{
public function actionIndex()
{
$params = [
'data' => [],
];
return $this->inertia('demo/index', $params);
}
}
You can find more information at https://github.com/tbreuss/yii2-inertia.
resources/js/Pages
for each controller action you added in the backend You can find more information at https://inertiajs.com.
RandomProvider is derived from ActiveDataProvider of the Yii 2.0 PHP Framework. It selects the records in a random fashion, which in some cases may be more attractive than the orderly way a regular ActiveDataProvider (usually) does it. RandomProvider is intended to co-operate with my LoadMorePager, but it will work with LinkPager or other pagers as well.
Notice that RandomProvider doesn't support CUBRID
or dblib
database drivers. Moreover,
I only tested it with mysql
. I'm pretty sure it'll work with other drivers, though.
If you have any experiences to share, I'll appreciate that.
Notice also that RandomProvider makes use of an algorithm named 'Order By Rand()'. This is rather slow, and doesn't scale very well. Therefore, it is advised to use RandomProvider only with relatively small data sets (think of less than a few thousands of records). More information here.
A demonstration of RandomProvider is here.
Install yii2-random-provider in the usual way with Composer.
Add the following to the require
section of your composer.json
file:
"sjaakp/yii2-random-provider": "*"
or run:
composer require sjaakp/yii2-random-provider
You can manually install yii2-random-provider by downloading the source in ZIP-format.
RandomProvider is a drop-in replacement for Yii's ActiveDataProvider. Just use it like ActiveDataProvider.
]]>本项目为yii2-debug的扩展,使用MongoDB对debug数据进行存储。
src/ 代码目录
src/models/ 数据模型
src/views/ 视图文件
src/controllers/ 控制器
composer require yagas/yii2-debug4mongo
if (YII_ENV_DEV) {
$config['bootstrap'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yagas\debug\Module',
'logTarget' => [
'class' => 'yagas\debug\LogTarget',
'app_no' => 'localhost_001', // 为当前站点设定标识
],
'percent' => 10, // 百分之十的几率清除历史数据(GC)
];
}
]]>Previewer PDF File with PDF.js for Yii2
Yii2 PDF.js uses PDF.js
Demo: https://mozilla.github.io/pdf.js/web/viewer.html
Package is available on Packagist, you can install it using Composer.
composer require diecoding/yii2-pdfjs '^1.0'
or add to the require section of your composer.json
file.
'diecoding/yii2-pdfjs': '^1.0'
...
'modules'=>[
'pdfjs' => [
'class' => \diecoding\pdfjs\Module::class,
],
],
...
echo \diecoding\pdfjs\PdfJs::widget([
'url' => '@web/uploads/dummy.pdf',
]);
echo Url::to(["/pdfjs", 'file' => Url::to('@web/uploads/dummy.pdf', true)], true);
echo \diecoding\pdfjs\PdfJs::widget([
'url' => '@web/uploads/dummy.pdf',
'options' => [
'style' => [
'width' => '100%',
'height' => '500px',
],
],
]);
echo \diecoding\pdfjs\PdfJs::widget([
'url' => '@web/uploads/dummy.pdf',
'sections' => [
'toolbarContainer' => false,
],
]);
]]>消息队列主要用于业务解耦,本项目采用rabbitmq,支持thinkPHP,laravel,webman,yii等常用框架,也可以单独使用。
composer require xiaosongshu/rabbitmq
<?php
namespace app\commands;
require_once __DIR__.'/vendor/autoload.php';
class Demo extends \Xiaosongshu\Rabbitmq\Client
{
/** 以下是rabbitmq配置 ,请填写您自己的配置 */
/** @var string $host 服务器地址 */
public static $host = "127.0.0.1";
/** @var int $port 服务器端口 */
public static $port = 5672;
/** @var string $user 服务器登陆用户 */
public static $user = "guest";
/** @var string $pass 服务器登陆密码 */
public static $pass = "guest";
/**
* 业务处理
* @param array $params
* @return int
*/
public static function handle(array $params): int
{
//TODO 这里写你的业务逻辑
// ...
var_dump($params);
return self::ACK;
//return self::NACK;
}
}
\app\commands\Demo::publish(['name'=>'tome','age'=>15]);
你可以在任何地方投递消息。
\app\commands\Demo::consume();
你可以把消费者放到command命令行里面,使用命令行执行队列消费。举个例子(这里以yii为例子,你也可以换成laravel,webman,thinkPHP等其他框架):
`
php
<?php
namespace app\commands;
use yii\console\Controller;
/**
@note 我只是一个例子 */ class QueueController extends Controller {
/**
开启消费者命令 consume
```bash
php yii queue/index
注:如果你需要开启多个消费者,那么可以在多个窗口执行开启消费者命令即可。当然你也可以使用多进程来处理。
\app\commands\Demo::close();
队列使用过程中请使用 \RuntimeException和\Exception捕获异常
本项目根目录有一个demo.php的测试文件,可以复制到你的项目根目录,在命令行窗口直接在命令行执行以下命令即可。
`
php
php demo.php
测试文件代码如下:
php
<?php
namespace xiaosongshu\test; require_once DIR . '/vendor/autoload.php';
/**
@purpose 定义一个队列演示 */ class Demo extends \Xiaosongshu\Rabbitmq\Client {
/* 以下是rabbitmq配置 ,请填写您自己的配置 / /* @var string $host 服务器地址 / public static $host = "127.0.0.1";
/* @var int $port 服务器端口 / public static $port = 5672;
/* @var string $user 服务器登陆用户 / public static $user = "guest";
/* @var string $pass 服务器登陆密码 / public static $pass = "guest";
/**
/ 投递普通消息 */
\xiaosongshu\test\Demo::publish(['name' => 'tom']);
\xiaosongshu\test\Demo::publish(['name' => 'jim']);
\xiaosongshu\test\Demo::publish(['name' => 'jack']);
/* 开启消费,本函数为阻塞,后面的代码不会执行 /
\xiaosongshu\test\Demo::consume();
/ 关闭消费者 */
\xiaosongshu\test\Demo::close();
`
composer require xiaosongshu/yii2-rabbitmq
<?php
namespace app\commands;
require_once __DIR__.'/vendor/autoload.php';
class Demo extends \Xiaosongshu\Rabbitmq\Client
{
/** 以下是rabbitmq配置 ,请填写您自己的配置 */
/** @var string $host 服务器地址 */
public static $host = "127.0.0.1";
/** @var int $port 服务器端口 */
public static $port = 5672;
/** @var string $user 服务器登陆用户 */
public static $user = "guest";
/** @var string $pass 服务器登陆密码 */
public static $pass = "guest";
/**
* 业务处理
* @param array $params
* @return int
*/
public static function handle(array $params): int
{
//TODO 这里写你的业务逻辑
// ...
var_dump($params);
return self::ACK;
//return self::NACK;
}
}
\app\commands\Demo::publish(['name'=>'tome','age'=>15]);
你可以在任何地方投递消息。
\app\commands\Demo::consume();
你可以把消费者放到command命令行里面,使用命令行执行队列消费。举个例子:
`
php
<?php
namespace app\commands;
use yii\console\Controller;
/**
@note 我只是一个例子 */ class QueueController extends Controller {
/**
开启消费者命令 consume
```bash
php yii queue/index
队列使用过程中请使用 \RuntimeException和\Exception捕获异常
composer require xiaosongshu/yii2-elasticsearch
`
php
'components' => [
'ESClient' => [
'class' => \Xiaosongshu\Elasticsearch\ESClient::class,
'node'=>['192.168.101.170:9200'],
'username' => '',
'password' => '',
],
]
`
$res = Yii::$app->ESClient->search('index','_doc','title','测试')['hits']['hits'];
创建索引:createIndex
创建表结构:createMappings
删除索引:deleteIndex
获取索引详情:getIndex
新增一行数据:create
批量写入数据:insert
根据id批量删除数据:deleteMultipleByIds
根据Id 删除一条记录:deleteById
获取表结构:getMap
根据id查询数据:find
根据某一个关键字搜索:search
使用原生方式查询es的数据:nativeQuerySearch
多个字段并列查询,多个字段同时满足需要查询的值:andSearch
or查询 多字段或者查询:orSearch
根据条件删除数据:deleteByQuery
根据权重查询:searchByRank
获取所有数据:all
添加脚本:addScript
获取脚本:getScript
使用脚本查询:searchByScript
使用脚本更新文档:updateByScript
索引是否存在:IndexExists
根据id更新数据:updateById
<?php
require_once 'vendor/autoload.php';
/** 实例化客户端 */
$client = new \Xiaosongshu\Elasticsearch\ESClient([
/** 节点列表 */
'nodes' => ['192.168.4.128:9200'],
/** 用户名 */
'username' => '',
/** 密码 */
'password' => '',
]);
/** 删除索引 */
$client->deleteIndex('index');
/** 如果不存在index索引,则创建index索引 */
if (!$client->IndexExists('index')) {
/** 创建索引 */
$client->createIndex('index', '_doc');
}
/** 创建表 */
$result = $client->createMappings('index', '_doc', [
'id' => ['type' => 'long',],
'title' => ['type' => 'text', "fielddata" => true,],
'content' => ['type' => 'text', 'fielddata' => true],
'create_time' => ['type' => 'text'],
'test_a' => ["type" => "rank_feature"],
'test_b' => ["type" => "rank_feature", "positive_score_impact" => false],
'test_c' => ["type" => "rank_feature"],
]);
/** 获取数据库所有数据 */
$result = $client->all('index','_doc',0,15);
/** 写入单条数据 */
$result = $client->create('index', '_doc', [
'id' => rand(1,99999),
'title' => '我只是一个测试呢',
'content' => '123456789',
'create_time' => date('Y-m-d H:i:s'),
'test_a' => 1,
'test_b' => 2,
'test_c' => 3,
]);
/** 批量写入数据 */
$result = $client->insert('index','_doc',[
[
'id' => rand(1,99999),
'title' => '我只是一个测试呢',
'content' => '你说什么',
'create_time' => date('Y-m-d H:i:s'),
'test_a' => rand(1,10),
'test_b' => rand(1,10),
'test_c' => rand(1,10),
],
[
'id' => rand(1,99999),
'title' => '我只是一个测试呢',
'content' => '你说什么',
'create_time' => date('Y-m-d H:i:s'),
'test_a' => rand(1,10),
'test_b' => rand(1,10),
'test_c' => rand(1,10),
],
[
'id' => rand(1,99999),
'title' => '我只是一个测试呢',
'content' => '你说什么',
'create_time' => date('Y-m-d H:i:s'),
'test_a' => rand(1,10),
'test_b' => rand(1,10),
'test_c' => rand(1,10),
],
]);
/** 使用关键字搜索 */
$result = $client->search('index','_doc','title','测试')['hits']['hits'];
/** 使用id更新数据 */
$result1 = $client->updateById('index','_doc',$result[0]['_id'],['content'=>'今天你测试了吗']);
/** 使用id 删除数据 */
$result = $client->deleteById('index','_doc',$result[0]['_id']);
/** 使用条件删除 */
$client->deleteByQuery('index','_doc','title','测试');
/** 使用关键字搜索 */
$result = $client->search('index','_doc','title','测试')['hits']['hits'];
/** 使用条件更新 */
$result = $client->updateByQuery('index','_doc','title','测试',['content'=>'哇了个哇,这么大的种子,这么大的花']);
/** 添加脚本 */
$result = $client->addScript('update_content',"doc['content'].value+'_'+'谁不说按家乡好'");
/** 添加脚本 */
$result = $client->addScript('update_content2',"(doc['content'].value)+'_'+'abcdefg'");
/** 获取脚本内容 */
$result = $client->getScript('update_content');
/** 使用脚本搜索 */
$result = $client->searchByScript('index', '_doc', 'update_content', 'title', '测试');
/** 删除脚本*/
$result = $client->deleteScript('update_content2');
/** 使用id查询 */
$result = $client->find('index','_doc','7fitkYkBktWURd5Uqckg');
/** 原生查询 */
$result = $client->nativeQuerySearch('index',[
'query'=>[
'bool'=>[
'must'=>[
[
'match_phrase'=>[
'title'=>'测试'
],
],
[
'script'=>[
'script'=>"doc['content'].value.length()>2"
]
]
]
]
]
]);
/** and并且查询 */
$result = $client->andSearch('index','_doc',['title','content'],'测试');
/** or或者查询 */
$result = $client->orSearch('index','_doc',['title','content'],'今天');
将本扩展包的phpunit.xml文件复制到项目的根目录下面然后执行下面的命令
`
bash
php ./vendor/bin/phpunit -c phpunit.xml
`
$config['components']['mailer'] = [
'class' => 'jatin\resend\Mailer',
'useFileTransport' => false,
'viewPath' => '@app/mail',
'transport' => [
'apiKey' => '<YOUR_API_KEY>'
],
];
]]>
This is a simple proxy for Yii2 framework. This extension provides the HTTP proxy action for the Yii framework 2.0.
For license information check the LICENSE-file.
composer require asminog/yii2-proxy
use asminog\proxy\ProxyAction;
class SiteController extends Controller
{
public function actions()
{
return [
'proxy' => [
'class' => ProxyAction::class,
// 'accessToken' => 'your-access-token', // - set access token for secure requests
// 'throw404Exception' => true, // - show 404 error if access token is not valid or request url is not valid
// 'proxyHeaders' => ['User-Agent', 'Content-Type'], // - set headers for proxy request
'proxyHeaders' => ['Authorization', 'Content-Type'], // - set headers for chatgpt proxy request
// 'proxyCookies' => ['cookie1', 'cookie2'], // - set cookies for proxy request
],
];
}
}
$this->client = new Client([
'transport' => CurlTransport::class,
'baseUrl' => 'https://domain.com/site/proxy', // - set url to your proxy action
'requestConfig' => [
'format' => Client::FORMAT_JSON,
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
'X-Proxy-Url' => 'https://api.openai.com/v1/chat/completions', // - set url to your api
// 'X-Access-Token' => 'your-access-token' // - set access token for secure requests
],
],
]);
$response = $this->client->post('', [
'model' => 'gpt-3.5-turbo',
'messages' => [
[
'role' => 'user',
'content' => 'Hello, how are you?',
],
],
]);
if ($response->isOk) {
$data = $response->data;
// - do something with response data
} else {
// - handle error
}
$this->client->close();
]]>A Yii2 form-wizard widget for bootstrap 5
The preferred way to install this extension is through composer.
Either run
php composer.phar require --prefer-dist sandritsch91/yii2-form-wizard
or add
"sandritsch91/yii2-form-wizard": "*"
to the require section of your composer.json
use sandritsch91\yii2-form-wizard\FormWizard;
echo FormWizard::widget([
// required
'model' => $model, // The model to be used in the form
'tabOptions' => [ // These are the options for the Bootstrap Tab widget
'items' => [
[
'label' => 'Step 1', // The label of the tab, if omitted, a default-label will be used (Step 1, Step 2, ...)
'content' => $this->render('_step1', ['model' => $model]), // Either the content of the tab
],
[
'label' => 'Step 2',
'view' => '/test/_step2', // or a view to be rendered. $model and $form are passed to the view
'params' => ['a' => 1, 'b' => 2] // Pass additional parameters to the view
]
],
'navType' => 'nav-pills'
],
// optional
'validateSteps' => [ // Optional, pass the fields to be validated for each step.
['name', 'surname'],
[], // Leave array empty if no validation is needed
['email', 'password']
],
'options' => [], // Wizard-container html options
'formOptions' => [], // Form html options
'buttonOptions' => [ // Button html options
'previous' => [
'class' => ['btn', 'btn-secondary'],
'data' => [
'formwizard' => 'previous' // If you change this, make sure the clientOptions match
]
],
'next' => [...],
'finish' => [...]
],
'clientOptions' => [ // Client options for the form wizard, if you need to change them
// 'finishSelector' => '...',
// 'nextSelector' => '...',
// 'previousSelector' => '...',
// 'keepPosition' => true // Keep scroll position on step change.
// Set to false to disable, or pass a selector if you have a custom scroll container.
// Defaults to true.
],
'clientEvents' => [ // Client events for the form wizard
// 'onNext' => 'function () {...}',
// 'onPrevious' => 'function () {...}',
// 'onFinish' => 'function (){...}'
]
]);
Contributions are welcome.
If you have any questions, ideas, suggestions or bugs, please open an issue.
This package uses codeception for testing. To run the tests, run the following commands:
#### Unit tests
run ```php.exe .\vendor\bin\codecept run Unit``` in the root directory of this repository.
#### Functional tests
run ```php.exe .\vendor\bin\codecept run Functional``` in the root directory of this repository.
#### Accpetance tests
To be able to run acceptance tests, a few requirements are needed:
For Windows:\
- install java runtime environment
- install nodejs
- install selenium-standalone: `npm install -g selenium-standalone`
- start selenium-standalone: `selenium-standalone install && selenium-standalone start`
- host a yii2 application on a server or locally via ```./yii serve```
- add this plugin as a dependency to your ```composer.json``` and update dependencies
- site must be reachable over http://formwizard.com/
- add an action ```actionTest``` to the ```SiteController```, as described below
- this action must return a view file, as described below
- run ```php.exe .\vendor\bin\codecept run Acceptance```
For Linux:
Never did that before, but I think it is similar to the Windows setup.
The action in the SiteController:
```php
public function actionTest(): string
{
include __DIR__ . '/../vendor/sandritsch91/yii2-widget-form-wizard/tests/Support/Data/models/User.php';
$model = new User();
if (Yii::$app->request->post() && $model->load(Yii::$app->request->post()) && $model->validate()) {
return 'success';
}
return $this->render('test', [
'model' => new User()
]);
}
```
The view returned by the action:
```php
/** @var User $model */
use sandritsch91\yii2\formwizard\FormWizard;
use sandritsch91\yii2\formwizard\tests\Support\Data\models\User;
$wizard = FormWizard::widget([
'model' => $model,
'tabOptions' => [
'options' => [
'class' => 'mb-3'
],
'items' => [
[
'label' => 'Step 1',
'view' => '@app/vendor/sandritsch91/yii2-widget-form-wizard/tests/Support/Data/views/site/step1',
'linkOptions' => [
'id' => 'step1-link',,
'params' => [
'test' => 'some test variable'
]
]
],
[
'label' => 'Step 2',
'view' => '@app/vendor/sandritsch91/yii2-widget-form-wizard/tests/Support/Data/views/site/step2',
'linkOptions' => [
'id' => 'step2-link',
]
],
[
'label' => 'Step 3',
'view' => '@app/vendor/sandritsch91/yii2-widget-form-wizard/tests/Support/Data/views/site/step3',
'linkOptions' => [
'id' => 'step3-link',
]
]
],
'navType' => 'nav-pills'
],
'validateSteps' => [
['firstname', 'lastname'],
['username', 'password', 'password_validate'],
['email']
],
'clientOptions' => [
'keepPosition' => true
]
]);
echo \yii\helpers\Html::tag('div', $wizard, [
'class' => 'col-4'
]);
```
After the initial installation, you only have to start the selenium-standalone server ```selenium-standalone start```
and run the tests ```php.exe .\vendor\bin\codecept run Acceptance``` in the root directory of this repository.
If you do not want to setup an application, just run the unit and functional tests by
running ```php.exe .\vendor\bin\codecept run Unit,Functional```, I can modify and run the acceptance tests for you,
after you opened a pull request.
]]>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
]]>Registra cambios de sus modelos ActiveRecord de Yii2.
Este paquete permite mantener un historial de cambios de los modelos proveyendo información sobre posibles discrepancias o anomalías en la información que puedan indicar actividades sospechosas. La información recibida y almacenada se puede posteriormente desplegar de diversas maneras.
La forma preferida de instalar esta extensión es a través de composer.
Luego ejecute
php composer.phar require --prefer-dist neoacevedo/yii2-auditing "*"
o agregue
"neoacevedo/yii2-auditing": "*"
a la sección require de su archivo composer.json
.
Una vez que la extensión está instalada, en el archivo de configuración de la consola de su aplicación, agregue en la zona migrationPath
...
'@vendor/neoacevedo/yii2-auditing/neoacevedo/auditing/migrations',
...
luego, agregue en el código de su modelo dentro del método behaviors
:
public function behaviors()
{
return [
[
'class' => \neoacevedo\auditing\behaviors\AuditBehavior::class,
'deleteOldData' => true, // Para borrar datos antiguos del registro de eventos
'deleteNumRows' => 20, // Borra esta cantidad de registros
'ignored' => ['foo', 'bar'], // No registra en el registro de eventos estos atributos del modelo
],
...
];
}
Puede desplegar la información como cualquier modelo que haya implementado dentro de su aplicación web.
Puede hacer uso de un controlador y una vista que use GridView
para listar el historial. Por ejemplo, puede crear un controllador que se llame AuditingController
y crear el método actionIndex
como lo siguiente:
/**
* Lists all Auditing models.
*
* @return string
*/
public function actionIndex()
{
$searchModel = new AuditingSearch();
$dataProvider = $searchModel->search($this->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
Para visualizar los datos, crear el método actionView
:
/**
* Displays a single Auditing model.
* @param int $id ID
* @return string
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}
Dentro de la vista view
puede agregar el GridView
para listar el histórico:
...
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'user_id',
'description',
'event',
'model',
'attribute',
'old_value',
'new_value',
'action',
'ip',
'created_at',
],
]); ?>
...
]]>A database extension for DM database
The preferred way to install this extension is through composer.
Either run
php composer.phar require --prefer-dist luguohuakai/yii2-dm "*"
or add
"luguohuakai/yii2-dm": "*"
to the require section of your composer.json
file.
Once the extension is installed, simply use it in your code by :
'components' => [
'db' => [
'class' => 'luguohuakai\db\dm\Connection',
'dsn' => 'dm:host=localhost:xxx;schema=xxx',
'username' => 'SYSDBA',
'password' => 'SYSDBA',
]
]
]]>The DataTable widget is used to create interactive and dynamic data tables. The provided JavaScript code demonstrates how to initialize DataTable with server-side processing, custom data handling, and column rendering and with full serverside Export .
`composer require rashedalkhatib/yii2-datatables:1.0.0
``../frontend/assets/AppAsset.php
`rashedalkhatib\datatables\DataTableAsset
your $depends
array public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapAsset',
'yii\bootstrap\BootstrapPluginAsset',
'rashedalkhatib\datatables\DataTableAsset'
];
$searchFormSelector = '#searchForm';
$ajaxUrl = Url::to(['api/yourEndPoint']); // Adjust the URL based on your routes
// Define your DataTable columns
$columns = [
[
'title' => 'ID',
'data' => 'id',
'visible' => true,
'render' => new JsExpression('function(data, type, row) {
return "demo";
}'),
],
];
// Configure other DataTable parameters
$processing = true;
$serverSide = true;
$pageLength = 10;
$dom = 'Btip';
$buttons = [
[
'extend' => 'excel',
'text' => 'Excel',
'titleAttr' => 'Excel',
'action' => new JsExpression('exportAll') // this is required
],
];
// Configure Ajax settings
$ajaxConfig = [
'url' => $ajaxUrl,
'bdestroy' => true,
'type' => 'POST',
'data' => new JsExpression('function(d) {
var searchForm = $('body').find('#searchForm').serializeArray();
searchForm[searchForm.length] = { name: 'YourModel[page]', value: d.start }; // required
searchForm[searchForm.length] = { name: 'YourModel[length]', value: d.length }; // required
searchForm[searchForm.length] = { name: 'YourModel[draw]', value: d.draw }; // required
var order = {
'attribute': d.columns[d.order[0]['column']]['data'],
'dir': d.order[0]['dir']
}; // required
searchForm[searchForm.length] = { name: 'YourModel[order]', value: JSON.stringify(order) };
return searchForm;
}'),
'dataSrc' => new JsExpression('function(d) {
var searchForm = $("' . $searchFormSelector . '").serializeArray();
if (d.validation) {
searchForm.yiiActiveForm("updateMessages", d.validation, true);
return [];
}
return d.data;
}'),
];
// Use the DataTableWidget with configured parameters
DataTable::widget([
'id' => 'yourDataTable',
'ajaxConfig' => $ajaxConfig,
'columns' => $columns,
'processing' => $processing,
'serverSide' => $serverSide,
'pageLength' => $pageLength,
'dom' => $dom,
'buttons' => $buttons,
]);
// The HTML container for your DataTable
echo '<form id="searchForm">// your inputs </form>';
echo '<table id="yourDataTable" class="display"></table>';
<form id="searchForm">
// your inputs
</form>
<table id="yourDataTable" class="display" style="width:100%">
</table>
var arrayToExport = [0,1];
$('#yourDataTable').DataTable({
"ajax": {
// Server-side processing configuration
"url": "../api/yourEndPoint",
"bdestroy": true, // this allows you to re init the dataTabel and destory it
"type": "POST", // request method
"data": function (d) { // this represent the data you are sending with your ajax request
// Custom function for sending additional parameters to the server
var searchForm = $('body').find('#searchForm').serializeArray();
searchForm[searchForm.length] = { name: "YourModel[page]", value: d.start }; // required
searchForm[searchForm.length] = { name: "YourModel[length]", value: d.length }; // required
searchForm[searchForm.length] = { name: "YourModel[draw]", value: d.draw }; // required
var order = {
'attribute': d.columns[d.order[0]['column']]['data'],
'dir': d.order[0]['dir']
}; // required
searchForm[searchForm.length] = { name: "YourModel[order]", value: JSON.stringify(order) };
return searchForm;
},
dataSrc: function (d) {
// Custom function to handle the response data
// EX:
var searchForm = $('body').find('#searchForm').serializeArray();
if (d.validation) {
searchForm.yiiActiveForm('updateMessages', d.validation, true);
return [];
}
return d.data;
}
},
"columns": [{
// Column configurations
"title": "ID",
"data": "id",
"visible": true // visablity of column
},
// ... (other columns)
{
"title": "Actions",
"data": "id",
"visible": actionCol,
"render": function (data, type, row) {
// Custom rendering function for the "Actions" column
return '<a class="showSomething" data-id="' + row.id + '">View</a>';
}
}],
processing: true,
serverSide: true,
"pageLength": 10,
dom: "Btip",
"buttons": [{
// "Excel" button configuration
"extend": 'excel',
exportOptions: {
columns: arrayToExport
},
"text": ' Excel',
"titleAttr": 'Excel',
"action": exportAll // newexportaction this action is to allow you exporting with server side without rendaring data
}],
});
// in your HTTP request you want to include these params
$_postData = [
'page' => $this->page == 0 ? 0 : $this->page / $this->length, // this equation is required to handle Yii2 Data provider Logic
'limit' => $this->length,
'export' => $this->export,
'order' => $this->order,
// add your custom params .....
];
return $this->asJson(
[
'data' => $_scoreData->data,
'draw' => $_scoreSearchForm->draw,
'recordsTotal' => $_scoreData->count,
'recordsFiltered' => $_scoreData->count
]);
public function actionYourEndPoint()
{
$searchModel = new SearchModel();
$dataProvider = $searchModel->search(Yii::$app->request->get());
return $this->asJson(
array(
'data' => $dataProvider['data'],
'count' => $dataProvider['count']
)
);
}
public function search($params)
{
$this->load($params, ''); // load your values into the model
$query = Data::find(); // Data model is your link to the database
$_order = json_decode($this->order);
if ($this->export == 'true') {
$dataProvider = new ActiveDataProvider([
'query' => $query
// we removed the page and pageSize keys to allow all data to be exported
]);
} else {
$_orderType = SORT_ASC;
if ($_order->dir == 'desc')
$_orderType = SORT_DESC;
$query->orderBy([$_order->attribute => $_orderType]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => $this->limit,
'page' => $this->page,
],
]);
}
return array(
'data' => $dataProvider->getModels(),
'count' => $dataProvider->getTotalCount()
);
}
This behavior automatically decodes attributes from JSON to arrays before validation, handling errors and re-encoding if validation fails. With this a "real" json string can be further processed.
The preferred way to install this extension is through composer.
Either run
composer require --prefer-dist eluhr/yii2-json-attribute-behavior "*"
or add
"eluhr/yii2-json-attribute-behavior": "*"
to the require
section of your composer.json
file.
In a yii\base\Model
or a derivation thereof, the behavior can be used as follows:
public function behaviors(): array
{
$behaviors = parent::behaviors();
$behaviors['json-attribute'] = [
'class' => eluhr\jsonAttributeBehavior\JsonAttributeBehavior::class,
'attributes' => [
'data_json'
]
];
return $behaviors;
}
By using this behavior it does not matter if the attribute is a string or an array. The behavior will always ensure, that the attribute is an array before saving the data to the database and yii will handle the rest.
This behavior supports i18n. By adding the json-attribute-behavior
category in your config you can overwrite the default error messages.
After installing dependencies via composer you can run the tests with:
make test
]]>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 intl
extension to provide most of its I18N features, such as the date and number formatting of theyii\i18n\Formatter
class 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 installintl
when 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:
fr
or French Canada:fr-CA
Portuguese:
pt
or Portuguese Brazil:pt-BR
The name
is the name of the language in that language. e.g. for French: 'Français'
, for Japanese: '日本の'
. This is important as the user may not understand the browser's current language.
In /config/params.php
create an array named languages
with the languages required. For example:
/* List of languages and their codes
*
* format:
* 'Language Code' => 'Language Name',
* e.g.
* 'fr' => 'Français',
*
* please use alphabetical order of language code
* Use the language name in the "user's" Language
* e.g.
* 'ja' => '日本の',
*/
'languages' => [
// 'da' => 'Danske',
// 'de' => 'Deutsche',
// 'en' => 'English', // NOT REQUIRED the sourceLanguage (i.e. the default)
'en-GB' => 'British English',
'en-US' => 'American English',
'es' => 'Español',
'fr' => 'Français',
'it' => 'Italiano',
// 'ja' => '日本の', // Japanese with the word "Japanese" in Kanji
// 'nl' => 'Nederlandse',
// 'no' => 'Norsk',
// 'pl' => 'Polski',
'pt' => 'Português',
// 'ru' => 'Русский',
// 'sw' => 'Svensk',
// 'zh' => '中国的',
],
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-jwt
AuthController.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('[email protected]') // account to receive all the emails
->setCc(null)
->setBcc(null)
->setReplyTo(null);
$mailer->useFileTransport = false;
$mailer->send($message);
$mailer->useFileTransport = true;
return $mailer->generateMessageFileName();
}
How it works? fileTransportCallback
is the callback to specify the filename that should be used to create the saved email on @runtime/mail
. It "intercepts" the send email process, so we can use it for our porpuses.
useFileTransport
useFileTransport
This way we both receive all the emails on the specified account and get them stored
on @runtime/mail
.
Pretty simple helper to review emails on Yii2 applications.
Originally posted on: https://glpzzz.github.io/2020/10/02/yii2-redirect-all-emails.html
]]>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' => ['[email protected]'],
'to' => ['[email protected]', '[email protected]'],
'subject' => 'Log message',
],
],
],
],
// ...
],
The \yii\log\EmailTarget component is another way to log messages, in this case emailing them via the mailer
component of the application as specified on the mailer
attribute of EmailTarget
configuration. Note that you can also specify messages properties and which levels of messages should be the sent trough this target.
If you want to receive messages via other platforms besides email, there are other components that represents log targets:
Or you can implement your own by subclassing \yii\log\Target
]]>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">
]]>Articles are separated into more files as there is the max lenght for each file on wiki.
You will need MSSQL drivers in PHP. Programatically you can list them or test their presence like this:
var_dump(\PDO::getAvailableDrivers());
if (in_array('sqlsrv', \PDO::getAvailableDrivers())) {
// ... MsSQL driver is available, do something
}
Based on your system you have to download different driver. The differences are x64 vs x86 and ThreadSafe vs nonThreadSafe. In Windows I always use ThreadSafe. Explanation.
Newest PHP drivers are here.
Older PHP drivers here.
Once drivers are downloaded and extracted, pick one DLL file and place it into folder "php/ext". On Windows it might be for example here: "C:\xampp\php\ext"
Note: In some situations you could also need these OBDC drivers, but I am not sure when:
Now file php.ini must be modified. On Windows it might be placed here: "C:\xampp\php\php.ini". Open it and search for rows starting with word "extension" and paste there cca this:
extension={filename.dll}
// Example:
extension=php_pdo_sqlsrv_74_ts_x64.dll
Now restart Apache and visit phpinfo() web page. You should see section "pdo_sqlsrv". If you are using XAMPP, it might be on this URL: http://localhost/dashboard/phpinfo.php.
Then just add connection to your MSSQL DB in Yii2 config. In my case the database was remote so I needed to create 2nd DB connection. Read next chapter how to do it.
Adding 2nd database is done like this in yii-config:
'db' => $db, // the original DB
'db2'=>[
'class' => 'yii\db\Connection',
'driverName' => 'sqlsrv',
// I was not able to specify database like this:
// 'dsn' => 'sqlsrv:Server={serverName};Database={dbName}',
'dsn' => 'sqlsrv:Server={serverName}',
'username' => '{username}',
'password' => '{pwd}',
'charset' => 'utf8',
],
That's it. Now you can test your DB like this:
$result = Yii::$app->db2->createCommand('SELECT * FROM {tblname}')->queryAll();
var_dump($result);
Note that in MSSQL you can have longer table names. Example: CATEGORY.SCHEMA.TBL_NAME
And your first test-model can look like this (file MyMsModel.php):
namespace app\models;
use Yii;
use yii\helpers\ArrayHelper;
class MyMsModel extends \yii\db\ActiveRecord
{
public static function getDb()
{
return \Yii::$app->db2; // or Yii::$app->get('db2');
}
public static function tableName()
{
return 'CATEGORY.SCHEMA.TBL_NAME'; // or SCHEMA.TBL_NAME
}
}
Usage:
$result = MyMsModel::find()->limit(2)->all();
var_dump($result);
Once you have added the 2nd database (read above) go to the Model Generator in Gii. Change there the DB connection to whatever you named the connection in yii-config (in the example above it was "db2") and set tablename in format: SCHEMA.TBL_NAME. If MSSQL server has more databases, one of them is set to be the main DB. This will be used I think. I haven't succeeded to change the DB. DB can be set in the DSN string, but it had no effect in my case.
In previous chapters I showed how to use PhpExcel in Yii 1. Now I needed it also in Yii 2 and it was extremely easy.
Note: PhpExcel is deprecated and was replaced with PhpSpreadsheet.
// 1) Command line:
// This downloads everything to folder "vendor"
composer require phpoffice/phpspreadsheet --prefer-source
// --prefer-source ... also documentation and samples are downloaded
// ... adds cca 40MB and 1400 files
// ... only for devel system
// 2) PHP:
// Now you can directly use the package without any configuration:
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Uncomment following rows if you want to set col width:
//$sheet->getColumnDimension('A')->setAutoSize(false);
//$sheet->getColumnDimension('A')->setWidth("50");
$sheet->setCellValue('A1', 'Hello World !');
$writer = new Xlsx($spreadsheet);
// You can save the file on the server:
// $writer->save('hello_world.xlsx');
// Or you can send the file directly to the browser so user can download it:
// header('Content-Type: application/vnd.ms-excel'); // This is probably for older XLS files.
header('Content-Type: application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); // This is for XLSX files (they are basically zip files).
header('Content-Disposition: attachment;filename="filename.xlsx"');
header('Cache-Control: max-age=0');
$writer->save('php://output');
exit();
Thanks to DbCreator for the idea how to send XLSX to browser. Nevertheless exit() or die() should not be called. Read the link.
Following is my idea which originates from method renderPhpFile() from Yii2:
ob_start();
ob_implicit_flush(false);
$writer->save('php://output');
$file = ob_get_clean();
return \Yii::$app->response->sendContentAsFile($file, 'file.xlsx',[
'mimeType' => 'application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'inline' => false
]);
This also worked for me:
$tmpFileName = uniqid('file_').'.xlsx';
$writer->save($tmpFileName);
header('Content-Type: application/application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="filename.xlsx"');
header('Cache-Control: max-age=0');
echo file_get_contents($tmpFileName);
unlink($tmpFileName);
exit();
Note: But exit() or die() should not be called. Read the "DbCreator" link above.
See part I of this guide for other PDF creators:
TCPDF was created in 2002 (I think) and these days (year 2020) is being rewritten into a modern PHP application. I will describe both, but lets begin with the older version.
Older version of TCPDF
Download it from GitHub and save it into folder
{projectPath}/_tcpdf
Into web/index.php add this:
require_once('../_tcpdf/tcpdf.php');
Now you can use any Example to test TCPDF. For example: https://tcpdf.org/examples/example_001/
Note: You have to call constructor with backslash:
$pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
Note: Texts are printed using more methods - see file tcpdf.php for details:
Note: Store your files in UTF8 no BOM format so diacritics is correct in PDF.
Importing new TTF fonts is done like this:
// this command creates filed in folder _tcpdf\fonts. Use the filename as the fontname in other commands.
$fontname = \TCPDF_FONTS::addTTFfont("path to TTF file", 'TrueTypeUnicode', '', 96);
Now you can use it in PHP like this:
$pdf->SetFont($fontname, '', 24, '', true);
Or in HTML:
<font size="9" face="fontName" style="color: rgb(128, 128, 128);">ABC</font>
Rendering is done like this:
$pdf->writeHTML($html);
Note: When printing pageNr and totalPageCount to the footer, writeHTML() was not able to correctly interpret methods getAliasNumPage() and getAliasNbPages() as shown in Example 3. I had to use rendering method Text() and position the numbers correctly like this:
$this->writeHTML($footerHtmlTable);
$this->SetTextColor('128'); // I have gray pageNr
$this->Text(185, 279, 'Page ' . $this->getAliasNumPage() . '/' . $this->getAliasNbPages());
$this->SetTextColor('0'); // returning black color
New version of TCPDF
... to be finished ...
If I generate a PDF-invoice it contains many numbers and it is nice to print them as integers when decimals are not needed. For example number 24 looks better and saves space compared to 24.00. So I created such a formatter. Original inspiration and how-to was found here:
My formatter looks like this:
<?php
namespace app\myHelpers;
class MyFormatter extends \yii\i18n\Formatter {
public function asDecimalOrInteger($value) {
$intStr = (string) (int) $value; // 24.56 => "24" or 24 => "24"
if ($intStr === (string) $value) {
// If input was integer, we are comparing strings "24" and "24"
return $this->asInteger($value);
}
if (( $intStr . '.00' === (string) $value)) {
// If the input was decimal, but decimals were all zeros, it is an integer.
return $this->asInteger($value);
}
// All other situations
$decimal = $this->asDecimal($value);
// Here I trim also the trailing zero.
// Disadvantage is that String is returned, but in PDF it is not important
return rtrim((string)$decimal, "0");
}
}
Usage is simple. Read the link above and give like to karpy47 or see below:
// file config/web.php
'components' => [
'formatter' => [
'class' => 'app\myHelpers\MyFormatter',
],
],
There is only one formatter in the whole of Yii and you can extend it = you can add more methods and the rest of the formatter will remain so you can use all other methods as mentioned in documentation.
... can be easily done by adding a MySQL VIEW into your DB, creating a model for it and using it in the "ParentSearch" model as the base class.
Let's show it on list of invoices and their items. Invoices are in table "invoice" (model Invoice) and their items in "invoice_item" (model InvoiceItem). Now we need to join them and sort and filter them by SUM of prices (amounts). To avoid calculations in PHP, DB can do it for us if we prepare a MySQL VIEW:
CREATE VIEW v_invoice AS
SELECT invoice.*,
SUM(invoice_item.units * invoice_item.price_per_unit) as amount,
COUNT(invoice_item.id) as items
FROM invoice
LEFT JOIN invoice_item
ON (invoice.id = invoice_item.id_invoice)
GROUP BY invoice.id
Note: Here you can read why it is better not to use COUNT(*) in LEFT JOIN:
This will technically clone the original table "invoice" into "v_invoice" and will append 2 calculated columns: "amount" + "items". Now you can easily use this VIEW as a TABLE (for reading only) and display it in a GridView. If you already have a GridView for table "invoice" the change is just tiny. Create this model:
<?php
namespace app\models;
class v_Invoice extends Invoice
{
public static function primaryKey()
{
// here is specified which column(s) create the fictive primary key in the mysql-view
return ['id'];
}
public static function tableName()
{
return 'v_invoice';
}
}
.. and in model InvoiceSearch replace word Invoice with v_Invoice (on 2 places I guess) plus add rules for those new columns. Example:
public function rules()
{
return [
// ...
[['amount'], 'number'], // decimal
[['items'], 'integer'],
];
}
Into method search() add condition if you want to filter by amount or items:
if (trim($this->amount)!=='') {
$query->andFilterWhere([
'amount' => $this->amount
]);
}
In the GridView you can now use the columns "amount" and "items" as native columns. Filtering and sorting will work.
Danger: Read below how to search and sort by related columns. This might stop working if you want to join your MySQL with another table.
I believe this approach is the simplest to reach the goal. The advantage is that the MySQL VIEW is only used when search() method is called - it means in the list of invoices. Other parts of the web are not influenced because they use the original Invoice model. But if you need some special method from the Invoice model, you have it also in v_Invoice. If data is saved or changed, you must always modify the original table "invoice".
Lets say you have table of invoices and table of companies. They have relation and you want to display list of Invoices plus on each row the corresponding company name. You want to filter and sort by this column.
Your GridView:
<?= GridView::widget([
// ...
'columns' => [
// ...
[
'attribute'=>'company_name',
'value'=>'companyRelation.name',
],
Your InvoiceSearch model:
class InvoiceSearch extends Invoice
{
public $company_name;
// ...
public function rules() {
return [
// ...
[['company_name'], 'safe'],
];
}
// ...
public function search($params) {
// ...
// You must use joinWith() in order to have both tables in one JOIN - then you can call WHERE and ORDER BY on the 2nd table.
// Explanation here:
// https://stackoverflow.com/questions/25600048/what-is-the-difference-between-with-and-joinwith-in-yii2-and-when-to-use-them
$query = Invoice::find()->joinWith('companyRelation');
// Appending new sortable column:
$sort = $dataProvider->getSort();
$sort->attributes['company_name'] = [
'asc' => ['table.column' => SORT_ASC],
'desc' => ['table.column' => SORT_DESC],
'label' => 'Some label',
'default' => SORT_ASC
];
// ...
if (trim($this->company_name)!=='') {
$query->andFilterWhere(['like', 'table.column', $this->company_name]);
}
}
In my tutorial for Yii v1 I presented following way how to send headers manually and then call exit(). But calling exit() or die() is not a good idea so I discovered a better way in Yii v2. See chapter Secured (secret) file download
Motivation: Sometimes you receive a PDF file encoded into a string using base64. For example a label with barcodes from FedEx, DPD or other delivery companies and your task is to show the label to users.
For me workes this algorithm:
$pdfBase64 = 'JVBERi0xLjQ ... Y0CiUlRU9GCg==';
// First I create a fictive stream in a temporary file
// Read more about PHP wrappers:
// https://www.php.net/manual/en/wrappers.php.php
$stream = fopen('php://temp','r+');
// Decoded base64 is written into the stream
fwrite($stream, base64_decode($pdfBase64));
// And the stream is rewound back to the start so others can read it
rewind($stream);
// This row sets "Content-Type" header to none. Below I set it manually do application/pdf.
Yii::$app->response->format = Yii::$app->response::FORMAT_RAW;
Yii::$app->response->headers->set('Content-Type', 'application/pdf');
// This row will download the file. If you do not use the line, the file will be displayed in the browser.
// Details here:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Downloads
// Yii::$app->response->headers->set('Content-Disposition','attachment; filename="hello.pdf"');
// Here is used the temporary stream
Yii::$app->response->stream = $stream;
// You can call following line, but you don't have to. Method send() is called automatically when current action ends:
// Details here:
// https://www.yiiframework.com/doc/api/2.0/yii-web-response#sendContentAsFile()-detail
// return Yii::$app->response->send();
Note: You can add more headers if you need. Check my previous article (linked above).
]]>Note:Pantahub is the only place where Linux firmware can be shared and deployed for any device, You can signup @pantahub here:http://www.pantahub.com
Run $ unxz rpi3_initial_stable.img.xz
Note: pvr is a CLI tool which can be used to interact with your device through pantahub platform.
Note: Using pvr you can share your firmware and projects as simple as with a git tree.
Note: Move the pvr binary to your bin folder after download.
Linux(AMD64): Download
Linux(ARM32v6): Download
Darwin(AMD64): Download
pvr clone; pvr commit; pvr post
Install from github source code:
$ go get gitlab.com/pantacor/pvr
$ go build -o ~/bin/pvr gitlab.com/pantacor/pvr
Note: You need "GOLANG" to be installed in your system for building pvr from github source code.
$ pvr scan
¶
$ pvr claim -c merely-regular-gorilla https://api.pantahub.com:443/devices/5f1b9c44e193a
Watch now on Amazon Prime Video
5000afa9901
$ pvr clone https://pvr.pantahub.com/sirinibin/presently_learning_pelican/0 presently_learning_pelican
Now your device is ready to deploy your Yii2 app
`$ cd presently_learning_pelican`
>sirinibin/yii2-basic-arm32v7:latest
is a Docker
Watch now on Amazon Prime Video
image made for the devices with ARM32 architecture
>> You can customise the docker image for your custom Yii2 app.
$ pvr app add yii2 --from=sirinibin/yii2-basic-arm32v7:latest
$ pvr add .
$ pvr commit
$ pvr post
Status 1:
Status 2:
Status 3:
Status 4:
Access the device IP: http://10.42.0.231/myapp1/web/ in your web browser.
]]>You are done!
Yii2 - Converting from Bootstrap3 to Bootstrap4
This article has been written because while conversion is a largely pain-free process, there are some minor issues. These are not difficult to solve, but it is not immediately obvious where the problem lies.
1 - Install Bootstrap4 My preference is to simply use composer. Change composer.json in the root of your project:
Copy the line, and change the new line:
"yiisoft/yii2-bootstrap" : "~2.0.6", "yiisoft/yii2-bootstrap4" : "^2.0.8",
yii\bootstrap\
and change it to yii\bootstrap4\
. However, be careful - your IDE may require the backslash to be escaped. For example, Eclipse will find all files with the string easily, but the search string must have double-backslashes, while the replacement string must be left as single ones.NavBar::begin
and look at it's class options. In my case they were the original: 'class' => 'navbar-inverse navbar-fixed-top'
At the end of all this, I had the class line changed to something which gave me a navbar very similar to the original:
//Bootstrap3: 'class' => 'navbar-inverse navbar-fixed-top',
//Changed for Bootstrap4:
'class' => 'navbar navbar-expand-md navbar-light bg-dark',
Breadcrumbs
Note - March 2020: This entire section on Breadcrumbs may no longer be an issue. While I've left the section in as a tutorial, before making any changes read what Davide has to say in the user comments.
So, that fixed my navbar. Next, I noticed that the breadcrumbs were not quite right - the slash separating the path elements was no longer there. Preparing for a lot of debugging, I went to the Bootstrap site to look for a little inspiration. I didn't need to look any further - Bootstrap 4 requires each Breadcrumb element to have a class of "breadcrumb-item". After I spent a little time looking at vendors/yiisoft/yii2/widgets/Breadcrumbs.php to get some understanding of the issue, I discovered all that's needed is to change the itemTemplate and activeItemTemplate. Of course, since these are part of the Yii2 framework, you don't want to change that file, otherwise, it will probably get updated at some stage, and all your changes would be lost. Since both of these attributes are public, you can change them from outside the class, and the easiest place to do this is in frontend/views/main.php:
`
html
<div class="container">
<?= Breadcrumbs::widget([
'itemTemplate' => "\n\t<li class=\"breadcrumb-item\"><i>{link}</i></li>\n", // template for all links
'activeItemTemplate' => "\t<li class=\"breadcrumb-item active\">{link}</li>\n", // template for the active link
'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
]) ?>
<?= Alert::widget() ?>
<?= $content ?>
</div>```
Data Grid ActionColumn One of my pages was a data grid generated for me by gii. On each row it has a set of buttons that you can click to view, edit or delete the row. Under Bootstrap 4, the ActionColumn disappeared. Viewing the page source showed me it was there, but I couldn't see it or click on it. Going to the migration guide, it turns out that Bootstrap 3 includes icons, but Bootstrap 4 doesn't. I got a lot of help from a question asked in the Yii2forum.. In the end, my solution was to get a local copy of FontAwesome 5 by including the line "fortawesome/font-awesome": "^5.12.1" in the require section of composer.json, and then choosing the icons that I wanted. I spent a lot of time figuring out how to do this, but when I was done, it seemed almost anti-climactic in it's simplicity. This is what I did in my data form:
['class' => 'yii\grid\ActionColumn',
'buttons' => [
'update' => function($url,$model) {
return Html::a('<i class="fas fa-edit"></i>', $url, [
'title' => Yii::t('app', 'update')
]);
},
'view' => function($url,$model) {
return Html::a('<i class="fas fa-eye"></i>', $url, [
'title' => Yii::t('app', 'view')
]);
},
'delete' => function($url,$model) {
return Html::a('<i class="fas fa-trash"></i>', $url, [
'title' => Yii::t('app', 'delete')
]);
}
]
],
Functional Tests
Not seeing anything more visually, at least nothing that was obvious, I now ran the suite of tests. These were all previously passing, but now several of them failed. One of them was the Contact Form, so I ran that one separately and the tests informed me they were failing because they couldn't see the error message:
1) ContactCest: Check contact submit no data
Test ../frontend/tests/functional/ContactCest.php:checkContactSubmitNoData
Step See "Name cannot be blank",".help-block"
Fail Element located either by name, CSS or XPath element with '.help-block' was not found.
I, on the other hand, could see the error messages on the form, so I used the browser's page source and discovered that the css class was no longer "help-block", it had changed to "invalid-feedback". Easy enough - in frontend/tests/_support/FunctionalTester.php, I changed the expected css class:
public function seeValidationError($message)
{
$this->see($message, '.invalid-feedback');
}
Of course, this little excerpt is just an example. I found the same thing had to be done in several locations, but all were easily found and resolved.
After this, running my tests pointed me to no other problems, but I don't expect that to mean there aren't any other problems. While everything seems to be working so far, I expect there are more issues hiding in the woodwork. Somehow, those problems don't seem quite so insurmountable anymore.
]]>