Yii Framework Twig Extension 3.0.0
Yii View Twig Renderer version 3.0.0 was released. In this version:
ViewRenderer to more understandable TwigTemplateRendererYiiTwigExtensionyiisoft/view to ^11|^12composer.json to 8.1 - 8.5^8.1Version 2.3.8 of Queue extension for Yii 2 was released.
This release fixes problems with SQS and Redis, prevents multiple execution of aborted jobs, and adds PHP 8.4 compatibility. Environment variables are now passed to sub-processes.
See the CHANGELOG for details.
]]>It happened! Yii3 is officially released after years of intensive development and polishing.
Yii was always following the same principles of being performant, flexible but with good defaults, practice-oriented, simple, explicit, and consistent. Yii3 is not an exception.
While Yii 1.1 and Yii 2.0 are good frameworks, Yii3 was born to improve things even more, addressing the following drawbacks:
In the following we will summarize some of the highlights of this long-awaited release. You may check out the Getting Started section if you want to rush to try it out first.
Compared to Yii 1.1 and Yii 2.0 which were monolithic frameworks, Yii3 is a package ecosystem with more than 130 official packages. These packages could be used in any PHP code or as a whole framework relatively similar to Yii 2.0 or Yii 1.1. Application templates help a lot in this case.
There are three application templates available out of the box:
Unlike Yii 2.0, these are very minimalistic, so you start with basic things such as routing, configuration, DI container, or environment already wired together for you. But, at the same time, additional functionality, such as connecting to a database, isn't in templates by default. Install only what you need—no bloat, just solutions.
Instead of focusing on Yii-specific extensions, as it was with Yii 2.0, we've made the framework to work well with any PHP packages available at Packagist, be they PSR-compatible ones, Symfony packages, or generic PHP code. The container is able to configure any of these.
Yii3 embraces the entire PHP ecosystem instead of reinventing the wheel. It integrates seamlessly with any PHP library. No vendor lock-in. No proprietary APIs. Just modern PHP standards enabling you to leverage the entire ecosystem's innovation.
The central part of the framework is its dependency injection container. It brings all the individual packages together, automatically resolving dependencies and helping to obtain classes requested. Usual usage is very simple and explicit:
return [
// Interface to class mapping
EngineInterface::class => EngineMarkOne::class,
// Detailed configuration
MyServiceInterface::class => [
'class' => MyService::class,
'__construct()' => [42],
'setDiscount()' => [10],
],
// Factory closures
'api' => static fn(ApiConfig $config) => new ApiClient($config),
];
// Dependencies are automatically injected based on type hints
final readonly class MyController
{
public function __construct(
private CacheInterface $cache
) {}
}
Another advantage of the container is that it is very fast and works at runtime. Your configuration is what is actually executed, so there is no magic, and you can even step-debug it with XDebug if needed.
Following the traditional way of configuring things with Yii 2.0, we've extracted what was included into an advanced application supporting multiple environments, config overrides, etc. The configuration in Yii3 is very powerful and could be configured to follow basically any layout of the project if needed. By default, application templates are supplying configuration that is divided into web (or API) and console, also having some common shared configs. The configuration is divided into two main parts there: DI container configuration, which maps configured classes to interfaces, and parameters, which are used for configuring these classes.
'yiisoft/view' => [
'basePath' => null,
'parameters' => [
'assetManager' => Reference::to(AssetManager::class),
'applicationParams' => Reference::to(ApplicationParams::class),
'aliases' => Reference::to(Aliases::class),
'urlGenerator' => Reference::to(UrlGeneratorInterface::class),
'currentRoute' => Reference::to(CurrentRoute::class),
],
],
'yiisoft/yii-view-renderer' => [
'viewPath' => null,
'layout' => '@src/Web/Shared/Layout/Main/layout.php',
'injections' => [
Reference::to(CsrfViewInjection::class),
],
],
Similar to Yii 1.1 and Yii 2.0, we take security seriously. It was considered when implementing every package, such as the presentation layer and database abstraction.
As for features:
There are also helpers for common security-related actions, such as the CSRF-protection package.
Documentation contains a dedicated guide page with established security response processes.
Yii 2.0 had an exceptional database abstraction layer. At first, we extracted it into a separate package and then reimagined and improved it. It resulted in yiisoft/db, which is database access, schema management, and a powerful query builder. Extra tools and storage, such as migrations, database caching, etc., were built on top of DB as well as a more explicit but still rapid Active Record implementation.
$posts = $connection
->select(['id', 'title', 'created_at'])
->from('{{%posts}}')
->where(['status' => 'published'])
->andWhere(['>', 'views', 1000])
->orderBy(['created_at' => SORT_DESC])
->limit(10)
->all();
Other than our own database layer, you can use Cycle ORM or Doctrine, make queries using PDO or native drivers. Whatever you prefer. Yii3 does not force you to stick to a single implementation.
Powerful data abstraction yiisoft/data, and widgets for it, such as grid view, are there and are perfect for creating powerful universal admin panels. The abstraction includes data formatting, pagination, sorting, and an ability to use any data source with any action/presentation and vice versa.
Caching got even more advanced than in Yii 2.0. First of all, out of the box there's a cache stampede protection. Second, cache drivers are PSR-16 compatible. That means you can use any PSR-16 implementation for a very specific storage, and it will work right away.
The following backends were implemented by our team already:
Similar to caching, the framework leverages PSR interfaces for more:
Standards-first architecture ensures your code stays portable and future-proof while maintaining full access to modern PHP tooling.
In traditional PHP servers, framework initialization and database connection are done for every request, which is not great for performance. Yii3 may run in worker mode together with RoadRunner, Swoole, or FrankenPHP. In this mode it initializes once and then serves many request-response cycles, which results in drastically lower response times.
While this mode is great, the state is largely shared between multiple request-response processing, so developers should take care of isolating the state (or not storing it at all) and potential memory leaks. Yii3 helps with it a lot: all the packages were designed either to not use any state or to reset it properly on every start of the request.
Not every project should be an API with a client so everything for classic server-rendered web applications is available: Widgets, Views with template engines support such as Twig, Forms, asset management, HTML helpers, etc.
Additionally, there are ready-to-use widgets for Bootstrap 5 and Bulma.
Yii3 provides many tools that help build APIs:
We plan to add more in the future but the current set is already enough for building powerful APIs.
Yii3 works with the HTTP layer on a more precise level than previous versions of the framework and follows PSR interfaces. On top of these relatively low-level interfaces, there are handy abstractions:
Additionally, network utilities are there to help with the IP protocol.
Yii3 has a powerful validator powered by attributes, the ability to fill forms with data from HTTP (input-http) or elsewhere, tools to work with forms, and more.
<?php
declare(strict_types=1);
namespace App\Web\Echo;
use Yiisoft\FormModel\FormModel;
use Yiisoft\Validator\Label;
use Yiisoft\Validator\Rule\Length;
final class Form extends FormModel
{
#[Label('The message to be echoed')]
#[Length(min: 2)]
public string $message = '';
}
There are various helpers available to simplify common programming tasks.
Since the Yii framework community and the team members are from all over the world, we take internationalization seriously and understand it very well.
Out of the box, Yii3 has:
Proper dependency Inversion makes it easy to unit test the code created with Yii3. Following PSR for request and response allows you to perform API testing without actually running an HTTP server.
To ease testing even more, a set of test-specific PSR implementations are provided by yiisoft/test-support.
We value developers' time. That's why we pay attention to error messages and the error page. All these are exceptionally useful. Messages provide context. We'll never use "Error" as a message. Additionally, we took it to the next level with "friendly exceptions." In development mode these are rendered with the information block that explains why the error happened and how to likely fix it. In the future we'll try to provide a button that will automatically try to apply the fix.
The error page contains the error. For each stack trace level, it displays the source code. The line where the error happened is highlighted. Stacktrace items that are part of the framework packages are collapsed by default because these are tested so much that it's unlikely that the error is there.
The handler itself, yiisoft/error-handler, is even more interesting than the one of Yii2. It handles out-of-memory errors and fatals, can maps certain exception types to custom exceptions, respond in different formats, etc.
Additionally, there is a package to integrate Sentry for error collection.
There are the following resources available:
Yii3 maintains exceptional code quality standards across all packages. Every line undergoes rigorous verification through multiple layers of automated testing and analysis.
For all the packages, we have nearly 100% coverage with tests, strict Psalm/PhpStan types, and a close to 100% mutation score. Every code change is reviewed publicly. All these measures give an exceptionally stable and predictable foundation for your projects.
The release policy is SemVer. Each package is versioned independently. Patch versions never break anything while fixing bugs. The minor version introduces enhancements and features but is still compatible. The major version introduces backwards incompatible changes requiring code updates.
There are many additional features that are less exciting but are very important:
Likely we forgot to mention some parts of the framework, so go ahead and explore.
Below are some useful links about Yii3:
Thank you for your support and patience! We did it together. All the core team members, community contributors, and backers.
We're pretty sure the Yii3 codebase will serve us well in at least the next 10 years or even more.
Future plans are:
Merry Christmas and Happy New Year! Enjoy!
]]>We are very pleased to announce the release of Redis extension version 2.1.0.
This version fixes one PHP 8.4 compatibility issue that was found since last release, adds support for predis, changes yii\redis\Cache::$forceClusterMode to false, and makes yii\redis\Connection implement yii\redis\ConnectionInterface.
See the CHANGELOG for details.
]]>We are very pleased to announce that Yii Framework version 1.1.32 is released. You can download it at yiiframework.com/download/.
Yii 1.1.32 adds support for PHP 8.4 and improves compatibility with PHP 8.
This release is a release of Yii 1.1, which has reached maintenance mode and will only receive security and compatibility fixes. For the complete list of changes in this release, please see the change log. For upgrading, always make sure to read the upgrade instructions.
We would like to express our gratitude to all contributors who have spent their precious time helping improve Yii and made this release possible.
]]>Yii Form Model version 1.1.0 was released. In this version:
populateFromGet() and populateFromGetAndValidate() to FormHydratorYii3 API project template version 1.1.0 was released. In this version:
stop goal for stopping Docker containers (@samdark,make prod-deployCycle ORM query adapter for yiisoft/data version 1.0.0 was released.
There package provides Cycle ORM query adapter and writer for Yii Data.
See package documentation for usage.
]]>Yii3 web application template version 1.1.0 was released. In this version:
stop goal for stopping Docker containerssrc/ directory<meta http-equiv="X-UA-Compatible" content="IE=edge"> from layoutmake prod-deployYii RBAC Database storage 2.1.0
Yii RBAC DB version 2.1.0 was released. In this version:
composer.json to 8.1 - 8.5AssignmentStorage::filterUserItemNames() methodyiisoft/rbac version to ^2.1ItemTreeTraversalFactory as finalYii Event version 2.2.0 was released. In this version:
composer.json to 8.1 - 8.5CallableFactory and ListenerCollectionFactory^8.1 and refactor codeYii Cache DB Handler version 1.1.0 was released. In this version:
yiisoft/db to ^2.08.1 - 8.5DbSchemaManager::$db property as readonlyYii Logging Library version 2.2.0 was released. In this version:
$levels parameter to Target constructor allowing log level filtering at instantiationext-psr to conflict section in composer.jsonAll targets were updated as well to allow $levels in constructor. Additionally:
categories, except and exportInterval setters to default config and replaced rotate by rename to rotate by copy.syslog() returned false.The first stable release of a powerful set of data displaying widgets is now available.
Yii DataView provides three flexible widgets for presenting data in web applications:
ListView — display data as a customizable list.
GridView — present data in a feature-rich grid with sorting, filtering, and pagination.
DetailView — show detailed information about a single record.
Data Source Independence. Works seamlessly with database, CSV, REST API, or any data source through the Yii Data package.
Built-in Pagination. Handle large datasets efficiently with integrated pagination controls.
Internationalization. Full translation support for multilingual applications.
Theming Support. Customize appearance to match your application design.
Flexible Column Types.
DataColumn with custom value presenters, ActionColumn, etc.
Modern PHP. Built for PHP 8.1 - 8.5 with type safety and modern best practices.
]]>Yii Data DB, a database query adapter for Yii Data, is now available.
This package provides a data reader implementation that bridges Yii DB with the Yii Data reading interface, making it easy to work with database queries in a flexible and consistent way.
QueryDataReader. Wraps database queries to provide pagination, sorting, and filtering capabilities.
Field Mapping. Map data reader field names to database columns for clean API design.
Batch Processing. Process large datasets efficiently with configurable batch sizes.
Multi-Database Support. Tested with SQLite, MySQL, PostgreSQL, Microsoft SQL Server, and Oracle.
Yii Data 2 is now available, bringing major improvements and modernization to this package that provides generic data abstractions for reading, writing, and processing.
PHP 8.1+ required: modernized codebase with readonly properties and improved type safety.
Enhanced filtering: added Stringable support, nested value filtering, case-sensitive Like filter with matching modes, and new All/None filters.
Improved pagination: new LimitableDataInterface, nextPage()/previousPage() methods, PageToken class, and better limit handling in paginators.
Better developer experience: comprehensive Psalm annotations, PageNotFoundException for clearer error handling, and OrderHelper for low-level order operations.
For a complete list of changes, see the CHANGELOG.md.
]]>Yii Middleware Dispatcher version 5.4.0 was released. In this version:
Yii HTML version 3.12.0 was released. In this version:
Tag::addStyle() and Tag::removeStyle() methodsColor class, Html::color() and Input::color() methodsRadioList::renderUncheckInput() and CheckboxList::renderUncheckInput() methodsYii Caching Library version 3.2.0 was released. In this version:
Ttl value object for working with time-to-live durationA Yii2 extension/widget that provides a simple and intuitive interface to define and manage recurring date patterns. It is designed to simplify the configuration of renewals, expirations, and periodic events, with clean integration into Yii2 forms and internationalization support.
previous | next).Install the extension with Composer:
composer require davidrnk/yii2-recurring-date
Register the asset (if the widget does not do it automatically) and add the widget to your views/forms according to the usage examples.
The extension can be used with Yii2 models (ActiveForm) or independently.
Usage with model (ActiveForm):
use davidrnk\RecurringDate\Widget\RecurringDate;
echo $form->field($model, 'recurrence_config')->widget(RecurringDate::class, [
// options
'options' => ['class' => 'form-control my-custom-class'],
'labels' => [
'title_modal' => 'Configure recurrence',
// you can override other labels
],
]);
Usage without model:
echo davidrnk\RecurringDate\Widget\RecurringDate::widget([
'name' => 'recurrence',
'value' => json_encode(['type' => 'monthly', 'day' => 1]),
'options' => ['class' => 'form-control'],
]);
The widget renders a read-only text control with a button to open a modal where recurrence is configured. The resulting JSON is saved in a hidden field (input.hidden) and is the content you should store in the database.
The widget persists the configuration in JSON format with the following main structure (examples):
{"type": "no_expiration"}
{ "type": "interval", "value": 10, "unit": "days" }
{ "type": "monthly", "day": 31, "adjust": "previous" }
{ "type": "yearly", "day": 29, "month": 2, "adjust": "previous" }
{ "type": "specific_date", "date": "2025-12-31" }
Relevant keys:
type: one of no_expiration, interval, monthly, yearly, specific_date.value, unit: used by interval (unit: days|months|years).day: day of the month (1-31).month: month (1-12).date: ISO date for specific_date.adjust: policy when the day does not exist in the period (values: previous — adjust to the last valid day of the month, or next — move to the next day).In the backend, the library provides a function to calculate the resulting date based on a base date and the JSON configuration. In the code the function is called:
use davidrnk\RecurringDate\Core\RecurringDateEngine;
$nextDueDate = RecurringDateEngine::calculateExpiration($startDate, $configArray);
// returns DateTime instance or null if configuration is invalid
In the documentation and examples of this README, we refer to this date as nextDueDate. If calculateExpiration returns null, the combination of parameters is invalid or could not be calculated.
Quick example:
$start = new \DateTime('2025-01-31');
$cfg = ['type' => 'monthly', 'day' => 31, 'adjust' => 'previous'];
$next = RecurringDateEngine::calculateExpiration($start, $cfg);
echo $next ? $next->format('Y-m-d') : 'invalid';
The widget exposes several ways to adjust its visual and textual behavior:
options (array): HTML attributes for the visible text field (e.g., class, style, placeholder).labels (array): you can override texts and labels used in the modal. Examples of keys you can customize:title_modal, type, configure, preview, save, cancel, quantity, unit, month_day, adjust, adjust_previous, adjust_next, etc.src/messages/en and src/messages/es. The displayed texts are also sent to JavaScript for preview.Example of label customization:
echo $form->field($model, 'recurrence_config')->widget(RecurringDate::class, [
'labels' => [
'title_modal' => 'Schedule repetition',
'adjust_previous' => 'Adjust to the last day of the month',
],
]);
adjust policy.adjust value is persisted in JSON and is considered by RecurringDateEngine::calculateExpiration.The default language of the extension is English. Translations are included in src/messages/en and src/messages/es. Strings used in views and JavaScript translations are defined and loaded from RecurringDate::getJSTranslations().
If you need to add another language, add a file in src/messages/XX/davidrnk.recurring.php with the required keys.
Unit tests for PHP are included for the calculation logic (tests/RecurringDateEngineTest.php) and should be executed with:
vendor/bin/phpunit tests/RecurringDateEngineTest.php
recurrence_config), and use RecurringDateEngine::calculateExpiration to obtain the next expiration date when needed.adjust policy for your domain (by default the extension uses previous — clamp to the last valid day). This avoids surprises when calculating next dates.Yii::$app->language) to ensure the UI displays the desired translations.Pull requests and issues are welcome. For major changes, first open an issue describing the proposed change.
BSD-3-Clause — see LICENSE file.
Yii2 - Modern Starter Kit is a modern, full-featured Yii 2 application template with React frontend powered by Inertia.js.
The template includes a beautiful UI built with Shadcn UI components, dark/light theme support, user authentication, CRUD operations, and all the modern features you need to rapidly build web applications.
#### Sign In
#### Sign Up
#### Dashboard
#### Users Management
#### Settings
#### Forgot Password
#### Sign In
#### Sign Up
#### Dashboard
#### Users Management
#### Settings
#### Forgot Password
Before you begin, ensure you have the following installed on your system:
git clone [email protected]:crenspire/yii2-react-starter.git
cd yii2-react-starter
Or download and extract the project archive to your desired directory.
Install all PHP dependencies using Composer:
composer install
This will install all required PHP packages including Yii2 framework and Inertia.js adapter.
Install all frontend dependencies:
npm install
This will install React, Inertia.js, Shadcn UI components, Tailwind CSS, and all other frontend dependencies.
CREATE DATABASE yii2basic CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
config/db.php:return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2basic',
'username' => 'root',
'password' => 'your_password',
'charset' => 'utf8mb4',
];
Replace your_password with your MySQL root password (or your database user credentials).
Run the migrations to create all necessary database tables:
php yii migrate
This will create the following tables:
users - User accounts with soft deletespassword_reset_tokens - Password reset functionalityCreate an admin user for testing:
php yii seed/admin
This creates an admin user with:
[email protected]admin123The cookie validation key should be automatically generated during composer install. If it wasn't, you can generate it manually:
config/web.phpcookieValidationKey in the request component'request' => [
'cookieValidationKey' => 'your-random-32-character-string-here',
],
You can generate a random string using:
php -r "echo bin2hex(random_bytes(16));"
You need to run two servers simultaneously:
PHP Development Server (Backend):
`bash
php yii serve
`
Vite Development Server (Frontend):
`bash
npm run dev
`
Or use concurrently to run both at once (if installed):
npx concurrently "php yii serve" "npm run dev"
Open your browser and navigate to:
http://localhost:8080
You should see the home page. You can now:
/auth/register/auth/login (or use admin credentials if you seeded)/dashboard (requires login)For production deployment, build the frontend assets:
npm run build
This will compile and optimize all React components and assets into the web/dist directory.
The application uses Yii2's environment configuration. You can set the environment by:
config/params.php and modifying as neededYII_ENV constant in web/index.php:YII_ENV_DEV - Development modeYII_ENV_PROD - Production modeconfig/web.php - Web application configurationconfig/console.php - Console application configurationconfig/db.php - Database configurationconfig/params.php - Application parametersNOTES:
runtime/ and web/assets/ directories are writable by the web serverconfig/web.phpweb/ directory as the document rootassets/ contains assets definition
commands/ contains console commands (controllers)
config/ contains application configurations
controllers/ contains Web controller classes
mail/ contains view files for e-mails
models/ contains model classes
migrations/ contains database migrations
resources/ contains frontend resources (React, CSS, JS)
js/ React components and pages
css/ Stylesheets
runtime/ contains files generated during runtime
tests/ contains various tests for the basic application
vendor/ contains dependent 3rd-party packages
views/ contains view files for the Web application
web/ contains the entry script and Web resources
images/ contains screenshots and images
dist/ contains built frontend assets (production)
Tests are located in tests directory. They are developed with Codeception PHP Testing Framework.
By default, there are 3 test suites:
unitfunctionalacceptanceTests can be executed by running
vendor/bin/codecept run
The command above will execute unit and functional tests. Unit tests are testing the system components, while functional tests are for testing user interaction. Acceptance tests are disabled by default as they require additional setup since they perform testing in real browser.
To execute acceptance tests do the following:
Rename tests/acceptance.suite.yml.example to tests/acceptance.suite.yml to enable suite configuration
Replace codeception/base package in composer.json with codeception/codeception to install full-featured
version of Codeception
Update dependencies with Composer
composer update
Download Selenium Server and launch it:
java -jar ~/selenium-server-standalone-x.xx.x.jar
In case of using Selenium Server 3.0 with Firefox browser since v48 or Google Chrome since v53 you must download GeckoDriver or ChromeDriver and launch Selenium with it:
`
# for Firefox
java -jar -Dwebdriver.gecko.driver=~/geckodriver ~/selenium-server-standalone-3.xx.x.jar
# for Google Chrome
java -jar -Dwebdriver.chrome.driver=~/chromedriver ~/selenium-server-standalone-3.xx.x.jar
`
As an alternative way you can use already configured Docker container with older versions of Selenium and Firefox:
docker run --net=host selenium/standalone-firefox:2.53.0
(Optional) Create yii2basic_test database and update it by applying migrations if you have them.
tests/bin/yii migrate
The database configuration can be found at config/test_db.php.
Start web server:
tests/bin/yii serve
Now you can run all available tests
# run all available tests
vendor/bin/codecept run
# run acceptance tests
vendor/bin/codecept run acceptance
# run only unit and functional tests
vendor/bin/codecept run unit,functional
By default, code coverage is disabled in codeception.yml configuration file, you should uncomment needed rows to be able
to collect code coverage. You can run your tests and collect coverage with the following command:
#collect coverage for all tests
vendor/bin/codecept run --coverage --coverage-html --coverage-xml
#collect coverage only for unit tests
vendor/bin/codecept run unit --coverage --coverage-html --coverage-xml
#collect coverage for unit and functional tests
vendor/bin/codecept run functional,unit --coverage --coverage-html --coverage-xml
You can see code coverage output under the tests/_output directory.
Inertia.js is a modern approach to building single-page applications (SPAs) without the complexity of building an API. It allows you to use modern JavaScript frameworks like React, Vue, or Svelte while keeping your Yii2 controllers and routing intact.
Why Inertia.js?
The crenspire/yii2-inertia package provides a seamless Inertia.js adapter for Yii2, matching the developer experience of popular frameworks like Laravel's Inertia adapter.
Install via Composer:
composer require crenspire/yii2-inertia
Add to your config/web.php:
'view' => [
'renderers' => [
'inertia' => \Crenspire\Yii2Inertia\ViewRenderer::class,
],
],
Create views/layouts/inertia.php:
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= Html::encode($this->title) ?></title>
<script type="module" src="/dist/assets/index.js"></script>
<link rel="stylesheet" href="/dist/assets/index.css">
</head>
<body>
<div id="app" data-page="<?= htmlspecialchars(json_encode($page), ENT_QUOTES, 'UTF-8') ?>"></div>
</body>
</html>
use Crenspire\Yii2Inertia\Inertia;
class HomeController extends \yii\web\Controller
{
public function actionIndex()
{
return Inertia::render('Home', [
'title' => 'Welcome',
'users' => User::find()->all(),
]);
}
}
Install Inertia.js and your framework:
# For React
npm install @inertiajs/inertia @inertiajs/inertia-react react react-dom
# For Vue
npm install @inertiajs/inertia @inertiajs/inertia-vue3 vue
# For Svelte
npm install @inertiajs/inertia @inertiajs/inertia-svelte svelte
Create src/main.jsx (React example):
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createInertiaApp } from '@inertiajs/inertia-react';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
createInertiaApp({
resolve: (name) => {
const pages = { Home, Dashboard };
return pages[name];
},
setup({ el, App, props }) {
ReactDOM.createRoot(el).render(<App {...props} />);
},
});
Share data that should be available on every page:
// In config/bootstrap.php or base controller
use Crenspire\Yii2Inertia\Inertia;
// Share user data
Inertia::share('user', function () {
return Yii::$app->user->identity;
});
// Share flash messages
Inertia::share('flash', function () {
return [
'success' => Yii::$app->session->getFlash('success'),
'error' => Yii::$app->session->getFlash('error'),
];
});
// Share multiple values
Inertia::share([
'appName' => 'My Application',
'version' => '1.0.0',
]);
Tip: Use closures for dynamic data (like user or flash messages) to ensure fresh data on each request.
For form submissions and other redirects, use Inertia::location():
public function actionStore()
{
$model = new User();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
Yii::$app->session->setFlash('success', 'User created!');
return Inertia::location('/users');
}
return Inertia::render('Users/Create', [
'errors' => $model->errors,
]);
}
The method automatically handles both Inertia requests (409 status) and regular requests (302 redirect).
Set up versioning for cache busting:
// Automatic (uses manifest.json mtime if exists)
$version = Inertia::version();
// Manual string
Inertia::version('1.0.0');
// Callback (recommended)
Inertia::version(function () {
return filemtime(Yii::getAlias('@webroot/dist/manifest.json'));
});
Optimize performance by only reloading specific props:
// Client requests only 'users' and 'stats' props
return Inertia::render('Dashboard', [
'users' => $users, // Included
'stats' => $stats, // Included
'other' => $other, // Excluded
]);
In your frontend component:
import { router } from '@inertiajs/inertia-react';
const refreshStats = () => {
router.reload({ only: ['stats'] });
};
Structure your frontend to match backend routes:
src/pages/
Home.jsx
Dashboard.jsx
Users/
Index.jsx
Show.jsx
Edit.jsx
// Maps to src/pages/Users/Index.jsx
return Inertia::render('Users/Index', ['users' => $users]);
Use Inertia's form helper for better UX:
import { useForm } from '@inertiajs/inertia-react';
export default function CreateUser() {
const { data, setData, post, processing, errors } = useForm({
name: '',
email: '',
});
const submit = (e) => {
e.preventDefault();
post('/users');
};
return (
<form onSubmit={submit}>
<input
value={data.name}
onChange={(e) => setData('name', e.target.value)}
/>
{errors.name && <div>{errors.name}</div>}
<button disabled={processing}>Submit</button>
</form>
);
}
Return validation errors from your controller:
public function actionStore()
{
$model = new User();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return Inertia::location('/users');
}
// Return with errors
return Inertia::render('Users/Create', [
'errors' => $model->errors,
'values' => $model->attributes,
]);
}
Create a reusable flash message component:
// Share flash messages globally
Inertia::share('flash', function () {
return [
'success' => Yii::$app->session->getFlash('success'),
'error' => Yii::$app->session->getFlash('error'),
];
});
// FlashMessage.jsx
import { usePage } from '@inertiajs/inertia-react';
export default function FlashMessage() {
const { flash } = usePage().props;
if (!flash) return null;
return (
<div>
{flash.success && <div className="alert-success">{flash.success}</div>}
{flash.error && <div className="alert-error">{flash.error}</div>}
</div>
);
}
Handle pagination with Yii2's DataProvider:
use yii\data\ActiveDataProvider;
public function actionIndex()
{
$dataProvider = new ActiveDataProvider([
'query' => User::find(),
'pagination' => ['pageSize' => 15],
]);
return Inertia::render('Users/Index', [
'users' => $dataProvider->getModels(),
'pagination' => [
'currentPage' => $dataProvider->pagination->page + 1,
'lastPage' => $dataProvider->pagination->pageCount,
'perPage' => $dataProvider->pagination->pageSize,
'total' => $dataProvider->totalCount,
],
]);
}
Use eager loading to prevent N+1 queries:
$users = User::find()
->with('posts', 'comments')
->all();
Only send necessary data:
// Instead of full models
return Inertia::render('Users/Index', [
'users' => User::find()
->select(['id', 'name', 'email'])
->asArray()
->all(),
]);
Create a base controller for shared functionality:
namespace app\controllers;
use Crenspire\Yii2Inertia\Inertia;
use yii\web\Controller;
abstract class BaseController extends Controller
{
public function init()
{
parent::init();
Inertia::share('user', function () {
return Yii::$app->user->identity;
});
}
protected function inertiaRender($component, $props = [])
{
return Inertia::render($component, $props);
}
}
class UsersController extends BaseController
{
public function actionIndex()
{
$users = User::find()->all();
return $this->inertiaRender('Users/Index', ['users' => $users]);
}
public function actionShow($id)
{
$user = User::findOne($id);
if (!$user) {
throw new NotFoundHttpException('User not found');
}
return $this->inertiaRender('Users/Show', ['user' => $user]);
}
public function actionStore()
{
$model = new User();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
Yii::$app->session->setFlash('success', 'User created!');
return Inertia::location(['users/show', 'id' => $model->id]);
}
return $this->inertiaRender('Users/Create', [
'errors' => $model->errors,
]);
}
}
If you're experiencing frequent full page reloads:
manifest.json exists and is readableInertia::version(function () {
$manifest = Yii::getAlias('@webroot/dist/manifest.json');
return file_exists($manifest) ? (string)filemtime($manifest) : '1';
});
Always use Inertia::location() instead of Yii::$app->response->redirect() for Inertia requests.
usePage() hook correctly:import { usePage } from '@inertiajs/inertia-react';
const { props } = usePage();
const { title, user } = props;
Share CSRF token globally:
Inertia::share('csrfToken', function () {
return Yii::$app->request->csrfToken;
});
Use in forms:
<input type="hidden" name="_token" value={props.csrfToken} />
examples/basic directory in the repositoryInertia.js with Yii2 provides a powerful way to build modern SPAs while keeping the simplicity and power of server-side routing and validation. The crenspire/yii2-inertia package makes it easy to integrate Inertia.js into your Yii2 applications with a familiar API.
Give it a try and let us know what you think! For issues, questions, or contributions, please visit the GitHub repository.
]]>We are pleased to announce the release of Yii Framework version 2.0.54.
Please refer to the instructions at https://www.yiiframework.com/download/ to install or upgrade to this version.
In this release:
BaseStringHelper::convertIniSizeToBytes().Thanks to all Yii community members who contribute to the framework, translators who keep documentation translations up to date, and community members who answer questions at forums.
Special thanks goes to Maksim Spirkov who took care of the majority of annotation-related changes.
There are many active Yii communities, so if you need help or want to share your experience, feel free to join them.
A complete list of changes can be found in the CHANGELOG.
]]>High-resolution cron-like job scheduler for Yii2 supporting:
{pid, host, ts}composer require ldkafka/yii2-scheduler
console/config/main.php):return [
'bootstrap' => [
// ensure the component can bootstrap its controller
'scheduler',
],
'components' => [
'cache' => [ /* your cache config */ ],
'queue_scheduler' => [ /* your yii2-queue config */ ],
'scheduler' => [
'class' => ldkafka\scheduler\Scheduler::class,
'config' => [
'cache' => 'cache', // optional; default 'cache' (uses Yii::$app->cache)
'queue' => 'queue_scheduler', // optional; omit to run inline synchronously
],
'jobs' => require __DIR__ . '/scheduler.php',
],
],
];
console/config/scheduler.php with your jobs:<?php
use ldkafka\scheduler\ScheduledJob;
return [
[
'class' => \common\jobs\ExampleJob::class, // must extend ScheduledJob
'run' => 'EVERY_MINUTE',
'single_instance' => true, // default true
'max_running_time' => 300, // seconds; 0 = unlimited
],
[
'class' => \common\jobs\NightlyJob::class,
'run' => [
'minutes' => 0,
'hours' => 2,
'wday' => '1-5', // Mon-Fri
],
],
];
php yii scheduler/run
php yii scheduler/daemon
{pid, host, ts} instead of bare PID. No migration needed; old locks will expire naturally (1-hour TTL).staleLockTtl and maxJobsPerTick from your scheduler config (not implemented).day key is now normalized to mday. Update job configs using day to use mday instead for clarity.EVERY_MINUTE, EVERY_HOUR, EVERY_DAY, EVERY_WEEK, EVERY_MONTHgetdate() keys: minutes, hours, mday (day of month), wday (weekday), mon, year* - wildcard (matches any value)5 - exact match*/5 - step/interval (every 5th unit: 0, 5, 10...)1-5 - range (1 through 5 inclusive)10-20/2 - range with step (10, 12, 14, 16, 18, 20)1,3,5 - list (matches 1 or 3 or 5)namespace common\jobs;
use ldkafka\scheduler\ScheduledJob;
use Yii;
class ExampleJob extends ScheduledJob
{
public function execute($queue = null)
{
Yii::info('ExampleJob executed', 'scheduler');
// do work
return true; // success
}
}
Notes:
{pid: int, host: string, ts: int} and are acquired atomically with cache->add().max_running_time are auto-removed from the run cache; if the queue driver supports remove(), the queued job is also removed.max_running_time is enforced during scheduler ticks; consider queueing long-running jobs for better concurrency.All logs use the scheduler category with production-appropriate levels:
Configure a log target in your console/config/main.php if needed:
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning', 'info'],
'categories' => ['scheduler'],
'logFile' => '@runtime/logs/scheduler.log',
'maxFileSize' => 10240, // 10 MB
],
],
],
The scheduler triggers events throughout job lifecycle for integration with monitoring systems. Attach event handlers in your scheduler configuration:
'scheduler' => [
'class' => ldkafka\scheduler\Scheduler::class,
'on jobBeforeRun' => function ($event) {
// $event is SchedulerJobEvent with: job_class, job_config, start_time
Yii::info("Starting job: {$event->job_class}", 'monitoring');
},
'on jobAfterRun' => function ($event) {
// $event includes: result, start_time, end_time
$duration = $event->end_time - $event->start_time;
Yii::info("Job {$event->job_class} completed in {$duration}s", 'monitoring');
},
'on jobError' => function ($event) {
// $event includes: error, exception, trace, error_time
Yii::error("Job {$event->job_class} failed: {$event->error}", 'monitoring');
},
'on jobBlocked' => function ($event) {
// $event->data includes: job_id, job_config, reason, running_time
$data = $event->data;
Yii::warning("Job {$data['job_config']['class']} blocked: {$data['reason']}", 'monitoring');
},
'config' => [ /* ... */ ],
'jobs' => [ /* ... */ ],
],
jobBeforeRun): Job is about to executejobAfterRun): Job completed successfullyjobError): Job threw exceptionjobBlocked): Job prevented from running (e.g., single-instance lock)jobTimeout): Reserved for future useAll events use SchedulerJobEvent class with typed properties for monitoring integration.
Scheduler::finalizeRuntimeJob() to remove the job entry from the persistent run cache.pcntl for graceful signal handling (SIGINT/SIGTERM). On platforms without pcntl, the daemon stops when too many ticks are missed (configurable).BSD-3-Clause
]]>Native, strongly-typed Yii2 component for the Google Gemini REST API. No external SDKs – only yii\\httpclient. Provides generation (text & multimodal), streaming, embeddings, Files API, token counting, and flexible caching strategies.
final class, consistent helper methods| Area | Capabilities |
|---|---|
| Generation | Text, multimodal (image/audio/video/document via inline or file references) |
| Streaming | SSE incremental output with user callback |
| Caching | none, client (Yii cache history), server (Gemini CachedContent) |
| Embeddings | Single + batch embeddings for RAG / similarity |
| Files | Upload, list, get, delete (simplified direct PUT) |
| Models | Enumerate models, inspect limits/capabilities |
| Tokens | Pre-flight token counting for cost estimation |
| Helpers | extractText, getFinishReason, getUsageMetadata |
composer require ldkafka/yii2-google-gemini
'components' => [
'gemini' => [
'class' => 'ldkafka\gemini\Gemini',
'apiKey' => 'YOUR_GEMINI_API_KEY',
'generationConfig' => [
'temperature' => 0.7,
'topP' => 0.95,
'maxOutputTokens' => 2048,
],
],
],
$gemini = Yii::$app->gemini;
$resp = $gemini->generateContent('gemini-2.5-flash', 'Explain quantum computing');
if ($resp['ok']) {
echo $gemini->extractText($resp['data']);
}
$resp = $gemini->generateContent('gemini-2.5-flash', 'What is PHP?');
if ($resp['ok']) {
$text = $gemini->extractText($resp['data']);
$usage = $gemini->getUsageMetadata($resp['data']);
echo "Response: $text\n";
echo "Tokens used: {$usage['totalTokenCount']}\n";
}
$gemini->streamGenerateContent('gemini-2.5-flash', 'Write a short story', function($chunk) {
if ($text = $chunk['candidates'][0]['content']['parts'][0]['text'] ?? null) {
echo $text;
flush();
}
});
$content = [[
'parts' => [
['text' => 'What is in this image?'],
['inline_data' => [
'mime_type' => 'image/jpeg',
'data' => base64_encode(file_get_contents('/path/to/image.jpg'))
]]
],
'role' => 'user'
]];
$resp = $gemini->generateContent('gemini-2.5-flash', $content);
$gemini->cacheType = 'client';
$gemini->cacheTtl = 3600;
// First message
$resp = $gemini->chat('gemini-2.5-flash', 'My name is Alice', 'user123');
// Follow-up (remembers context)
$resp = $gemini->chat('gemini-2.5-flash', 'What is my name?', 'user123');
// Response: "Your name is Alice."
$gemini->cacheType = 'server';
// Create cache with system instruction (requires 32k+ tokens)
$cacheName = $gemini->createServerCache(
'gemini-2.5-flash',
'assistant-id',
'You are a helpful travel assistant. [... long system instruction ...]',
3600
);
// Use cached context
$resp = $gemini->chatServer('gemini-2.5-flash', 'Best beaches in Sydney?', 'assistant-id');
$gemini->systemInstruction = [
'parts' => [['text' => 'You are a helpful coding assistant who explains concepts simply.']]
];
$resp = $gemini->generateContent('gemini-2.5-flash', 'Explain recursion');
// Upload a large video file
$resp = $gemini->uploadFile('/path/to/video.mp4', 'My Video', 'video/mp4');
$fileUri = $resp['data']['file']['uri'];
// Use in generation
$content = [[
'parts' => [
['text' => 'Summarize this video'],
['file_data' => [
'file_uri' => $fileUri,
'mime_type' => 'video/mp4'
]]
]
]];
$resp = $gemini->generateContent('gemini-2.5-flash', $content);
// List uploaded files
$files = $gemini->listFiles();
// Delete file
$gemini->deleteFile('files/abc123');
// Single embedding
$resp = $gemini->embedContent(
'text-embedding-004',
'Hello world',
'RETRIEVAL_QUERY'
);
$embedding = $resp['data']['embedding']['values'];
// Batch embeddings
$requests = [
[
'content' => ['parts' => [['text' => 'Document 1']]],
'taskType' => 'RETRIEVAL_DOCUMENT'
],
[
'content' => ['parts' => [['text' => 'Document 2']]],
'taskType' => 'RETRIEVAL_DOCUMENT'
],
];
$resp = $gemini->batchEmbedContents('text-embedding-004', $requests);
$tokens = $gemini->countTokens('gemini-2.5-flash', 'Your prompt text here');
echo "This prompt will use approximately $tokens tokens\n";
// List all available models
$models = $gemini->listModels();
foreach ($models['data']['models'] as $model) {
echo "{$model['name']}: {$model['displayName']}\n";
}
// Get specific model details
$model = $gemini->getModel('gemini-2.5-flash');
echo "Context window: {$model['data']['inputTokenLimit']} tokens\n";
| Mode | Purpose | Storage | Pros | Cons |
|---|---|---|---|---|
none | Stateless requests | None | Simplicity | No memory of prior turns |
client | Short/medium chats | Yii cache (gem_chat_<id>) | Fast, light, adjustable TTL | History grows; prune for very long sessions |
server | Large domain context | Gemini CachedContent | Huge reusable context on provider side | Requires ~32K+ tokens; creation often fails if too small |
Server cache creation requires a very large system instruction document. Use countTokens() before attempting createServerCache().
Example test commands in console/controllers/TestController.php:
# Stateless generation
php yii test/gemini-none "What is the capital of France?"
# Client-side caching (conversation)
php yii test/gemini-client
# Server-side caching
php yii test/gemini-server "Tell me about Sydney beaches"
# Clear cache
php yii test/gemini-client test-chat 1
| Property | Type | Default | Description |
|---|---|---|---|
apiKey | string | required | Your Gemini API key (Get one) |
baseUrl | string | https://generativelanguage.googleapis.com/v1/ | API base URL |
generationConfig | array | [] | Default generation parameters |
safetySettings | array | [] | Content safety filters |
systemInstruction | array|null | null | Default system instruction |
cacheType | string | 'none' | Cache mode: 'none', 'client', 'server' |
cacheTtl | int | 3600 | Cache TTL in seconds |
cacheComponent | string|null | 'cache' | Yii cache component name |
httpConfig | array | [] | Custom HTTP client configuration |
'generationConfig' => [
'temperature' => 0.7, // 0.0-2.0, creativity level
'topP' => 0.95, // 0.0-1.0, nucleus sampling
'topK' => 40, // Token selection limit
'maxOutputTokens' => 2048, // Max response length
'stopSequences' => ['END'], // Stop generation triggers
'candidateCount' => 1, // Number of responses
]
| Model | Description | Context Window |
|---|---|---|
gemini-2.5-pro | Most powerful thinking model | 2M tokens |
gemini-2.5-flash | Balanced, fast, 1M context | 1M tokens |
gemini-2.5-flash-lite | Fastest, cost-efficient | 1M tokens |
text-embedding-004 | Text embeddings for RAG | N/A |
See Gemini Models Documentation for full list.
All methods return:
[
'ok' => true|false, // Success status
'status' => 200, // HTTP status code
'data' => [...], // Response data
'error' => null|string // Error message if failed
]
// Extract text from response
$text = $gemini->extractText($resp['data']);
// Get finish reason ('STOP', 'MAX_TOKENS', 'SAFETY', etc.)
$reason = $gemini->getFinishReason($resp['data']);
// Get usage metadata
$usage = $gemini->getUsageMetadata($resp['data']);
// ['promptTokenCount' => 10, 'candidatesTokenCount' => 50, 'totalTokenCount' => 60]
$text = $gemini->extractText($resp['data']);
$reason = $gemini->getFinishReason($resp['data']);
$usage = $gemini->getUsageMetadata($resp['data']);
$gemini->cacheType = 'none';
$resp = $gemini->generateContent('gemini-2.5-flash', 'Hello');
// Each request is independent
$gemini->cacheType = 'client';
$resp = $gemini->chat('gemini-2.5-flash', 'My name is Bob', 'user123');
$resp = $gemini->chat('gemini-2.5-flash', 'What is my name?', 'user123');
// Conversation stored in Yii cache component
$gemini->cacheType = 'server';
$cacheName = $gemini->createServerCache(
'gemini-2.5-flash',
'id',
'[Large system instruction 32k+ tokens]',
3600
);
$resp = $gemini->chatServer('gemini-2.5-flash', 'Question', 'id');
// System instruction cached on Google's servers
Note: Server caching requires minimum 32,000 tokens in cached content.
$resp = $gemini->generateContent('gemini-2.5-flash', 'Hello');
if (!$resp['ok']) {
Yii::error("Gemini API error: {$resp['error']} (HTTP {$resp['status']})");
// Common error codes:
// 400 - Bad request (invalid parameters)
// 401 - Invalid API key
// 429 - Rate limit exceeded
// 500 - Server error
}
'gemini' => [
'class' => 'ldkafka\gemini\Gemini',
'apiKey' => 'YOUR_KEY',
'httpConfig' => [
'timeout' => 60,
'transport' => 'yii\httpclient\CurlTransport',
],
],
$content = [[
'parts' => [
['text' => 'Compare these images'],
['inline_data' => ['mime_type' => 'image/jpeg', 'data' => base64_encode($image1)]],
['inline_data' => ['mime_type' => 'image/jpeg', 'data' => base64_encode($image2)]],
]
]];
$gemini->safetySettings = [
['category' => 'HARM_CATEGORY_HARASSMENT', 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE'],
['category' => 'HARM_CATEGORY_HATE_SPEECH', 'threshold' => 'BLOCK_ONLY_HIGH'],
];
safetySettings lets you tell Gemini which kinds of harmful content to filter and at what strictness. The value is an array of objects with a category and a threshold.
HARM_CATEGORY_HARASSMENT, HARM_CATEGORY_HATE_SPEECH, HARM_CATEGORY_SEXUALLY_EXPLICIT, HARM_CATEGORY_DANGEROUS_CONTENT, HARM_CATEGORY_VIOLENCE.BLOCK_NONE, BLOCK_LOW_AND_ABOVE, BLOCK_MEDIUM_AND_ABOVE, BLOCK_ONLY_HIGH.Example JSON payload as sent to the API:
[
{ "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE" },
{ "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_ONLY_HIGH" }
]
Notes:
safetySettings is empty, no custom filters are applied (provider defaults may still apply).The package includes comprehensive test actions:
Ensure your API key is set in the component configuration or params.
Server caching requires:
Use client-side caching for shorter conversations.
Ensure your HTTP client supports Server-Sent Events (SSE). The default Yii2 HTTP client may need custom transport configuration.
429 & transient 5xx responses.countTokens().Yii::info([...], 'gemini') for observability.BSD-3-Clause (matches class header).
Open issues or PRs at: https://github.com/ldkafka/yii2-google-gemini
When reporting an issue, include:
Enjoy building with Gemini! Suggestions & improvements welcome.
]]>A reusable Yii2 widget that provides a searchable dropdown list with support for dependent (cascading) dropdowns.
It is designed to work seamlessly within the wbraganca/yii2-dynamicform widget and has no dependency on any specific CSS framework like Bootstrap.
Note: This package uses the
rft\searchabledepdrop\widgetsnamespace. Make sure to update your imports if you're upgrading from an older version.
wbraganca/yii2-dynamicform for creating dynamic forms.The preferred way to install this extension is through Composer.
composer require rft/yii2-searchable-depdrop
Yii2 will automatically load the widget via Composer’s autoloader.
If you don't want to use Composer, you can still install it manually:
src/ directorycommon/widgets/searchable_dep_drop/)src/assets/ directoryFor dependent dropdowns, you need a controller action that returns data in JSON format.
The widget expects the parent value as a POST parameter (the name of the parameter is derived from the parent field's name).
The action should return a JSON object with an output key, which is an array of objects, each having an id and text property.
Example Controller Action:
public function actionListCities()
{
\Yii::\$app->response->format = \yii\web\Response::FORMAT_JSON;
$out = ['output' => [], 'selected' => ''];
if (Yii::\$app->request->post('state')) {
$state = Yii::\$app->request->post('state');
if ($state) {
$cities = AddressCity::find()
->where(['state' => $state])
->orderBy('name')
->all();
$output = [];
foreach ($cities as $city) {
$output[] = ['id' => $city->id, 'text' => $city->name];
}
$out['output'] = $output;
}
}
return $out;
}
In your view file, you can use the widget like any other Yii2 input widget.
A. Standalone Searchable Dropdown
use rft\searchabledepdrop\widgets\SearchableDepDrop;
echo $form->field($model, 'state')->widget(SearchableDepDrop::class, [
'data' => [
'California' => 'California',
'Texas' => 'Texas',
// ... other states
],
'placeholder' => 'Select a state...',
]);
B. Multiple Selection Dropdown
use rft\searchabledepdrop\widgets\SearchableDepDrop;
echo $form->field($model, 'tags')->widget(SearchableDepDrop::class, [
'data' => [
'1' => 'PHP',
'2' => 'JavaScript',
'3' => 'Python',
'4' => 'Java',
'5' => 'C#',
// ... other options
],
'allowMultiple' => true,
'placeholder' => 'Select multiple technologies...',
]);
C. Dependent Dropdown
Example for a State → City dropdown setup:
use rft\searchabledepdrop\widgets\SearchableDepDrop;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;
use common\models\AddressCity;
// State Dropdown (Parent)
echo $form->field($model, 'state')->widget(SearchableDepDrop::class, [
'data' => ArrayHelper::map(
AddressCity::find()->select('state')->distinct()->orderBy('state')->all(),
'state',
'state'
),
'options' => [
'id' => 'address-state',
],
'placeholder' => 'Select a state...',
]);
// City Dropdown (Child)
echo $form->field($model, 'city_id')->widget(SearchableDepDrop::class, [
'options' => [
'id' => 'address-city',
],
'placeholder' => 'Select City/Municipality',
'pluginOptions' => [
'depends' => ['address-state'],
'url' => Url::to(['/site/list-cities']),
],
])->label('City/Municipality');
The widget comes with built-in CSS that provides:
You can override the default styles by targeting the CSS classes:
/* Main container */
.sdd-container {
/* Your custom styles */
}
/* Display area */
.sdd-display {
border: 2px solid #your-color;
border-radius: 6px;
}
/* Selected items in multiple selection */
.sdd-selected-item {
background-color: #your-color;
border-radius: 8px;
}
.sdd-selected-item-container {
max-width: 100px; /* Adjust tag width */
}
.sdd-item-text {
font-size: 12px; /* Adjust text size */
}
.sdd-remove-btn {
color: #your-remove-color;
}
/* Dropdown */
.sdd-dropdown {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
/* Search input */
.sdd-search {
padding: 10px 12px;
font-size: 14px;
}
/* List items */
.sdd-list li {
padding: 10px 15px;
}
.sdd-list li:hover {
background-color: #your-hover-color;
}
| Class | Purpose |
|---|---|
.sdd-container | Main widget container |
.sdd-display | Display area showing selected values |
.sdd-selected-item | Individual selected item tag |
.sdd-selected-item-container | Container for selected item and remove button |
.sdd-item-text | Text within selected item |
.sdd-remove-btn | Remove button (×) for selected items |
.sdd-dropdown | Dropdown container |
.sdd-search | Search input field |
.sdd-list | List of available options |
.sdd-active | Currently highlighted option |
.sdd-no-results | No results message |
The widget supports several configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
data | array | [] | Array of options for the dropdown |
url | string | null | URL for dependent dropdown data |
depends | array | [] | Array of parent field IDs for dependent dropdowns |
paramNames | array | [] | Custom parameter names for dependent requests |
placeholder | string | 'Select...' | Placeholder text for the dropdown |
allowMultiple | boolean | false | Enable multiple selection |
rowSelector | string | '.item-item, .item' | CSS selector for dynamic form rows |
pluginOptions | array | [] | Additional JavaScript options |
The widget works seamlessly with wbraganca/yii2-dynamicform. Here's how to implement dependent dropdowns in dynamic forms:
Essential Widget Usage:
use rft\searchabledepdrop\widgets\SearchableDepDrop;
// State dropdown (parent)
echo $form->field($addresses[$i], "[{$i}]state")->widget(SearchableDepDrop::classname(), [
'data' => ArrayHelper::map(
AddressCity::find()->select('state')->distinct()->orderBy('state')->all(),
'state', 'state'
),
'placeholder' => 'Select State...',
'options' => ['class' => 'form-control state-dropdown'],
]);
// City dropdown (child - depends on state)
echo $form->field($addresses[$i], "[{$i}]city_id")->widget(SearchableDepDrop::classname(), [
'data' => [],
'placeholder' => 'Select City...',
'options' => ['class' => 'form-control city-dropdown'],
'pluginOptions' => [
'depends' => ['.state-dropdown'],
'paramNames' => ['state'],
'url' => Url::to(['/site/cities']),
]
]);
Required JavaScript for Dynamic Forms:
function initSearchableDepDrop(context) {
$(context)
.find(".sdd-container")
.each(function () {
var $container = $(this);
if ($container.data("searchableDepDrop")) return;
var optionsJson = $container.data("sdd-options");
if (optionsJson) {
var options = typeof optionsJson === "string" ? JSON.parse(optionsJson) : optionsJson;
$container.searchableDepDrop(options);
}
});
}
// Initialize widgets on new form rows
$(".dynamicform_wrapper").on("afterInsert", function (e, item) {
initSearchableDepDrop(item);
});
// Initialize existing widgets
initSearchableDepDrop(document.body);
Note: Replace
.dynamicform_wrapperwith your actualwidgetContainerclass fromDynamicFormWidget::begin().
To enable multiple selection in any dropdown, simply set allowMultiple => true:
// Multiple selection for skills/tags
echo $form->field($model, 'skills')->widget(SearchableDepDrop::classname(), [
'data' => [
'1' => 'PHP',
'2' => 'JavaScript',
'3' => 'Python',
'4' => 'Java',
'5' => 'C#',
'6' => 'Ruby',
'7' => 'Go',
'8' => 'Swift',
],
'allowMultiple' => true,
'placeholder' => 'Select your skills...',
'options' => ['class' => 'form-control skills-dropdown'],
]);
// Multiple selection for categories in dynamic form
echo $form->field($contact, "[{$i}]categories")->widget(SearchableDepDrop::classname(), [
'data' => [
'1' => 'Business',
'2' => 'Personal',
'3' => 'Emergency',
'4' => 'Family',
'5' => 'Friend',
],
'allowMultiple' => true,
'placeholder' => 'Select contact categories...',
'options' => ['class' => 'form-control categories-dropdown'],
]);
Important Notes:
paramNames option is crucial for dependent dropdowns to work properlySearchableDepDrop.php to src/widgets/SearchableDepDrop.phpSearchableDepDropAsset.php to src/widgets/SearchableDepDropAsset.phpsrc/
├── widgets/
│ ├── SearchableDepDrop.php
│ └── SearchableDepDropAsset.php
└── assets/
├── css/
│ └── searchable-dep-drop.css
└── js/
└── searchable-dep-drop.js
If you're upgrading from a previous version:
composer update rft/yii2-searchable-depdropuse rft\searchabledepdrop\widgets\SearchableDepDrop;This project is licensed under the MIT License.
]]>A package of helper classes for working with web components in Yii2.
Run
php composer.phar require mspirkov/yii2-web
or add
"mspirkov/yii2-web": "^0.1"
to the require section of your composer.json file.
A wrapper for \yii\web\Request for easier handling of GET and POST parameters.
It contains the following methods:
getGetInt - gets the value of a GET parameter by its name and tries to convert it to an integer.getGetFloat - gets the value of the GET parameter by its name and tries to convert it to a floating-point number.getGetBool - gets the value of the GET parameter by its name and tries to convert it to a boolean.getGetString - gets the value of the GET parameter by its name and tries to convert it to a string.getGetArray - gets the value of the GET parameter by its name and tries to convert it to an array.getPostInt - gets the value of a POST parameter by its name and tries to convert it to an integer.getPostFloat - gets the value of the POST parameter by its name and tries to convert it to a floating-point number.getPostBool - gets the value of the POST parameter by its name and tries to convert it to a boolean.getPostString - gets the value of the POST parameter by its name and tries to convert it to a string.getPostArray - gets the value of the POST parameter by its name and checks that the value is an array.First, you need to replace the request component in the configuration:
<?php
use MSpirkov\Yii2\Web\Request;
return [
...
'components' => [
'request' => [
'class' => Request::class,
...
],
...
],
];
You also need to specify this class in __autocomplete.php so that the IDE knows which class to use:
<?php
use yii\BaseYii;
use yii\web\Application;
use MSpirkov\Yii2\Web\Request;
class Yii extends BaseYii
{
/** @var __Application */
public static $app;
}
/**
* @property-read Request $request
*/
class __Application extends Application {}
I also recommend that you create your own basic controller and specify Request there:
use MSpirkov\Yii2\Web\Request;
/**
* @property Request $request
*/
class Controller extends \yii\web\Controller
{
public function init(): void
{
parent::init();
$this->request = Instance::ensure($this->request, Request::class);
}
}
class ProductController extends Controller
{
public function __construct(
string $id,
Module $module,
private readonly ProductService $service,
array $config = [],
) {
parent::__construct($id, $module, $config);
}
public function actionDelete(): array
{
$this->response->format = Response::FORMAT_JSON;
return $this->service->delete($this->request->getPostInt('id'));
}
}
A utility class for managing cookies.
This class encapsulates the logic for adding, removing, checking existence, and retrieving cookies, using the \yii\web\Request
and \yii\web\Response objects. It simplifies working with cookies by abstracting implementation details and providing more
convenient methods.
It contains the following methods:
has - checks if a cookie with the specified name exists.get - returns the cookie with the specified name.add - adds a cookie to the response.remove - removes a cookie.removeAll - removes all cookies.class CookieManager extends \MSpirkov\Yii2\Web\CookieManager
{
public function __construct()
{
parent::__construct(
Instance::ensure('request', Request::class),
Instance::ensure('response', Response::class),
);
}
}
class ExampleService
{
public function __construct(
private readonly CookieManager $cookieManager,
) {}
public function addCookie(): void
{
$this->cookieManager->add([
'name' => 'someCookieName',
'value' => 'someCookieValue',
]);
}
}
]]>A package of helper classes for working with databases in Yii2.
Run
php composer.phar require mspirkov/yii2-db
or add
"mspirkov/yii2-db": "^0.2"
to the require section of your composer.json file.
An abstract class for creating repositories that interact with ActiveRecord models. Contains the most commonly used methods: findOne, findAll, save and others. It also has several additional methods: findOneWith, findAllWith.
This way, you can separate the logic of executing queries from the ActiveRecord models themselves. This will make your ActiveRecord models thinner and simpler. It will also make testing easier, as you can mock the methods for working with the database.
/**
* @extends AbstractRepository<Customer>
*/
class CustomerRepository extends AbstractRepository
{
public function __construct()
{
parent::__construct(Customer::class);
}
}
class CustomerService
{
public function __construct(
private readonly CustomerRepository $customerRepository,
) {}
public function getCustomer(int $id): ?Customer
{
return $this->customerRepository->findOne($id);
}
}
Behavior for ActiveRecord models that automatically fills the specified attributes with the current date and time.
By default, this behavior uses the current date, time, and time zone. If necessary, you can specify your own attributes and time zone.
/**
* @property int $id
* @property string $content
* @property string $created_at
* @property string|null $updated_at
*/
class Message extends ActiveRecord
{
public static function tableName(): string
{
return '{{messages}}';
}
public function behaviors(): array
{
return [
DateTimeBehavior::class,
];
}
}
A utility class for managing database transactions with a consistent and safe approach.
This class simplifies the process of wrapping database operations within transactions, ensuring that changes are either fully committed or completely rolled back in case of errors.
It provides two main methods:
safeWrap - executes a callable within a transaction, safely handling exceptions and logging them.wrap - executes a callable within a transaction.class TransactionManager extends \MSpirkov\Yii2\Db\TransactionManager
{
public function __construct()
{
parent::__construct(Yii::$app->db);
}
}
class ProductService
{
public function __construct(
private readonly TransactionManager $transactionManager,
private readonly ProductFilesystem $productFilesystem,
private readonly ProductRepository $productRepository,
) {}
/**
* @return array{success: bool, message?: string}
*/
public function deleteProduct(int $id): array
{
$product = $this->productRepository->findOne($id);
// There's some logic here. For example, checking for the existence of a product.
$transactionResult = $this->transactionManager->safeWrap(function () use ($product) {
$this->productRepository->delete($product);
$this->productFilesystem->delete($product->preview_filename);
return [
'success' => true,
];
});
if ($transactionResult === false) {
return [
'success' => false,
'message' => 'Something went wrong',
];
}
return $transactionResult;
}
}
]]>(draft - all will be retested later)
In Yii3 it is not as easy to start as it was with Yii2. You have to install and configure basic things on your own. Yii3 uses the modern approach based on independent packages and dependency injection, but it makes it harder for newcomers. I am here to show all how I did it.
All the code is available in my new GitHub repository. I will be using it as a boiler-plate for my future projects so it should be always up-to-date and working.
Instead of installing local WAMP- or XAMPP-server I will be using Docker. Do not forget about a modern IDE like PhpStorm, which comes bundled with all you will ever need.
First of all, learn what PHP Standards Recommendations by Framework Interoperability Group (FIG) are. It will help you understand why so many "weird" PSR imports are in the Yii3 code. In short: These interfaces help authors of different frameworks to write compatible classes so they can be reused in any other framework following these principles.
Check this YouTube video for explanation
The __invoke() public method is called when you call the instance as a method. (Therefore the constructor was already executed)
$obj = new MyObj(); // Now the __construct() is executed.
$obj(); // Now the __invoke() is executed (The instance is needed!)
I never used it, but prepared a following example that shows when invoking can be applied:
class MyUpper
{
public function __invoke($a) { return $this->go($a); }
public function go($a) { return strtoupper($a); }
}
$instance = new MyUpper();
$array = ['a', 'B', 1, '1'];
// __invoke is used:
var_dump($instance($array[0]));
var_dump(array_map($instance, $array));
// These do the same without invoking:
var_dump(array_map('strtoupper', $array));
var_dump(array_map([$instance, 'go'], $array));
var_dump(array_map(function($a) use ($instance) { return $instance->go($a); }, ['a','B',1,'1']));
PHP 8 introduces annotations like this (not only for class attributes):
#[Column(type: 'primary')]#[Column(type: 'string(255)', nullable: true)]#[Entity(repository: UserRepository::class)]#[ManyToMany(target: Role::class, through: UserRole::class)]They should replace the original DocBlock annotatinos and provide more new functionalities.
Learn what they mean and how they are used by Yii3. To me this is a brand new topic as well.
Yii3 offers more basic applications: Web, Console, API. I will be using the API application:
Clone it like this:
.. and follow the docker instructions in the documentation.
If you don't have Docker, I recommend installing the latest version of Docker Desktop:
You may be surprised that docker-compose.yml is missing in the root. Instead the "make up" and "make down" commands are prepared. If you run both basic commands as mentioned in the documentation:
... then the web will be available on URL
If you want to modify the data that was returned by the endpoint, just open the action-class src/Api/IndexAction.php and add one more element to the returned array.
You may be missing 'docker compose stop' or 'make stop', because 'make down' removes your containers and drops your DB. In that case you can add it to the Makefile in the root (see below). If you then type 'make help' you will see the new command.
ifeq ($(PRIMARY_GOAL),stop)
stop: ## Stop the dev environment
$(DOCKER_COMPOSE_DEV) stop
endif
Your project now does not contain any DB. Let's add MariaDB and Adminer (DB browser) into file docker/dev/compose.yml:
In my case the resulting file looks like this:
services:
app:
container_name: yii3api_php
build:
dockerfile: docker/Dockerfile
context: ..
target: dev
args:
USER_ID: ${UID}
GROUP_ID: ${GID}
env_file:
- path: ./dev/.env
- path: ./dev/override.env
required: false
restart: unless-stopped
depends_on:
- db
ports:
- "${DEV_PORT:-80}:80"
volumes:
- ../:/app
- ../runtime:/app/runtime
- caddy_data:/data
- caddy_config:/config
tty: true
db:
image: mariadb:12.0.2-noble
container_name: yii3api_db
environment:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: db
MARIADB_USER: db
MARIADB_PASSWORD: db
adminer:
image: adminer:latest
container_name: yii3api_adminer
environment:
ADMINER_DEFAULT_SERVER: db
ports:
- ${DEV_ADMINER_PORT}:8080
depends_on:
- db
volumes:
mariadb_data:
Plus add/modify these variables in file docker/.env
Then run following commands:
Now you should see a DB browser on URL http://localhost:9081/?server=db&username=db&db=db
Login, server and pwd is defined in the snippet above.
If you type "docker ps" into your host console, you should see 3 running containers: yii3api_php, yii3api_adminer, yii3api_db.
The web will be, from now on, available on URL http://localhost:9080 which is more handy than just ":80" I think. (Later you may run 4 different projects at the same time and all cannot run on port 80)
Now when your project contains MariaDB, you may wanna use it in the code ...
After some time of searching you will discover you need to install these composer packages:
So you need to run following commands:
composer require yiisoft/db-mysql
composer require yiisoft/cache
composer require yiisoft/db-migration
To run composer (or any other command inside your dockerized yii3 application) you have 4 options:
Other solutions:
If you have Composer running locally, you can call these commands directly on your computer. (I do not recommend)
You can SSH into your docker container and call it there as Composer is installed inside. In that case:
- Find the name of the PHP container by typing "docker ps"
- Call "docker exec -it {containerName} /bin/bash"
- Now you are in the console of your php server and you can run composer.
If you are using PhpStorm, find the small icon "Services" in the left lower corner (looks ca like a cog wheel), find item "Docker-compose: app-api", inside click the "app" service, then "yii3api_php" container and then hit the button "terminal" on the righthand side.
Follow their documentations. Quick links:
The documentations want you to create 2 files:
- config/common/di/db-mysql.php
- config/common/db.php
- But you actually need only one. I recommend db-mysql.php
Note: If you want to create a file using commandline, you can use command "touch". For example "touch config/common/di/db-mysql.php"
Note: In the documentation the PHP snippets do not contain tag and declaration. Prepend it:
<?php
declare(strict_types=1);
When this is done, call "composer du" or "make composer du" and then try "make yii list". You should see the migration commands.
Run the command to create a migration:
Open the file and paste following content to the up() method:
$b->createTable('user', [
'id' => $b->primaryKey(),
'name' => $b->string()->notNull(),
'surname' => $b->string()->notNull(),
'username' => $b->string(),
'email' => $b->string()->notNull()->unique(),
'phone' => $b->string(),
'admin_enabled' => $b->boolean()->notNull()->defaultValue(false)->comment('Can user access the administration?'),
'vuejs_enabled' => $b->boolean()->notNull()->defaultValue(false)->comment('Can user access the mobile application?'),
'auth_key' => $b->string(32)->notNull()->unique(),
'access_token' => $b->string(32)->unique()->comment('For API purposes'),
'password_hash' => $b->string(),
'password_default' => $b->string(),
'password_vuejs_default' => $b->string(),
'password_vuejs_hash' => $b->string(),
'password_reset_token' => $b->string()->unique(),
'verification_token' => $b->string()->unique(),
'verified_at' => $b->dateTime(),
'status' => $b->smallInteger()->notNull()->defaultValue(100),
'created_by' => $b->integer(),
'updated_by' => $b->integer(),
'deleted_by' => $b->integer(),
'created_at' => $b->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
'updated_at' => $b->dateTime(),
'deleted_at' => $b->dateTime(),
]);
The down() method should contain this:
$b->dropTable('user');
Try to run "make yii migrate:up" and you will see error "could not find driver", because file "docker/Dockerfile" does not install the "pdo_mysql" extention. Add it to the place where "install-php-extensions" is called.
Then call:
Now you will see error "Connection refused" It means you have to update dns, user and password in file "config/common/params.php" based on what is written in "docker/dev/compose.yml".
If you run "make yii migrate:up" it should work now and your DB should contain the first table. Check it via adminer: http://localhost:9081/?server=db&username=db&db=db
In Yii we were always using ActiveRecord and its models, but in Yii3 the package is not ready yet. The solution is to use existing class Yiisoft\Db\Query\Query.
Open class src/Api/IndexAction.php and modify it a little to return all users via your REST API. You have more options:
You can manually instantiate the Query object, but you need to provide the DB connection manually:
declare(strict_types=1);
namespace App\Api;
use App\Api\Shared\ResponseFactory;
use App\Shared\ApplicationParams;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Db\Query\Query;
final class IndexAction
{
public function __invoke(
ResponseFactory $responseFactory,
ApplicationParams $applicationParams,
ConnectionInterface $db,
): ResponseInterface
{
$query = (new Query($db))
->select('*')
->from('user');
return $responseFactory->success($query->all());
}
}
Or you can use the DI container to provide you with the instance. I like this better as I can omit input parameters:
declare(strict_types=1);
namespace App\Api;
use App\Api\Shared\ResponseFactory;
use App\Shared\ApplicationParams;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Db\Query\Query;
final class IndexAction
{
public function __invoke(
ResponseFactory $responseFactory,
ApplicationParams $applicationParams,
ContainerInterface $container,
): ResponseInterface
{
$query = $container->get(Query::class)
->select('*')
->from('user');
return $responseFactory->success($query->all());
}
}
Now you can call the URL and see all the users. (If you entered some) http://localhost:9080
Note: You can also use Injector (and method
$injector->make()) instead of ContainerInterface (and method$container->get()). Injector seems to allow you to pass input arguments if needed.
PS: The input parameter of
new Query(ConnectionInterface $db)is automatically provided as it is defined in DI. See the file you created earlier above:config/common/di/db-mysql.php
Seeding = inserting fake data.
You can technically create a migration or a command and insert random data manually. But you can also use the Faker. In that case I needed following dependencies:
composer require fakerphp/faker
composer require yiisoft/security (not only for generating random strings)
Now find the class HelloCommand.php, copy and rename it to SeedCommand.php
Inside you will need the instance of ConnectionInterface. It can be automatically provided by the DI (because you defined it in config/common/di/db-mysql.php), you only need to create a new constructor and then use the instance in method execute():
namespace App\Console;
use Faker\Factory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Security\Random;
use Yiisoft\Yii\Console\ExitCode;
#[AsCommand(
name: 'seed',
description: 'Run to seed the DB',
)]
final class SeedCommand extends Command
{
public function __construct(
private readonly ConnectionInterface $db
)
{
parent::__construct();
}
protected function execute(
InputInterface $input,
OutputInterface $output
): int
{
$faker = Factory::create();
for ($i = 0; $i < 10; $i++) {
$this->db->createCommand()
->insert('user', [
'name' => $faker->firstName(),
'surname' => $faker->lastName(),
'username' => $faker->userName(),
'email' => $faker->email(),
'auth_key' => Random::string(32),
])
->execute();
}
$output->writeln('Seeding DONE.');
return ExitCode::OK;
}
}
Register the new command in file config/console/commands.php.
You can also obtain the ConnectionInterface in the same way as you did it in
IndexActionwith theQueryobject. Just useContainerInterface $containerin the constructor instead ofConnectionInterface $db. Then you can call$db = $this->container->get(ConnectionInterface::class);.
Each entity should have its Model class and Repository class if you are storing it in DB. Have a look at the demo application "blog-api": https://github.com/yiisoft/demo
In my case the User model (file src/Entity/User.php) will only contain private attributes, setters and getters. UserRepository (placed in the same folder) may look like this to enable CRUD (compressed code):
<?php
declare(strict_types=1);
namespace App\Entity;
use DateTimeImmutable;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Db\Exception\Exception;
use Yiisoft\Db\Exception\InvalidConfigException;
use Yiisoft\Db\Query\Query;
final class UserRepository
{
public const TABLE_NAME = 'user';
public function __construct(private readonly ConnectionInterface $db){}
public function findAll(array $orderBy = [], $asArray = false): array
{
$query = (new Query($this->db))->select('*')->from(self::TABLE_NAME)->orderBy($orderBy ?: ['created_at' => SORT_DESC]);
if ($asArray) {
return $query->all();
}
return array_map(
fn(array $row) => $this->hydrate($row),
$query->all()
);
}
public function findBy(string $attr, mixed $value): ?User
{
$row = (new Query($this->db))->select('*')->from(self::TABLE_NAME)->where([$attr => $value])->one();
return $row ? $this->hydrate($row) : null;
}
public function findByUsername(string $username): ?User
{
return $this->findBy('username', $username);
}
public function save(User $user): void
{
$data = ['name' => $user->getName(), 'surname' => $user->getSurname(), 'username' => $user->getUsername(), 'email' => $user->getEmail(), 'auth_key' => $user->getAuthKey()];
if ($user->getId() === null) {
$data['created_at'] = (new DateTimeImmutable())->format('Y-m-d H:i:s');
$this->db->createCommand()->insert(self::TABLE_NAME, $data)->execute();
} else {
$this->db->createCommand()->update(self::TABLE_NAME, $data, ['id' => $user->getId()])->execute();
}
}
public function delete(int $id): bool
{
try {
$this->db->createCommand()->delete(self::TABLE_NAME, ['id' => $id])->execute();
} catch (\Throwable $e) {
return false;
}
return true;
}
private function hydrate(array $row): User
{
$user = new User();
$reflection = new \ReflectionClass($user);
$this->hydrateAttribute($reflection, $user, 'id', (int) $row['id']);
$this->hydrateAttribute($reflection, $user, 'name', ($row['name']));
$this->hydrateAttribute($reflection, $user, 'surname', $row['surname']);
$this->hydrateAttribute($reflection, $user, 'username', $row['username']);
$this->hydrateAttribute($reflection, $user, 'email', $row['email']);
$this->hydrateAttribute($reflection, $user, 'created_at', new DateTimeImmutable($row['created_at']));
$this->hydrateAttribute($reflection, $user, 'updated_at', new DateTimeImmutable($row['updated_at'] ?? ''));
return $user;
}
private function hydrateAttribute(\ReflectionClass $reflection, object $obj, string $attribute, mixed $value)
{
$idProperty = $reflection->getProperty($attribute);
$idProperty->setAccessible(true);
$idProperty->setValue($obj, $value);
}
}
Now you can modify IndexAction to contain this: (read above to understand details)
// use App\Entity\UserRepository;
$userRepository = $container->get(UserRepository::class);
return $responseFactory->success($userRepository->findAll([], true));
Once user logs in you want to create an access-token. Why? Because in APIs the PHP session is not used, so users would have to send their login in every request, which would be a potential risk. So random strings with limited lifetime are generated and users send them in their requests intstead of the login. After a few minutes or hours the access token expires and a new one must be created. Each user can have more tokens for different situations. Details here: https://goteleport.com/learn/authentication-and-authorization/simple-random-tokens-secure-authentication/
Below I am indicating how to implement "Random Token Authentication". Other options would be:
Before you start, install dependency:
composer require yiisoft/security
Let's create a migration for storing the access tokens:
// method up():
$b->createTable('user_token', [
'id' => $b->primaryKey(),
'id_user' => $b->integer()->notNull(),
'token' => $b->string()->notNull()->unique(),
'expires_at' => $b->dateTime()->notNull(),
'created_at' => $b->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
'updated_at' => $b->dateTime(),
'deleted_at' => $b->dateTime(),
]);
Then create a model App\Entity\UserToken. It again contains only private properties, getters and setters. Plus I added __construct() and toArray():
// Uglified code:
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'integer')]
private int $id_user;
#[Column(type: 'string(255)', default: '')]
private string $token = '';
#[Column(type: 'datetime')]
private DateTimeImmutable $expires_at;
#[Column(type: 'datetime', nullable: true)]
private ?DateTimeImmutable $created_at;
#[Column(type: 'datetime', nullable: true)]
private ?DateTimeImmutable $updated_at;
#[Column(type: 'datetime', nullable: true)]
private ?DateTimeImmutable $deleted_at;
public function __construct(int $userId, string $token, DateTimeImmutable $expiresAt = null)
{
$this->id_user = $userId;
$this->token = $token;
$this->expires_at = $expiresAt;
}
public function toArray(): array
{
return [
'id' => $this->id,
'id_user' => $this->id_user,
'token' => $this->token,
'expires_at' => $this->expires_at->format('Y-m-d H:i:s'),
];
}
Then you will also need class App\Entity\UserTokenRepository for DB manipulation. Copy and modify the UserRepository. These methods will be handy:
public function findByToken(string $token): ?UserToken
{
$tokenEntity = $this->findBy('token', $token);
if (!$tokenEntity) {
return null;
}
if ($tokenEntity->getExpiresAt() < new DateTimeImmutable()) {
// Optionally delete expired token
$this->delete($tokenEntity->getId());
return null;
}
return $tokenEntity;
}
public function create(int $userId, ?string $token = null, ?DateTimeImmutable $expiresAt = null, $lifespan = '+2 hours'): UserToken
{
if (!$token) {
$token = bin2hex(Random::string(32));
// Example: 654367506342505647634a6f4c6945784d793447355048734b364a4e62483743
}
if (!$expiresAt) {
$expiresAt = (new DateTimeImmutable())->modify($lifespan);
}
$entity = new UserToken($userId, $token, $expiresAt);
$this->db->createCommand()
->insert(self::TABLE_NAME, $entity->toArray())
->execute();
return $entity;
}
The User model will need one more method:
// use Yiisoft\Security\PasswordHasher;
public function validatePassword(string $password): bool
{
return (new PasswordHasher())->validate($password, $this->password_vuejs_hash);
}
In the end you can create the login action. Register it again in config/common/routes.php.
<?php
declare(strict_types=1);
namespace App\Api;
use App\Api\Shared\ResponseFactory;
use App\Entity\UserRepository;
use App\Entity\UserTokenRepository;
use App\Shared\ApplicationParams;
use DateTimeImmutable;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\DataResponse\DataResponse;
use Yiisoft\Http\Status;
final class LoginAction
{
public function __construct(
private UserRepository $userRepository,
private UserTokenRepository $userTokenRepository,
){}
public function __invoke(
ResponseFactory $responseFactory,
ApplicationParams $applicationParams,
ContainerInterface $container,
ServerRequestInterface $request
): ResponseInterface
{
$data = json_decode((string) $request->getBody(), true);
$username = $data['username'] ?? '';
$password = $data['password'] ?? '';
$user = $this->userRepository->findByUsername($username);
if (!$user || !$user->validatePassword($password)) {
return new DataResponse(['error' => 'Invalid credentials'], Status::UNAUTHORIZED);
}
$this->userTokenRepository->deleteByUserId($user->getId());
$userToken = $this->userTokenRepository->create($user->getId());
return $responseFactory->success([
'token' => $userToken->getToken(),
'expires_at' => $userToken->getExpiresAt()->format(DateTimeImmutable::ATOM),
]);
}
}
Next we also need an algorithm that will enforce these tokens in each request, will validate and refresh them and will restrict access only to endpoints that the user can use. This is a bigger topic for later. It may be covered by the package https://github.com/yiisoft/auth/ which offers "HTTP bearer authentication".
If you create a REST API you may be interested in a JS frontend that will communicate with it using Ajax. Below you can peek into my very simple VueJS3 attempt. It is an installable PWA application that works in offline mode (=1 data transfer per day, not on every mouse click) and is meant for situations when customer does not have wifi everywhere. See my Gitlab.
]]>Yii 2 extension for render views using Fenom template engine.
composer require ensostudio/yii2-fenom
You can add a custom template engine by reconfiguring view component's behavior:
[
'components' => [
'view' => [
'class' => yii\web\View::class,
'renderers' => [
'tpl' => [
'class' => ensostudio\yii2fenom\FenomViewRenderer::class,
// customize renderer options,
],
],
],
],
]
]]>Yii2 File Crafter - library for generating a many templates with minimal differences
Setup
(Recommended)
composer require andy87/yii2-file-crafter:dev-master --dev
````
php composer.phar require andy87/yii2-file-crafter:dev-master --dev
composer.jsonOpen file composer.json, in section with key require add line:
"andy87/yii2-file-crafter": "dev-master"
dont forget update composer
`bash
composer update
`
- - - - -
Config
Config in the configuration file.
config/(web|web-local|local).php (frontend|backend)/config/main-local.php Minimum config
`php
$config['modules']['gii'] = [
'class' => yii\gii\Module::class,
'generators' => [
'fileCrafter' => [
'class' => andy87\yii2\file_crafter\Crafter::class,
'options' => [
'templates' => [
'group_name' => [
// 'template' => 'path/to/file.php',
'common/services/PascalCaseService' => '@app/common/services/items/{PascalCase}Service.php',
'template/test/unit/camelCaseService.tpl' => '@backend/test/unit/{{camelCase}}Service.php',
'templates/view/index.php' => 'custom/dir/{{snake_case}}/index.php',
]
]
]
]
],
];
`
Full Config with all options
`php
$config['modules']['gii'] = [
'class' => yii\gii\Module::class,
'generators' => [
'fileCrafter' => [
'class' => andy87\yii2\file_crafter\Crafter::class,
'options' => [
'cache' => [
'dir' => '@runtime/yii2-file-crafter/cache',
'ext' => '.tpl'
],
'source' => [
'dir' => '@runtime/yii2-file-crafter/templates/source',
'ext' => '.tpl'
],
'custom_fields' => [
'singular' => 'label - one',
'plural' => 'label - many',
],
'commands' => [
'php yii gii/model --tableName={{snake_case}} --modelClass={{PascalCase}} --ns="common\models" --interactive=0' //...
],
'eventHandler' => app\composents\behavior\FileCrafterBehavior::class,
'autoCompleteStatus' => true,
'autoCompleteList' => [
'autocomplete name 1',
'autocomplete name 2',
],
'previewStatus' => true,
'canDelete' => true,
'parseDataBase' => ['autocomplete','fakeCache'],
'templates' => [
'common' => [
'common/services/PascalCaseService' => 'app/common/services/items/{[PascalCase]}Service.php',
],
'backend' => [
'backend/test/unit/camelCaseService.tpl' => 'backend/test/unit/{{camelCase}}Service.php',
],
'frontend' => [
'frontend/view/index.php' => 'app/frontend/view/{{snake_case}}/index.php',
],
'all' => [
'common/services/PascalCaseService' => 'app/common/services/items/{PascalCase}Service.php',
'backend/test/unit/camelCaseService.tpl' => 'backend/test/unit/{{camelCase}}Service.php',
'frontend/view/index.php' => 'app/frontend/view/{{snake_case}}/index.php',
]
],
]
]
],
];
`
Using
Module use marks for replace variables in templates.
{{PascalCase}} - PascalCase by schema name {{camelCase}} - camelCase by schema name {{snake_case}} - snake_case by schema name {{kebab-case}} - kebab-case by schema name {{UPPERCASE}} - UPPERCASE by schema name {{lowercase}} - lowercase by schema name{{[key]}} - custom key from property custom_fields on config ( see Custom Fields )for schema name Product Items replace marks:
{{PascalCase}} - ProductItems {{camelCase}} - productItems {{snake_case}} - product_items {{kebab-case}} - product-items {{UPPERCASE}} - PRODUCT ITEMS {{lowercase}} - product items
Cache
<small style="color: #009; font-size:9px">(optional)</small>
Configuration for the cache folder with schema data.
dir - path to the cache directory with schema data ext - extension of the cache file Default configuration:
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'cache' => [
'dir' => '@runtime/yii2-file-crafter/cache',
'ext' => '.json'
],
// ...
],
],
]
];
`
Source
<small style="color: #009; font-size:9px">(optional)</small>
Configuration for the source folder with templates files.
dir - path to the directory with the templates files source for generation ext - extension of the templates file Default configuration:
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'source' => [
'dir' => '@runtime/yii2-file-crafter/templates/source',
'ext' => '.tpl'
],
// ...
],
],
]
];
`
Templates
<small style="color: #900; font-size:9px">(required)</small>
Array with groups of templates for use on generate files.
`php
[
['group1'] => [
'template1' => 'path/from/project/root/to/resultFile.tpl',
'template2.tpl' => 'path/from/project/root/to/resultFile.php',
// ...
],
['group2'] => [
'template1.php' => '@path/alias/to/resultFile.tpl',
'@alias/to/template' => 'path/from/project/root/to/resultFile.php',
// ...
],
]
`
The source path may contain:
@ alias ( source['dir'] - default container ) ext for generate any file type ( .php default ) {{variable}} ( see Marks ) File source-template will be searched in the source folder.
Source folder path can be set in the configuration file. ( see Source )
The resultFile path may contain:
@ alias ( @app/ - default prefix ) ext for generate any file type ( .php default ) {{variable}} ( see Marks ) Content of the templates file rendered with the View method renderFile
And prepared with the $replaceList array contains all marks. ( see Marks )
And also passed to the render method:
$schema - schema object $generator - self generator object$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'templates' => [
'all' => [
'@backend/dir/by/alias/camelCaseService.tpl' => '@backend/generate/by/alias/{{camelCase}}Service.php',
'dir/on/source/dir/generate_file' => 'custom/dir/on/source/dir/{{snake_case}}/generate_file.tpl',
],
],
// ...
],
],
]
];
Custom Fields
<small style="color: #009; font-size:9px">(optional)</small>
Array with custom fields for use custom variables in templates.
Using on template key wrapped in square brackets: {{%key%}}
Example: {{key_one}}, {{key_two}}...
Example simple config
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'custom_fields' => [
'singular' => 'one',
'plural' => 'many',
],
// ...
],
],
]
];
with template:php
Value - ONE = {{singular}}
Value - MANY = ({{plural}})
`
___
Schema 1: Product Items
Field one = !!product!!
Field many = >>> products <<<
...generate...
Result: app/frontend/views/info--product_items.php
`php
Value - ONE = !!product!!
Value - MANY = (>>> products <<<)
`
___
Schema 2: Category Group
Field label one = --category--
Field label many = +++categories+++
...generate...
Result: app/frontend/views/info--category_group.php
`php
Value - ONE = --category--
Value - MANY = (+++categories+++)
`
Key autoCompleteStatus contain status for autocomplete field Schema name in the form 200 populated values.
Variants: true or false(default)
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'autoCompleteStatus' => true,
// ...
],
],
],
];
`
Autocomplete list
<small style="color: #009; font-size:9px">(optional)</small>
Key autoCompleteList contain list of autocomplete field Schema name in the form self custom list.
Type: array
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'autoCompleteList' => [
'Product Items',
'Category Group',
'User Profile',
// ...
],
// ...
],
],
],
];
`
<h2 align="center">Preview status
<small style="color: #009; font-size:9px">(optional)</small>
Key previewStatus contain status for preview file content on hover icon in the form.
Variants: true(default) or false
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'previewStatus' => true,
// ...
],
],
],
];
`
<h2 align="center">Can delete
<small style="color: #009; font-size:9px">(optional)</small>
Key canDelete contain status for delete schema from the form.
Variants: true(default) or false
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'canDelete' => true,
// ...
],
],
],
];
`
Parse data base
<small style="color: #009; font-size:9px">(optional)</small>
Key parseDataBase contain list of target for extend schema name list from database.
Variants: array with values:
autocompletefakeCacheDefault empty;
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'parseDataBase' => ['autocomplete','fakeCache'],
// ...
],
],
],
];
`
Commands
<small style="color: #009; font-size:9px">(optional)</small>
Key commands contain list cli command for call before generate any file.
command make use of the {{variable}} in the command string ( see Marks )
Example: generate gii model for table name from schema name before generate fileContent
Default empty;
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'fileCrafter' => [
'options' => [
// ...
'commands' => [
'php yii gii/model --tableName={{snake_case}} --modelClass={{PascalCase}} --ns="common\models" --interactive=0 --overwrite=1' // ...
],
// ...
],
],
],
];
`
Events
<small style="color: #009; font-size:9px">(optional)</small>
Make use of the eventHandlers key to add a behavior to the module.
Example: add behavior FileCrafterBehavior to the module
Default null;
`php
$config['modules']['gii'] = [
'class' => Module::class,
'generators' => [
'options' => [
// ...
'eventHandlers' => FileCrafterBehavior::class,
// ...
],
],
],
];
`
CrafterEvent::BEFORE_INIT before init module
CrafterEvent::AFTER_INIT after init module
Come events has special properties...
CrafterEventGenerate::BEFORE before generate all files
//class FileCrafterBehavior extends Behavior
public function beforeGenerate(CrafterEventGenerate $crafterEventGenerate): void {
Yii::info([ 'Generated files', [
$crafterEventGenerate->files // empty (call before generate)
]]);
}
CrafterEventCommand::BEFORE before run cli command
//class FileCrafterBehavior extends Behavior
public function beforeCommand(CrafterEventCommand $crafterEventCommand): void {
Yii::error([ __METHOD__, [
$crafterEventCommand->cmd->exec,
$crafterEventCommand->cmd->output, // empty (call before exec command)
$crafterEventCommand->cmd->replaceList
]]);
}
\andy87\yii2\file_crafter\components\models\Dto
CrafterEventCommand::AFTER after run cli command
//class FileCrafterBehavior extends Behavior
public function afterCommand(CrafterEventCommand $crafterEventCommand): void {
Yii::error([ __METHOD__, [
$crafterEventCommand->cmd->exec,
$crafterEventCommand->cmd->output, // output command
$crafterEventCommand->cmd->replaceList
]]);
}
CrafterEventRender::BEFORE before render file
//class FileCrafterBehavior extends Behavior
public function beforeRender(CrafterEventRender $crafterEventRender): void {
Yii::error([ __METHOD__, [
$crafterEventRender->schema,
$crafterEventRender->sourcePath,
$crafterEventRender->generatePath,
$crafterEventRender->replaceList,
$crafterEventRender->content // empty (call before render file)
]]);
}
CrafterEventRender::AFTER after render file
//class FileCrafterBehavior extends Behavior
public function afterRender(CrafterEventRender $crafterEventRender): void {
Yii::error([ __METHOD__, [
$crafterEventRender->schema,
$crafterEventRender->sourcePath,
$crafterEventRender->generatePath,
$crafterEventRender->replaceList,
$crafterEventRender->content // content file
]]);
}
CrafterEventGenerate::AFTER after generate all files
public function afterGenerate(CrafterEventGenerate $crafterEventGenerate): void {
Yii::info([ 'Generated files', [
$crafterEventGenerate->files // CodeFile[]
]]);
}
]]>Esta es una extensión primaria para Yii framework 2.0. Encapsula componentes de Material Design en términos de Widgets Yii.
NOTA: Material Web 3 no tiene los componentes Card, Snackbar, TopAppBar ni NavigationRail, así que se han creado desde 0 intentando seguir los lineamientos del diseño de Material 3.
La forma preferida de instalar esta extensión es a través de composer.
Luego ejecute
php composer.phar require --prefer-dist neoacevedo/yii2-material
o agregue
"neoacevedo/yii2-material": "*"
a la sección require de su archivo composer.json.
Incluya antes del cierre de la etiqueta 'body' de su plantilla principal lo siguiente:
<?= MaterialAsset::publishMaterialScripts() ?>
Los data-* atributos programados no funcionan en Material Design Components para la web, por lo que se tendrán que programar los elementos que tengan estos atributos de manera separada.
Si este proyecto te es útil, considera hacer una donación:
| Ko-fi | Litecoin | | ------------------------------------------------------ | ------------------------------------------------------------ | | [](https://ko-fi.com/neoacevedo) |
|
| ☕ [Ko-fi](https://ko-fi.com/neoacevedo) | Ł Donaciones Litecoin |
Este proyecto está licenciado bajo la Licencia GPL-3.0+ - ver el archivo LICENSE.md para más detalles.
]]>Bootstrap 4 and 5 are expecting `html` like this to decorate validation error:
<input type="text" id="eventform-datetime" class="form-control is-invalid" name="EventForm[datetime]" aria-required="true">
<div class="invalid-feedback">Error message</div>
Element with `div.invalid-feedbackis supposed to be on the same level with yourinput.is-invalid`.
But sometimes when we are using any widgets or custom template we get html like this:
<div class="some-plugin-wrapper">
<input type="text" id="eventform-datetime" class="form-control is-invalid" name="EventForm[datetime]" aria-required="true">
</div>
<div class="invalid-feedback">Error message</div>
, so our error message is not shown.
Of cource you can make `div.invalid-feedback` visible by css for this page.
But if that does not suit you, this library propose another solution.
We are adding special `to a field template right before{error}part. And we **synchronize** thiswith the **input field** so it gets.is-invalid` class when input does
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 ...
],
],
// ...
There are multiple blog that shows how to use seperate login for yii2 application but in this article i will show you how to use a single login screen for all your YII2 Advanced, YII2 Basic, Application, It will also work when your domain on diffrent server or the same server.
Here are few Steps you need to follow ot achive this.
1. For Advanced Templates
Step 1 : Add this into your component inside
/path/common/config/main.php
'components' => [
'user' => [
'identityClass' => 'common\models\User',
'enableAutoLogin' => true,
'identityCookie' => ['name' => '_identity', 'httpOnly' => true],
],
'request' => [
'csrfParam' => '_csrf',
],
],
Step 2: Add Session and Request into main-local.php
/path/common/config/main-local.php
'components' => [
'session' => [
'cookieParams' => [
'path' => '/',
'domain' => ".example.com",
],
],
'user' => [
'identityCookie' => [
'name' => '_identity',
'path' => '/',
'domain' => ".example.com",
],
],
'request' => [
'csrfCookie' => [
'name' => '_csrf',
'path' => '/',
'domain' => ".example.com",
],
],
],
Note: example.com is the main domain. All other domain should be sub domain of this.
Step 3: Now Update the Same Validation Key for all the applications
/path/frontend/config/main-local.php
/path/backend/config/main-local.php
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => 'fFUeb5HDj2P-1a1FTIqya8qOE',
],
],
Note : Remove the Session and request keys from your main.php of Both frontend and backend application.
Step 4: Note Somethign that you also have and console application so update session, user,and request into the main-local.php of your console application
/path/console/config/main-local.php
'components' => [
'session' => null,
'user' => null,
'request' => null,
]
2. For Basic Templates
Additionaly If you have an basic templates installed for another project and you want to use same login for that templates. To Achive this follow the given steps
Step 1: Update You main-local.php of basic template
/path/basic-app/config/main-local.php
'components' => [
'session' => [
'cookieParams' => [
'path' => '/',
'domain' => ".example.com",
],
],
'user' => [
'identityCookie' => [
'name' => '_identity',
'path' => '/',
'domain' => ".example.com",
],
],
'request' => [
'csrfCookie' => [
'name' => '_csrf',
'path' => '/',
'domain' => ".example.com",
],
],
],
I Hope you understand well how to use a single login for all of your domain and subdomain or repository.
:) Thanks for Reading
]]>I was recently assigned with the task of integrating several extensive forms into a WordPress website. These forms comprised numerous fields, intricate validation rules, dynamic fields (one to many relationships) and even interdependencies, where employing PHP inheritance could mitigate code duplication.
Upon initial exploration, it became evident that the conventional approach for handling forms in WordPress typically involves either installing a plugin or manually embedding markup using the editor or custom page templates. Subsequently, one largely relies on the plugin's functionality to manage form submissions or resorts to custom coding.
Given that part of my task entailed logging data, interfacing with API endpoints, sending emails, and more, I opted to develop the functionality myself, rather than verifying if existing plugins supported these requirements.
Furthermore, considering the current landscape (as of March 2024) where most Yii 3 packages are deemed production-ready according to official sources, and being a long-time user of the Yii framework, I deemed it an opportune moment to explore and acquaint myself with these updates.
You can explore the entire project and review the code by accessing it on Github.
Additionally, you can deploy it effortlessly using Docker by simply executing docker-compose up from the project's
root directory. Check the Dockerfile for the WordPress setup and content generation which is done automatically.
My objective was to render and manage forms within a WordPress framework utilizing Yii3 packages. For demonstration purposes, I chose to implement a basic Rating Form, where the focus is solely on validating the data without executing further actions.
To proceed, let's start with a minimalistic classic theme as an example. I created a WordPress page named "The Rating
Form" within the dashboard. Then, a file named page-the-rating-form.php is to be created within the theme's root
folder to display this specific page.
This designated file serves as the blueprint for defining our form's markup.
To harness Yii3's functionalities, we'll incorporate the following packages:
To begin, let's initialize a Composer project in the root of our theme by executing composer init. This process will
generate a composer.json file. Subsequently, we'll proceed to include the Yii3 packages in our project.
composer require yiisoft/form-model:dev-master yiisoft/validator yiisoft/form:dev-master
and instruct the theme to load the composer autoload by adding the following line to the functions.php file:
require __DIR__ . '/vendor/autoload.php';
Following the execution of the composer init command, a src directory has been created in the root directory of the
theme. We will now proceed to add our form model class within this directory.
Anticipating the expansion of the project, it's imperative to maintain organization. Thus, we shall create the
directory src/Forms and place the RatingForm class inside it.
<?php
namespace Glpzzz\Yii3press\Forms;
use Yiisoft\FormModel\FormModel;
class RatingForm extends FormModel
{
private ?string $name = null;
private ?string $email = null;
private ?int $rating = null;
private ?string $comment = null;
private string $action = 'the_rating_form';
public function getPropertyLabels(): array
{
return [
'name' => 'Name',
'email' => 'Email',
'rating' => 'Rating',
'comment' => 'Comment',
];
}
}
Beyond the requisite fields for our rating use case, it's crucial to observe the action class attribute. This
attribute is significant as it instructs WordPress on which theme hook should manage the form submission. Further
elaboration on this will follow.
Now, let's incorporate some validation rules into the model to ensure input integrity. Initially, we'll configure the
class to implement the RulesProviderInterface. This enables the form package to access these rules and augment the
HTML markup with native validation attributes.
class RatingForm extends FormModel implements RulesProviderInterface
Now we need to implement the getRules() method on the class.
public function getRules(): iterable
{
return [
'name' => [
new Required(),
],
'email' => [
new Required(),
new Email(),
],
'rating' => [
new Required(),
new Integer(min: 0, max: 5),
],
'comment' => [
new Length(min: 100),
],
];
}
To generate the form markup, we require an instance of RatingForm to be passed to the template. In WordPress, the
approach I've adopted involves creating a global variable (admittedly not the most elegant solution) prior to rendering
the page.
$hydrator = new Hydrator(
new CompositeTypeCaster(
new NullTypeCaster(emptyString: true),
new PhpNativeTypeCaster(),
new HydratorTypeCaster(),
)
);
add_filter('template_redirect', function () use ($hydrator) {
// Get the queried object
$queried_object = get_queried_object();
// Check if it's a page
if ($queried_object instanceof WP_Post && is_page()) {
if ($queried_object->post_name === 'the-rating-form') {
global $form;
if ($form === null) {
$form = $hydrator->create(RatingForm::class, []);
}
}
}
});
It's worth noting that we've instantiated the Hydrator class outside any specific function, enabling us to reuse it
for all necessary callbacks. With the RatingForm instance now available, we'll proceed to craft the markup for the
form within the page-the-rating-form.php file.
<?php
use Glpzzz\Yii3press\Forms\RatingForm;
use Yiisoft\FormModel\Field;
use Yiisoft\Html\Html;
/** @var RatingForm $form */
global $form;
?>
<?php get_header(); ?>
<h1><?php the_title(); ?></h1>
<?php the_content(); ?>
<?= Html::form()
->post(esc_url(admin_url('admin-post.php')))
->open()
?>
<?= Field::hidden($form, 'action')->name('action') ?>
<?= Field::text($form, 'name') ?>
<?= Field::email($form, 'email') ?>
<?= Field::range($form, 'rating') ?>
<?= Field::textarea($form, 'comment') ?>
<?= Html::submitButton('Send') ?>
<?= "</form>" ?>
<?php get_footer(); ?>
In the markup generation of our form, we've leveraged a combination of Yii3's Html helpers and the Field class.
Notable points include:
admin-post.php WordPress endpoint.action value in the form submission, we utilized a hidden field named 'action'. We opted to rename
the input to 'action' as the Field::hidden method generates field names in the
format TheFormClassName[the_field_name], whereas we required it to be simply named 'action'.This adjustment facilitates hooking into a theme function to handle the form request, as elucidated in the subsequent section.
Before delving further, let's capitalize on Yii's capabilities to enhance the form. Although we've already defined
validation rules in the model for validating input post-submission, it's advantageous to validate input within the
browser as well. While we could reiterate defining these validation rules directly on the input elements, Yii offers a
streamlined approach. By incorporating the following code snippet into the functions.php file:
add_action('init', function () {
ThemeContainer::initialize([
'default' => [
'enrichFromValidationRules' => true,
]
], 'default', new ValidationRulesEnricher()
);
});
By implementing this code snippet, we activate the ValidationRulesEnricher for the default form theme. Upon
activation, we'll notice that the form fields are now enriched with validation rules such as 'required', 'min', and '
max', aligning with the validation rules previously defined in the model class. This feature streamlines the process,
saving us valuable time and minimizing the need for manual code composition. Indeed, this showcases some of the
remarkable functionality offered by Yii3.
When the form is submitted, it is directed to admin-post.php, an endpoint provided by WordPress. However, when dealing
with multiple forms, distinguishing the processing of each becomes essential. This is where the inclusion of
the action value in the POST request proves invaluable.
Take note of the initial two lines in the following code snippet: the naming convention for the hook
is admin_post_<action_name>. Therefore, if a form has action = 'the-rating-form', the corresponding hook name will
be admin_post_the_rating_form.
As for the inclusion of both admin_post_<action_name> and admin_post_nopriv_<action_name>, this is because WordPress
allows for different handlers depending on whether the user is logged in or not. In our scenario, we require the same
handler regardless of the user's authentication status.
add_action('admin_post_the_rating_form', fn() => handleForms($hydrator));
add_action('admin_post_nopriv_the_rating_form', fn() => handleForms($hydrator));
function handleForms(Hydrator $hydrator): void
{
global $form;
$form = $hydrator->create(RatingForm::class, $_POST['RatingForm']);
$result = (new Yiisoft\Validator\Validator())->validate($form);
if ($form->isValid()) {
// handle the form
}
get_template_part('page-the-rating-form');
}
Returning to the Yii aspect: we instantiate and load the posted data into the form utilizing the hydrator. We then
proceed to validate the data. If the validation passes successfully, we can proceed with the intended actions using the
validated data. However, if validation fails, we re-render the form, populating it with the submitted data and any error
messages generated during validation.
Originally posted on https://glpzzz.dev/2024/03/03/integrating-yii3-packages-into-wordpress.html
]]>Use the following css styles for carousel to work as expected.
.product_img_slide {
padding: 100px 0 0 0;
}
.product_img_slide > .carousel-inner > .carousel-item {
overflow: hidden;
max-height: 650px;
}
.carousel-inner {
position: relative;
width: 100%;
}
.product_img_slide > .carousel-indicators {
top: 0;
left: 0;
right: 0;
width: 100%;
bottom: auto;
margin: auto;
font-size: 0;
cursor: e-resize;
/* overflow-x: auto; */
text-align: left;
padding: 10px 5px;
/* overflow-y: hidden;*/
white-space: nowrap;
position: absolute;
}
.product_img_slide > .carousel-indicators li {
padding: 0;
width: 76px;
height: 76px;
margin: 0 5px;
text-indent: 0;
cursor: pointer;
background: transparent;
border: 3px solid #333331;
-webkit-border-radius: 0;
border-radius: 0;
-webkit-transition: all 0.7s cubic-bezier(0.22, 0.81, 0.01, 0.99);
transition: all 1s cubic-bezier(0.22, 0.81, 0.01, 0.99);
}
.product_img_slide > .carousel-indicators .active {
width: 76px;
border: 0;
height: 76px;
margin: 0 5px;
background: transparent;
border: 3px solid #c13c3d;
}
.product_img_slide > .carousel-indicators > li > img {
display: block;
/*width:114px;*/
height: 76px;
}
.product_img_slide .carousel-inner > .carousel-item > a > img, .carousel-inner > .carousel-item > img, .img-responsive, .thumbnail a > img, .thumbnail > img {
display: block;
max-width: 100%;
line-height: 1;
margin: auto;
}
.product_img_slide .carousel-control-prev {
top: 58%;
/*left: auto;*/
right: 76px;
opacity: 1;
width: 50px;
bottom: auto;
height: 50px;
font-size: 50px;
cursor: pointer;
font-weight: 700;
overflow: hidden;
line-height: 50px;
text-shadow: none;
text-align: center;
position: absolute;
background: transparent;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.6);
-webkit-box-shadow: none;
box-shadow: none;
-webkit-border-radius: 0;
border-radius: 0;
-webkit-transition: all 0.6s cubic-bezier(0.22, 0.81, 0.01, 0.99);
transition: all 0.6s cubic-bezier(0.22, 0.81, 0.01, 0.99);
}
.product_img_slide .carousel-control-next {
top: 58%;
left: auto;
right: 25px;
opacity: 1;
width: 50px;
bottom: auto;
height: 50px;
font-size: 50px;
cursor: pointer;
font-weight: 700;
overflow: hidden;
line-height: 50px;
text-shadow: none;
text-align: center;
position: absolute;
background: transparent;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.6);
-webkit-box-shadow: none;
box-shadow: none;
-webkit-border-radius: 0;
border-radius: 0;
-webkit-transition: all 0.6s cubic-bezier(0.22, 0.81, 0.01, 0.99);
transition: all 0.6s cubic-bezier(0.22, 0.81, 0.01, 0.99);
}
.product_img_slide .carousel-control-next:hover, .product_img_slide .carousel-control-prev:hover {
color: #c13c3d;
background: transparent;
}
Here is a Corousel widget that is an extension of yii\bootstrap5\Carousel, to show image thumbnails as indicators for the carousel.
Here is the widget code.
<?php
namespace app\widgets;
use Yii;
use yii\bootstrap5\Html;
class Carousel extends \yii\bootstrap5\Carousel
{
public $thumbnails = [];
public function init()
{
parent::init();
Html::addCssClass($this->options, ['data-bs-ride' => 'carousel']);
if ($this->crossfade) {
Html::addCssClass($this->options, ['animation' => 'carousel-fade']);
}
}
public function renderIndicators(): string
{
if ($this->showIndicators === false){
return '';
}
$indicators = [];
for ($i = 0, $count = count($this->items); $i < $count; $i++){
$options = [
'data' => [
'bs-target' => '#' . $this->options['id'],
'bs-slide-to' => $i
],
'type' => 'button',
'thumb' => $this->thumbnails[$i]['thumb']
];
if ($i === 0){
Html::addCssClass($options, ['activate' => 'active']);
$options['aria']['current'] = 'true';
}
$indicators[] = Html::tag('li',Html::img($options['thumb']), $options);
}
return Html::tag('ol', implode("\n", $indicators), ['class' => ['carousel-indicators']]);
} }
You can use the above widget in your view file as below:
<?php
$indicators = [
'0' =>[ 'thumb' => "https://placehold.co/150X150?text=A"],
'1' => ['thumb' => 'https://placehold.co/150X150?text=B'],
'2' => [ 'thumb' => 'https://placehold.co/150X150?text=C']
];
$items = [
[ 'content' =>Html::img('https://live.staticflickr.com/8333/8417172316_c44629715e_w.jpg')],
[ 'content' =>Html::img('https://live.staticflickr.com/3812/9428789546_3a6ba98c49_w.jpg')],
[ 'content' =>Html::img('https://live.staticflickr.com/8514/8468174902_a8b505a063_w.jpg')]
];
echo Carousel::widget([
'items' =>
$items,
'thumbnails' => $indicators,
'options' => [
'data-interval' => 3, 'data-bs-ride' => 'scroll','class' => 'carousel product_img_slide',
],
]);
]]>Yii comes with internationalisation (i18n) "out of the box". There are instructions in the manual as to how to configure Yii to use i18n, but little information all in one place on how to fully integrate it into the bootstrap menu. This document attempts to remedy that.

The Github repository also contains the language flags, some country flags, a list of languages codes and their language names and a list of the languages Yii recognises "out of the box". A video will be posted on YouTube soon.
Ensure that your system is set up to use i18n. From the Yii2 Manual:
Yii uses the
PHP intlextension to provide most of its I18N features, such as the date and number formatting of theyii\i18n\Formatterclass and the message formatting usingyii\i18n\MessageFormatter. Both classes provide a fallback mechanism when the intl extension is not installed. However, the fallback implementation only works well for English target language. So it is highly recommended that you installintlwhen I18N is needed.
First you need to create a configuration file.
Decide where to store it (e.g. in the ./messages/ directory with the name create_i18n.php). Create the directory in the project then issue the following command from Terminal (Windows: CMD) from the root directory of your project:
./yii message/config-template ./messages/create_i18n.php
or for more granularity:
./yii message/config --languages=en-US --sourcePath=@app --messagePath=messages ./messages/create_i18n.php
In the newly created file, alter (or create) the array of languages to be translated:
// array, required, list of language codes that the extracted messages
// should be translated to. For example, ['zh-CN', 'de'].
'languages' => [
'en-US',
'fr',
'pt'
],
If necessary, change the root directory in create_i18n.php to point to the messages directory - the default is messages. Note, if the above file is in the messages directory (recommended) then don't alter this 'messagePath' => __DIR__,. If you alter the directory for messages to, say, /config/ (not a good idea) you can use the following:
// Root directory containing message translations.
'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'config',
The created file should look something like this after editing the languages you need:
<?php
return [
// string, required, root directory of all source files
'sourcePath' => __DIR__ . DIRECTORY_SEPARATOR . '..',
// array, required, list of language codes (in alphabetical order) that the extracted messages
// should be translated to. For example, ['zh-CN', 'de'].
'languages' => [
// to localise a particular language use the language code followed by the dialect in CAPS
'en-US', // USA English
'es',
'fr',
'it',
'pt',
],
/* 'languages' => [
'af', 'ar', 'az', 'be', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'el', 'es', 'et', 'fa', 'fi', 'fr', 'he', 'hi',
'pt-BR', 'ro', 'hr', 'hu', 'hy', 'id', 'it', 'ja', 'ka', 'kk', 'ko', 'kz', 'lt', 'lv', 'ms', 'nb-NO', 'nl',
'pl', 'pt', 'ru', 'sk', 'sl', 'sr', 'sr-Latn', 'sv', 'tg', 'th', 'tr', 'uk', 'uz', 'uz-Cy', 'vi', 'zh-CN',
'zh-TW'
], */
// string, the name of the function for translating messages.
// Defaults to 'Yii::t'. This is used as a mark to find the messages to be
// translated. You may use a string for single function name or an array for
// multiple function names.
'translator' => ['\Yii::t', 'Yii::t'],
// boolean, whether to sort messages by keys when merging new messages
// with the existing ones. Defaults to false, which means the new (untranslated)
// messages will be separated from the old (translated) ones.
'sort' => false,
// boolean, whether to remove messages that no longer appear in the source code.
// Defaults to false, which means these messages will NOT be removed.
'removeUnused' => false,
// boolean, whether to mark messages that no longer appear in the source code.
// Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks.
'markUnused' => true,
// array, list of patterns that specify which files (not directories) should be processed.
// If empty or not set, all files will be processed.
// See helpers/FileHelper::findFiles() for pattern matching rules.
// If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
'only' => ['*.php'],
// array, list of patterns that specify which files/directories should NOT be processed.
// If empty or not set, all files/directories will be processed.
// See helpers/FileHelper::findFiles() for pattern matching rules.
// If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
'except' => [
'.*',
'/.*',
'/messages',
'/migrations',
'/tests',
'/runtime',
'/vendor',
'/BaseYii.php',
],
// 'php' output format is for saving messages to php files.
'format' => 'php',
// Root directory containing message translations.
'messagePath' => __DIR__,
// boolean, whether the message file should be overwritten with the merged messages
'overwrite' => true,
/*
// File header used in generated messages files
'phpFileHeader' => '',
// PHPDoc used for array of messages with generated messages files
'phpDocBlock' => null,
*/
/*
// Message categories to ignore
'ignoreCategories' => [
'yii',
],
*/
/*
// 'db' output format is for saving messages to database.
'format' => 'db',
// Connection component to use. Optional.
'db' => 'db',
// Custom source message table. Optional.
// 'sourceMessageTable' => '{{%source_message}}',
// Custom name for translation message table. Optional.
// 'messageTable' => '{{%message}}',
*/
/*
// 'po' output format is for saving messages to gettext po files.
'format' => 'po',
// Root directory containing message translations.
'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
// Name of the file that will be used for translations.
'catalog' => 'messages',
// boolean, whether the message file should be overwritten with the merged messages
'overwrite' => true,
*/
];
/config/web.php file ¶In the web.php file, below 'id' => 'basic', add:
'language' => 'en',
'sourceLanguage' => 'en',
Note: you should always use the 'sourceLanguage' => 'en' as it is, usually, easier and cheaper to translate from English into another language. If the sourceLanguage is not set it defaults to 'en'.
Add the following to the 'components' => [...] section:
'i18n' => [
'translations' => [
'app*' => [
'class' => 'yii\i18n\PhpMessageSource', // Using text files (usually faster) for the translations
//'basePath' => '@app/messages', // Uncomment and change this if your folder is not called 'messages'
'sourceLanguage' => 'en',
'fileMap' => [
'app' => 'app.php',
'app/error' => 'error.php',
],
// Comment out in production version
// 'on missingTranslation' => ['app\components\TranslationEventHandler', 'handleMissingTranslation'],
],
],
],
Now tell Yii which text you want to translate in your view files. This is done by adding Yii::t('app', 'text to be translated') to the code.
For example, in /views/layouts/main.php, change the menu labels like so:
'items' => [
// ['label' => 'Home', 'url' => ['/site/index']], // Orignal code
['label' => Yii::t('app', 'Home'), 'url' => ['/site/index']],
['label' => Yii::t('app', 'About'), 'url' => ['/site/about']],
['label' => Yii::t('app', 'Contact'), 'url' => ['/site/contact']],
Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'Login'), 'url' => ['/site/login']] : '<li class="nav-item">'
. Html::beginForm(['/site/logout'])
. Html::submitButton(
// 'Logout (' . Yii::$app->user->identity->username . ')', // change this line as well to the following:
Yii::t('app', 'Logout ({username})'), ['username' => Yii::$app->user->identity->username]),
['class' => 'nav-link btn btn-link logout']
)
. Html::endForm()
. '</li>',
],
To create the translation files, run the following, in Terminal, from the root directory of your project:
./yii message ./messages/create_i18n.php
Now, get the messages translated. For example in the French /messages/fr/app.php
'Home' => 'Accueil',
'About' => 'À propos',
...
This takes a number of steps.
A key and a name is required for each language.
The key is the ICU language code ISO 639.1 in lowercase (with optional Country code ISO 3166 in uppercase) e.g.
French:
fror French Canada:fr-CAPortuguese:
ptor Portuguese Brazil:pt-BR
The name is the name of the language in that language. e.g. for French: 'Français', for Japanese: '日本の'. This is important as the user may not understand the browser's current language.
In /config/params.php create an array named languages with the languages required. For example:
/* List of languages and their codes
*
* format:
* 'Language Code' => 'Language Name',
* e.g.
* 'fr' => 'Français',
*
* please use alphabetical order of language code
* Use the language name in the "user's" Language
* e.g.
* 'ja' => '日本の',
*/
'languages' => [
// 'da' => 'Danske',
// 'de' => 'Deutsche',
// 'en' => 'English', // NOT REQUIRED the sourceLanguage (i.e. the default)
'en-GB' => 'British English',
'en-US' => 'American English',
'es' => 'Español',
'fr' => 'Français',
'it' => 'Italiano',
// 'ja' => '日本の', // Japanese with the word "Japanese" in Kanji
// 'nl' => 'Nederlandse',
// 'no' => 'Norsk',
// 'pl' => 'Polski',
'pt' => 'Português',
// 'ru' => 'Русский',
// 'sw' => 'Svensk',
// 'zh' => '中国的',
],
In /controllers/SiteController.php, the default controller, add an "Action" named actionLanguage(). This "Action" changes the language and sets a cookie so the browser "remembers" the language for page requests and return visits to the site.
/**
* Called by the ajax handler to change the language and
* Sets a cookie based on the language selected
*
*/
public function actionLanguage()
{
$lang = Yii::$app->request->post('lang');
// If the language "key" is not NULL and exists in the languages array in params.php, change the language and set the cookie
if ($lang !== NULL && array_key_exists($lang, Yii::$app->params['languages']))
{
$expire = time() + (60 * 60 * 24 * 365); // 1 year - alter accordingly
Yii::$app->language = $lang;
$cookie = new yii\web\Cookie([
'name' => 'lang',
'value' => $lang,
'expire' => $expire,
]);
Yii::$app->getResponse()->getCookies()->add($cookie);
}
Yii::$app->end();
}
Remember to set the method to POST. In behaviors(), under actions, set 'language' => ['post'], like so:
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'logout' => ['post'],
'language' => ['post'],
],
],
Make sure that the correct language is served for each request.
In the /components/ directory, create a file named: LanguageHandler.php and add the following code to it:
<?php
/*
* Copyright ©2023 JQL all rights reserved.
* http://www.jql.co.uk
*/
/*
Created on : 19-Nov-2023, 13:23:54
Author : John Lavelle
Title : LanguageHandler
*/
namespace app\components;
use yii\helpers\Html;
class LanguageHandler extends \yii\base\Behavior
{
public function events()
{
return [\yii\web\Application::EVENT_BEFORE_REQUEST => 'handleBeginRequest'];
}
public function handleBeginRequest($event)
{
if (\Yii::$app->getRequest()->getCookies()->has('lang') && array_key_exists(\Yii::$app->getRequest()->getCookies()->getValue('lang'), \Yii::$app->params['languages']))
{
// Get the language from the cookie if set
\Yii::$app->language = \Yii::$app->getRequest()->getCookies()->getValue('lang');
}
else
{
// Use the browser language - note: some systems use an underscore, if used, change it to a hyphen
\Yii::$app->language = str_replace('_', '-', HTML::encode(locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE'])));
}
}
}
/* End of file LanguageHandler.php */
/* Location: ./components/LanguageHandler.php */
LanguageHandler.php from /config/web.php ¶"Call" the LanguageHandler.php file from /config/web.php by adding the following to either just above or just below 'params' => $params,
// Update the language on selection
'as beforeRequest' => [
'class' => 'app\components\LanguageHandler',
],
/views/layouts/main.php ¶main.php uses Bootstrap to create the menu. An item (Dropdown) needs to be added to the menu to allow the user to select a language.
Add use yii\helpers\Url; to the "uses" section of main.php.
Just above echo Nav::widget([...]) add the following code:
// Get the languages and their keys, also the current route
foreach (Yii::$app->params['languages'] as $key => $language)
{
$items[] = [
'label' => $language, // Language name in it's language - already translated
'url' => Url::to(['site/index']), // Route
'linkOptions' => ['id' => $key, 'class' => 'language'], // The language "key"
];
}
In the section:
echo Nav::widget([...])`
between
'options' => ['class' => 'navbar-nav ms-auto'], // ms-auto aligns the menu right`
and
'items' => [...]
add:
'encodeLabels' => false, // Required to enter HTML into the labels
like so:
echo Nav::widget([
'options' => ['class' => 'navbar-nav ms-auto'], // ms-auto aligns the menu right
'encodeLabels' => false, // Required to enter HTML into the labels
'items' => [
['label' => Yii::t('app', 'Home'), 'url' => ['/site/index']],
...
Now add the Dropdown. This can be placed anywhere in 'items' => [...].
// Dropdown Nav Menu: https://www.yiiframework.com/doc/api/2.0/yii-widgets-menu
[
'label' => Yii::t('app', 'Language')),
'url' => ['#'],
'options' => ['class' => 'language', 'id' => 'languageTop'],
'encodeLabels' => false, // Optional but required to enter HTML into the labels for images
'items' => $items, // add the languages into the Dropdown
],
The code in main.php for the NavBar should look something like this:
NavBar::begin([
'brandLabel' => Yii::$app->name, // set in /config/web.php
'brandUrl' => Yii::$app->homeUrl,
'options' => ['class' => 'navbar-expand-md navbar-dark bg-dark fixed-top']
]);
// Get the languages and their keys, also the current route
foreach (Yii::$app->params['languages'] as $key => $language)
{
$items[] = [
'label' => $language, // Language name in it's language
'url' => Url::to(['site/index']), // Current route so the page refreshes
'linkOptions' => ['id' => $key, 'class' => 'language'], // The language key
];
}
echo Nav::widget([
'options' => ['class' => 'navbar-nav ms-auto'], // ms-auto aligns the menu right
'encodeLabels' => false, // Required to enter HTML into the labels
'items' => [
['label' => Yii::t('app', 'Home'), 'url' => ['/site/index']],
['label' => Yii::t('app', 'About'), 'url' => ['/site/about']],
['label' => Yii::t('app', 'Contact'), 'url' => ['/site/contact']],
// Dropdown Nav Menu: https://www.yiiframework.com/doc/api/2.0/yii-widgets-menu
[
'label' => Yii::t('app', 'Language') ,
'url' => ['#'],
'options' => ['class' => 'language', 'id' => 'languageTop'],
'encodeLabels' => false, // Required to enter HTML into the labels
'items' => $items, // add the languages into the Dropdown
],
Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'Login'), 'url' => ['/site/login']] : '<li class="nav-item">'
. Html::beginForm(['/site/logout'])
. Html::submitButton(
// 'Logout (' . Yii::$app->user->identity->username . ')',
Yii::t('app', 'Logout ({username})', ['username' => Yii::$app->user->identity->username]),
['class' => 'nav-link btn btn-link logout']
)
. Html::endForm()
. '</li>',
],
]);
NavBar::end();
If Language flags or images are required next to the language name see Optional Items at the end of this document.
To call the Language Action actionLanguage() make an Ajax call in a JavaScript file.
Create a file in /web/js/ named language.js.
Add the following code to the file:
/*
* Copyright ©2023 JQL all rights reserved.
* http://www.jql.co.uk
*/
/**
* Set the language
*
* @returns {undefined}
*/
$(function () {
$(document).on('click', '.language', function (event) {
event.preventDefault();
let lang = $(this).attr('id'); // Get the language key
/* if not the top level, set the language and reload the page */
if (lang !== 'languageTop') {
$.post(document.location.origin + '/site/language', {'lang': lang}, function (data) {
location.reload(true);
});
}
});
});
To add the JavaScript file to the Assets, alter /assets/AppAsset.php in the project directory. In public $js = [] add 'js/language.js', like so:
public $js = [
'js/language.js',
];
Internationalisation should now be working on your project.
The following are optional but may help both you and/or the user.
Yii can check whether a translation is present for a particular piece of text in a Yii::t('app', 'text to be translated') block.
There are two steps:
A. In /config/web.php uncomment the following line:
// 'on missingTranslation' => ['app\components\TranslationEventHandler', 'handleMissingTranslation'],
B. Create a TranslationEventHandler:
In /components/ create a file named: TranslationEventHandler.php and add the following code to it:
<?php
/**
* TranslationEventHandler
*
* @copyright © 2023, John Lavelle Created on : 14 Nov 2023, 16:05:32
*
*
* Author : John Lavelle
* Title : TranslationEventHandler
*/
// Change the Namespace (app, frontend, backend, console etc.) if necessary (default in Yii Basic is "app").
namespace app\components;
use yii\i18n\MissingTranslationEvent;
/**
* TranslationEventHandler
*
*
* @author John Lavelle
* @since 1.0 // Update version number
*/
class TranslationEventHandler
{
/**
* Adds a message to missing translations in Development Environment only
*
* @param MissingTranslationEvent $event
*/
public static function handleMissingTranslation(MissingTranslationEvent $event)
{
// Only check in the development environment
if (YII_ENV_DEV)
{
$event->translatedMessage = "@MISSING: {$event->category}.{$event->message} FOR LANGUAGE {$event->language} @";
}
}
}
If there is a missing translation, the text is replaced with a message similar to the following text:
@MISSING: app.Logout (John) FOR LANGUAGE fr @
Here Yii has found that there is no French translation for:
Yii::t('app', 'Logout ({username})', ['username' => Yii::$app->user->identity->username]),
This is very useful and recommended as it aids the User to locate the correct language. There are a number of steps for this.
a. Create images of the flags.
The images should be 25px wide by 15px high. The images must have the same name as the language key in the language array in params.php. For example: fr.png or en-US.png. If the images are not of type ".png" change the code in part b. below to the correct file extension.
Place the images in a the directory /web/images/flags/.
b. Alter the code in /views/layouts/main.php so that the code for the "NavBar" reads as follows:
<header id="header">
<?php
NavBar::begin([
'brandLabel' => Yii::$app->name,
'brandUrl' => Yii::$app->homeUrl,
'options' => ['class' => 'navbar-expand-md navbar-dark bg-dark fixed-top']
]);
// Get the languages and their keys, also the current route
foreach (Yii::$app->params['languages'] as $key => $language)
{
$items[] = [
// Display the image before the language name
'label' => Html::img('/images/flags/' . $key . '.png', ['alt' => 'flag ' . $language, 'class' => 'inline-block align-middle', 'title' => $language,]) . ' ' . $language, // Language name in it's language
'url' => Url::to(['site/index']), // Route
'linkOptions' => ['id' => $key, 'class' => 'language'], // The language key
];
}
echo Nav::widget([
'options' => ['class' => 'navbar-nav ms-auto'], // ms-auto aligns the menu right
'encodeLabels' => false, // Required to enter HTML into the labels
'items' => [
['label' => Yii::t('app', 'Home'), 'url' => ['/site/index']],
['label' => Yii::t('app', 'About'), 'url' => ['/site/about']],
['label' => Yii::t('app', 'Contact'), 'url' => ['/site/contact']],
// Dropdown Nav Menu: https://www.yiiframework.com/doc/api/2.0/yii-widgets-menu
[
// Display the current language "flag" after the Dropdown title (before the caret)
'label' => Yii::t('app', 'Language') . ' ' . Html::img('@web/images/flags/' . Yii::$app->language . '.png', ['class' => 'inline-block align-middle', 'title' => Yii::$app->language]),
'url' => ['#'],
'options' => ['class' => 'language', 'id' => 'languageTop'],
'encodeLabels' => false, // Required to enter HTML into the labels
'items' => $items, // add the languages into the Dropdown
],
Yii::$app->user->isGuest ? ['label' => Yii::t('app', 'Login'), 'url' => ['/site/login']] : '<li class="nav-item">'
. Html::beginForm(['/site/logout'])
. Html::submitButton(
// 'Logout (' . Yii::$app->user->identity->username . ')',
Yii::t('app', 'Logout ({username})', ['username' => Yii::$app->user->identity->username]),
['class' => 'nav-link btn btn-link logout']
)
. Html::endForm()
. '</li>',
],
]);
NavBar::end();
?>
</header>
That's it! Enjoy...
For further reading and information see:
Yii2 Internationalization Tutorial
If you use this code, please credit me as follows:
Internationalization (i18n) Menu code provided by JQL, https://visualaccounts.co.uk ©2023 JQL
Licence (BSD-3-Clause Licence)
Copyright Notice
Internationalization (i18n) Menu code provided by JQL, https://visualaccounts.co.uk ©2023 JQL all rights reserved
Redistribution and use in source and binary forms with or without modification are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the names of John Lavelle, JQL, Visual Accounts nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
"ALL JQL CODE & SOFTWARE INCLUDING WORLD WIDE WEB PAGES (AND THOSE OF IT'S AUTHORS) ARE SUPPLIED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE AUTHOR AND PUBLISHER AND THEIR AGENTS SPECIFICALLY DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. WITH RESPECT TO THE CODE, THE AUTHOR AND PUBLISHER AND THEIR AGENTS SHALL HAVE NO LIABILITY WITH RESPECT TO ANY LOSS OR DAMAGE DIRECTLY OR INDIRECTLY ARISING OUT OF THE USE OF THE CODE EVEN IF THE AUTHOR AND/OR PUBLISHER AND THEIR AGENTS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. WITHOUT LIMITING THE FOREGOING, THE AUTHOR AND PUBLISHER AND THEIR AGENTS SHALL NOT BE LIABLE FOR ANY LOSS OF PROFIT, INTERRUPTION OF BUSINESS, DAMAGE TO EQUIPMENT OR DATA, INTERRUPTION OF OPERATIONS OR ANY OTHER COMMERCIAL DAMAGE, INCLUDING BUT NOT LIMITED TO DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL OR OTHER DAMAGES."
]]>There are Multiple Ways to Create a Validator But here we use Regular Expression or JavaScript Regular Expression or RegExp for Creation Validators. In this article, we will see the most Frequently Used Expression
Step 1 : Create a New Class for Validator like below or Validator
See First Example 10 Digit Mobile Number Validation
<?php
namespace common\validators;
use yii\validators\Validator;
class MobileValidator extends Validator {
public function validateAttribute($model, $attribute) {
if (isset($model->$attribute) and $model->$attribute != '') {
if (!preg_match('/^[123456789]\d{9}$/', $model->$attribute)) {
$this->addError($model, $attribute, 'In Valid Mobile / Phone number');
}
}
}
}
Here We can Writee Diffrent Diffrent Regular Expression as Per Requirement
`php
preg_match('/^[123456789]\d{9}$/', $model->$attribute)
`
Step 2: How tO Use Validator
I Hope Everyone Know How to use a validator but here is a example how to use it.
Add a New Rule in your Model Class Like this
`php
[['mobile'],\common\validators\MobileValidator::class],
[['mobile'], 'string', 'max' => 10],
So It's Very Simple to use a Custom Validator.
As I Told you Earlier that i show you some more Example for Using Regular Expression Validator Just Replace these string in preg_match.
1. Aadhar Number Validator
```php
preg_match('/^[2-9]{1}[0-9]{3}[0-9]{4}[0-9]{4}$/', $model->$attribute)
Bank Account Number Validator
`php
preg_match("/^[0-9]{9,18}+$/", $model->$attribute)
`
Bank IFSC Code Validator
`php
preg_match("/^[A-Z]{4}0[A-Z0-9]{6}$/", $model->$attribute)
`
Pan Card Number Validator
`php
preg_match('/^([a-zA-Z]){5}([0-9]){4}([a-zA-Z]){1}?$/', $model->$attribute)
`
Pin Code Validator
`php
preg_match('/^[0-9]{6}+$/', $model->$attribute)
`
GSTIN Validator
`php
preg_match("/^([0][1-9]|[1-2][0-9]|[3][0-5])([a-zA-Z]{5}[0-9]{4}[a-zA-Z]{1}[1-9a-zA-Z]{1}[zZ]{1}[0-9a-zA-Z]{1})+$/", $model->$attribute)
`
This is Other Type of Custom Validator
<?php
namespace common\validators;
use yii\validators\Validator;
/**
* Class Word500Validator
* @author Aayush Saini <aayushsaini9999@gmail.com>
*/
class Word500Validator extends Validator
{
public function validateAttribute($model, $attribute)
{
if ($model->$attribute != '') {
if (str_word_count($model->$attribute) > 500) {
$this->addError($model, $attribute, $model->getAttributeLabel($attribute) . ' length can not exceeded 500 words.');
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
return $model->errors;
}
}
}
}
Now I assume that after reading this article you can create any type of validator as per your Requirement.
:) Thanks for Reading
]]>GridView show sum of columns in footer
`PHP
use yii\grid\DataColumn;
/**
@author shiv / class TSumColumn extends DataColumn { public function getDataCellValue($model, $key, $index) {
$value = parent::getDataCellValue($model, $key, $index);
if ( is_numeric($value))
{
$this->footer += $value;
}
return $value;
}
}
`
Now you have to enable footer in GridView
echo GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'showFooter' => true,
Also change the coulmn class
[
'class' => TSumColumn::class,
'attribute' => 'amount'
],
You would see the total in footer of the grid. you can apply this to multiple columns if need
]]>I have a calls which help me display json directly in html table.
Json2Table::formatContent($json);
The code of Json2Table class:
/**
* Class convert Json to html table. It help view json data directly.
* @author shiv
*
*/
class Json2Table
{
public static function formatContent($content, $class = 'table table-bordered')
{
$html = "";
if ($content != null) {
$arr = json_decode(strip_tags($content), true);
if ($arr && is_array($arr)) {
$html .= self::arrayToHtmlTableRecursive($arr, $class);
}
}
return $html;
}
public static function arrayToHtmlTableRecursive($arr, $class = 'table table-bordered')
{
$str = "<table class='$class'><tbody>";
foreach ($arr as $key => $val) {
$str .= "<tr>";
$str .= "<td>$key</td>";
$str .= "<td>";
if (is_array($val)) {
if (! empty($val)) {
$str .= self::arrayToHtmlTableRecursive($val, $class);
}
} else {
$val = nl2br($val);
$str .= "<strong>$val</strong>";
}
$str .= "</td></tr>";
}
$str .= "</tbody></table>";
return $str;
}
}
]]>In India have Aadhar number an we may need to valid it a input. So I created a validator for yii2
use yii\validators\Validator;
class TAadharNumberValidator extends Validator
{
public $regExPattern = '/^\d{4}\s\d{4}\s\d{4}$/';
public function validateAttribute($model, $attribute)
{
if (preg_match($this->regExPattern, $model->$attribute)) {
$model->addError($attribute, 'Not valid Aadhar Card Number');
}
}
}
]]>Hey Everyone, In this post I Just shared my Experience what most of interviewer ask in YII2 Interview.
These are most common question a interviewer can be asked to you if you are going to a Interview.
If anyone have other question please share in comments!!!!
Searching the Answers of these Question Find on Dynamic Duniya
]]>One of my sites has been flooded with spam bots and as a result - Gmail gave my mailing domain a bad score and I couldn't send emails to @gmail addresses anymore, not from my email, not from my system, not from any of other domains and websites I host...
I did remove all the spambots activity from one of my sites, appealed the decision via Gmail support forums, but still, I'm blocked from contacting my customers that has mailboxes at @gmail.com and there seems to be no way to change the domain score back to where it was.
It's been almost 2 weeks and my domain score is stuck at bad in https://postmaster.google.com/
Thanks @Google :(
As a result, I had to figure way out to send purchases, expired licenses, and other notifications to my customers.
I'm using PHP Yii2 framework and it turns out it was a breeze.
We need a @gmail.com account to send the notifications. One thing is important. After you create the account, you need to enable Less Secure Apps Access option:

It allows us to send emails via Gmail SMTP server.
In your Yii2 framework directory, modify your configuration file /common/config/Main.php (I'm using Advanced Theme) and include custom mailing component (name it however you want):
<?php
return [
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
...
'components' => [
'mailerGmail' => [
'class' => 'yii\swiftmailer\Mailer',
'viewPath' => '@common/mail',
'useFileTransport' => false,
'transport' => [
'class' => 'Swift_SmtpTransport',
'host' => 'smtp.gmail.com',
'username' => 'gmail.helper.account',
'password' => 'PUT-YOUR-PASSWORD-HERE',
'port' => '587',
'encryption' => 'tls',
],
],
],
];
I have added a helper function to one of my components registered as Yii::$app->Custom. It returns default mailer instance depending on the delivery email domain name.
I have also updated the code to detect the cases where the email doesn't contain @gmail.com string in it but still is using Gmail MX servers to handle emailing.
Detection is based on checking domain mailing server records using PHP built-in function getmxrr() and if that fails I send remote GET query to Google DNS service API to check the MX records.
////////////////////////////////////////////////////////////////////////////////
//
// get default mailer depending on the provided email address
//
////////////////////////////////////////////////////////////////////////////////
public function getMailer($email)
{
// detect if the email or domain is using Gmail to send emails
if (Yii::$app->params['forwardGmail'])
{
// detect @gmail.com domain first
if (str_ends_with($email, "@gmail.com"))
{
return Yii::$app->mailerGmail;
}
// extract domain name
$parts = explode('@', $email);
$domain = array_pop($parts);
// check DNS using local server requests to DNS
// if it fails query Google DNS service API (might have limits)
if (getmxrr($domain, $mx_records))
{
foreach($mx_records as $record)
{
if (stripos($record, "google.com") !== false || stripos($record, "googlemail.com") !== false)
{
return Yii::$app->mailerGmail;
}
}
// return default mailer (if there were records detected but NOT google)
return Yii::$app->mailer;
}
// make DNS request
$client = new Client();
$response = $client->createRequest()
->setMethod('GET')
->setUrl('https://dns.google.com/resolve')
->setData(['name' => $domain, 'type' => 'MX'])
->setOptions([
'timeout' => 5, // set timeout to 5 seconds for the case server is not responding
])
->send();
if ($response->isOk)
{
$parser = new JsonParser();
$data = $parser->parse($response);
if ($data && array_key_exists("Answer", $data))
{
foreach ($data["Answer"] as $key => $value)
{
if (array_key_exists("name", $value) && array_key_exists("data", $value))
{
if (stripos($value["name"], $domain) !== false)
{
if (stripos($value["data"], "google.com") !== false || stripos($value["data"], "googlemail.com") !== false)
{
return Yii::$app->mailerGmail;
}
}
}
}
}
}
}
// return default mailer
return Yii::$app->mailer;
}
If the domain ends with @gmail.com or the domain is using Gmail mailing systems the mailerGmail instance is used, otherwise the default mailing component Yii::$app->mailer is used.
/**
* Sends an email to the specified email address using the information collected by this model.
*
* @return boolean whether the email was sent
*/
public function sendEmail()
{
// find all active subscribers
$message = Yii::$app->Custom->getMailer($this->email)->compose();
$message->setTo([$this->email => $this->name]);
$message->setFrom([\Yii::$app->params['supportEmail'] => "Bartosz Wójcik"]);
$message->setSubject($this->subject);
$message->setTextBody($this->body);
$headers = $message->getSwiftMessage()->getHeaders();
// message ID header (hide admin panel)
$msgId = $headers->get('Message-ID');
$msgId->setId(md5(time()) . '@pelock.com');
$result = $message->send();
return $result;
}
This is only the temporary solution and you need to be aware you won't be able to send bulk mail with this method, Gmail enforces some limitations on fresh mailboxes too.
It seems if your domain lands on that bad reputation scale there isn't any easy way out of it. I read on Gmail support forums, some people wait for more than a month for Gmail to unlock their domains without any result and communication back. My domain is not listed in any other blocked RBL lists (spam lists), it's only Gmail blocking it, but it's enough to understand how influential Google is, it can ruin your business in a second without a chance to fix it...
]]>JWT is short for JSON Web Token. It is used eg. instead of sessions to maintain a login in a browser that is talking to an API - since browser sessions are vulnerable to CSRF security issues. JWT is also less complicated than setting up an OAuth authentication mechanism.
The concept relies on two tokens:
This token is generated using \sizeg\jwt\Jwt::class
It is not stored server side, and is sent on all subsequent API requests through the Authorization header
How is the user identified then? Well, the JWT contents contain the user ID. We trust this value blindly.
This token is generated upon login only, and is stored in the table user_refresh_token.
A user may have several RefreshToken in the database.
/auth/login endpoint: ¶In our actionLogin() method two things happens, if the credentials are correct:
httpOnly cookie,
restricted to the /auth/refresh-token path.The JWT is stored in the browser's localStorage, and have to be sent on all requests from now on.
The RefreshToken is in your cookies, but can't be read/accessed/tempered with through Javascript (since it is httpOnly).
After some time, the JWT will eventually expire. Your API have to return 401 - Unauthorized in this case.
In your app's HTTP client (eg. Axios), add an interceptor, which detects the 401 status, stores the failing request in a queue,
and calls the /auth/refresh-token endpoint.
When called, this endpoint will receive the RefreshToken via the cookie. You then have to check in your table if this is a valid RefreshToken, who is the associated user ID, generate a new JWT and send it back as JSON.
Your HTTP client must take this new JWT, replace it in localStorage, and then cycle through the request queue and replay all failed requests.
If you set up an /auth/sessions endpoint, that returns all the current user's RefreshTokens, you can then display
a table of all connected devices.
You can then allow the user to remove a row (i.e. DELETE a particular RefreshToken from the table). When the compromised token expires (after eg. 5 min) and the renewal is attempted, it will fail. This is why we want the JWT to be really short lived.
This is by design the purpose of JWT. It is secure enough to be trustable. In big setups (eg. Google), the Authentication is handled by a separate authentication server. It's responsible for accepting a login/password in exchange for a token.
Later, in Gmail for example, no authentication is performed at all. Google reads your JWT and give you access to your email, provided your JWT is not dead. If it is, you're redirected to the authentication server.
This is why when Google authentication had a failure some time ago - some users were able to use Gmail without any problems, while others couldn't connect at all - JWT still valid versus an outdated JWT.
https enabled site is required for the HttpOnly cookie to work cross-siteCREATE TABLE `user_refresh_tokens` (
`user_refresh_tokenID` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`urf_userID` INT(10) UNSIGNED NOT NULL,
`urf_token` VARCHAR(1000) NOT NULL,
`urf_ip` VARCHAR(50) NOT NULL,
`urf_user_agent` VARCHAR(1000) NOT NULL,
`urf_created` DATETIME NOT NULL COMMENT 'UTC',
PRIMARY KEY (`user_refresh_tokenID`)
)
COMMENT='For JWT authentication process';
composer require sizeg/yii2-jwtAuthController.php. You can name it what you want.Create an ActiveRecord model for the table user_refresh_tokens. We'll use the class name app\models\UserRefreshToken.
Disable CSRF validation on all your controllers:
Add this property: public $enableCsrfValidation = false;
/config/params.php:'jwt' => [
'issuer' => 'https://api.example.com', //name of your project (for information only)
'audience' => 'https://frontend.example.com', //description of the audience, eg. the website using the authentication (for info only)
'id' => 'UNIQUE-JWT-IDENTIFIER', //a unique identifier for the JWT, typically a random string
'expire' => 300, //the short-lived JWT token is here set to expire after 5 min.
],
JwtValidationData class in /components which uses the parameters we just set:<?php
namespace app\components;
use Yii;
class JwtValidationData extends \sizeg\jwt\JwtValidationData {
/**
* @inheritdoc
*/
public function init() {
$jwtParams = Yii::$app->params['jwt'];
$this->validationData->setIssuer($jwtParams['issuer']);
$this->validationData->setAudience($jwtParams['audience']);
$this->validationData->setId($jwtParams['id']);
parent::init();
}
}
/config/web.php for initializing JWT authentication: $config = [
'components' => [
...
'jwt' => [
'class' => \sizeg\jwt\Jwt::class,
'key' => 'SECRET-KEY', //typically a long random string
'jwtValidationData' => \app\components\JwtValidationData::class,
],
...
],
];
AuthController.php we must exclude actions that do not require being authenticated, like login, refresh-token, options (when browser sends the cross-site OPTIONS request). public function behaviors() {
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => \sizeg\jwt\JwtHttpBearerAuth::class,
'except' => [
'login',
'refresh-token',
'options',
],
];
return $behaviors;
}
generateJwt() and generateRefreshToken() to AuthController.php. We'll be using them in the login/refresh-token actions.
Adjust class name for your user model if different. private function generateJwt(\app\models\User $user) {
$jwt = Yii::$app->jwt;
$signer = $jwt->getSigner('HS256');
$key = $jwt->getKey();
$time = time();
$jwtParams = Yii::$app->params['jwt'];
return $jwt->getBuilder()
->issuedBy($jwtParams['issuer'])
->permittedFor($jwtParams['audience'])
->identifiedBy($jwtParams['id'], true)
->issuedAt($time)
->expiresAt($time + $jwtParams['expire'])
->withClaim('uid', $user->userID)
->getToken($signer, $key);
}
/**
* @throws yii\base\Exception
*/
private function generateRefreshToken(\app\models\User $user, \app\models\User $impersonator = null): \app\models\UserRefreshToken {
$refreshToken = Yii::$app->security->generateRandomString(200);
// TODO: Don't always regenerate - you could reuse existing one if user already has one with same IP and user agent
$userRefreshToken = new \app\models\UserRefreshToken([
'urf_userID' => $user->id,
'urf_token' => $refreshToken,
'urf_ip' => Yii::$app->request->userIP,
'urf_user_agent' => Yii::$app->request->userAgent,
'urf_created' => gmdate('Y-m-d H:i:s'),
]);
if (!$userRefreshToken->save()) {
throw new \yii\web\ServerErrorHttpException('Failed to save the refresh token: '. $userRefreshToken->getErrorSummary(true));
}
// Send the refresh-token to the user in a HttpOnly cookie that Javascript can never read and that's limited by path
Yii::$app->response->cookies->add(new \yii\web\Cookie([
'name' => 'refresh-token',
'value' => $refreshToken,
'httpOnly' => true,
'sameSite' => 'none',
'secure' => true,
'path' => '/v1/auth/refresh-token', //endpoint URI for renewing the JWT token using this refresh-token, or deleting refresh-token
]));
return $userRefreshToken;
}
AuthController.php: public function actionLogin() {
$model = new \app\models\LoginForm();
if ($model->load(Yii::$app->request->getBodyParams()) && $model->login()) {
$user = Yii::$app->user->identity;
$token = $this->generateJwt($user);
$this->generateRefreshToken($user);
return [
'user' => $user,
'token' => (string) $token,
];
} else {
return $model->getFirstErrors();
}
}
AuthController.php. Call POST /auth/refresh-token when JWT has expired,
and call DELETE /auth/refresh-token when user requests a logout (and then delete the JWT token from client's localStorage). public function actionRefreshToken() {
$refreshToken = Yii::$app->request->cookies->getValue('refresh-token', false);
if (!$refreshToken) {
return new \yii\web\UnauthorizedHttpException('No refresh token found.');
}
$userRefreshToken = \app\models\UserRefreshToken::findOne(['urf_token' => $refreshToken]);
if (Yii::$app->request->getMethod() == 'POST') {
// Getting new JWT after it has expired
if (!$userRefreshToken) {
return new \yii\web\UnauthorizedHttpException('The refresh token no longer exists.');
}
$user = \app\models\User::find() //adapt this to your needs
->where(['userID' => $userRefreshToken->urf_userID])
->andWhere(['not', ['usr_status' => 'inactive']])
->one();
if (!$user) {
$userRefreshToken->delete();
return new \yii\web\UnauthorizedHttpException('The user is inactive.');
}
$token = $this->generateJwt($user);
return [
'status' => 'ok',
'token' => (string) $token,
];
} elseif (Yii::$app->request->getMethod() == 'DELETE') {
// Logging out
if ($userRefreshToken && !$userRefreshToken->delete()) {
return new \yii\web\ServerErrorHttpException('Failed to delete the refresh token.');
}
return ['status' => 'ok'];
} else {
return new \yii\web\UnauthorizedHttpException('The user is inactive.');
}
}
findIdentityByAccessToken() in your user model to find the authenticated user via the uid claim from the JWT: public static function findIdentityByAccessToken($token, $type = null) {
return static::find()
->where(['userID' => (string) $token->getClaim('uid') ])
->andWhere(['<>', 'usr_status', 'inactive']) //adapt this to your needs
->one();
}
afterSave() in your user model: public function afterSave($isInsert, $changedOldAttributes) {
// Purge the user tokens when the password is changed
if (array_key_exists('usr_password', $changedOldAttributes)) {
\app\models\UserRefreshToken::deleteAll(['urf_userID' => $this->userID]);
}
return parent::afterSave($isInsert, $changedOldAttributes);
}
user_refresh_tokens that belongs to the given user
and allow him to delete the ones he chooses.The Axios interceptor (using React Redux???):
let isRefreshing = false;
let refreshSubscribers: QueuedApiCall[] = [];
const subscribeTokenRefresh = (cb: QueuedApiCall) =>
refreshSubscribers.push(cb);
const onRefreshed = (token: string) => {
console.log("refreshing ", refreshSubscribers.length, " subscribers");
refreshSubscribers.map(cb => cb(token));
refreshSubscribers = [];
};
api.interceptors.response.use(undefined,
error => {
const status = error.response ? error.response.status : false;
const originalRequest = error.config;
if (error.config.url === '/auth/refresh-token') {
console.log('REDIRECT TO LOGIN');
store.dispatch("logout").then(() => {
isRefreshing = false;
});
}
if (status === API_STATUS_UNAUTHORIZED) {
if (!isRefreshing) {
isRefreshing = true;
console.log('dispatching refresh');
store.dispatch("refreshToken").then(newToken => {
isRefreshing = false;
onRefreshed(newToken);
}).catch(() => {
isRefreshing = false;
});
}
return new Promise(resolve => {
subscribeTokenRefresh(token => {
// replace the expired token and retry
originalRequest.headers["Authorization"] = "Bearer " + token;
resolve(axios(originalRequest));
});
});
}
return Promise.reject(error);
}
);
Thanks to Mehdi Achour for helping with much of the material for this tutorial.
]]>Articles are separated into more files as there is the max lenght for each file on wiki.
I already wrote how translations work. Here I will show how language can be switched and saved into the URL. So let's add the language switcher into the main menu:
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => [
['label' => 'Language', 'items' => [
['label' => 'German' , 'url' => \yii\helpers\Url::current(['sys_lang' => 'de']) ],
['label' => 'English', 'url' => \yii\helpers\Url::current(['sys_lang' => 'en']) ],
],
]
Now we need to process the new GET parameter "sys_lang" and save it to Session in order to keep the new language. Best is to create a BaseController which will be extended by all controllers. Its content looks like this:
<?php
namespace app\controllers;
use yii\web\Controller;
class _BaseController extends Controller {
public function beforeAction($action) {
if (isset($_GET['sys_lang'])) {
switch ($_GET['sys_lang']) {
case 'de':
$_SESSION['sys_lang'] = 'de-DE';
break;
case 'en':
$_SESSION['sys_lang'] = 'en-US';
break;
}
}
if (!isset($_SESSION['sys_lang'])) {
$_SESSION['sys_lang'] = \Yii::$app->sourceLanguage;
}
\Yii::$app->language = $_SESSION['sys_lang'];
return true;
}
}
If you want to have the sys_lang in the URL, right behind the domain name, following URL rules can be created in config/web.php:
'components' => [
// ...
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
// https://www.yiiframework.com/doc/api/2.0/yii-web-urlmanager#$rules-detail
// https://stackoverflow.com/questions/2574181/yii-urlmanager-language-in-url
// https://www.yiiframework.com/wiki/294/seo-conform-multilingual-urls-language-selector-widget-i18n
'<sys_lang:[a-z]{2}>' => 'site',
'<sys_lang:[a-z]{2}>/<controller:\w+>' => '<controller>',
'<sys_lang:[a-z]{2}>/<controller:\w+>/<action:\w+>' => '<controller>/<action>',
],
],
],
Now the language-switching links will produce URL like this: http://myweb.com/en/site/index . Without the rules the link would look like this: http://myweb.com/site/index?sys_lang=en . So the rule works in both directions. When URL is parsed and controllers are called, but also when a new URL is created using the URL helper.
I am using Notepad++ for massive changes using Regex. If you press Ctrl+Shift+F you will be able to replace in all files.
Yii::t()
Yii::t('text' , 'text' ) // NO
Yii::t('text','text') // YES
search: Yii::t\('([^']*)'[^']*'([^']*)'[^\)]*\)
replace with: Yii::t\('$1','$2'\)
URLs (in Notepad++)
return $this->redirect('/controller/action')->send(); // NO
return $this->redirect(['controller/action'])->send(); // YES
search: ->redirect\(['][/]([^']*)[']\)
replace: ->redirect\(['$1']\)
====
return $this->redirect('controller/action')->send(); // NO
return $this->redirect(['controller/action'])->send(); // YES
search: ->redirect\((['][^']*['])\)
replace: ->redirect\([$1]\)
PHP short tags
search: (<\?)([^p=]) // <?if ...
replace: $1php $2 // <?php if ...
// note that sometimes <?xml can be found and it is valid, keep it
View usage
search: render(Ajax|Partial)?\s*\(\s*['"]\s*[a-z0-9_\/]*(viewName)
Both Vagrant and Docker create a virtual machine using almost any OS or SW configuration you specify, while the source codes are on your local disk so you can easily modify them in your IDE under your OS.
Can be used not only for PHP development, but in any other situation.
What is this good for? ... Your production server runs a particular environment and you want to develop/test on the same system. Plus you dont have to install XAMPP, LAMP or other servers locally. You just start the virtual and its ready. Plus you can share the configuration of the virtual system with other colleagues so you all work on indentical environment. You can also run locally many different OS systems with different PHP versions etc.
Vagrant and Docker work just like composer or NPM. It is a library of available OS images and other SW and you just pick some combination. Whole configuration is defined in one text-file, named Vagrantfile or docker-compose.yml, and all you need is just a few commands to run it. And debugging is no problem.
Info: This chapter works with PHP 7.0 in ScotchBox. If you need PHP 7.4, read next chapter where CognacBox is used (to be added when tested)
Basic overview and Vagrant configuration:
List of all available OS images for Vagrant is here:
Both Yii demo-applications already contain the Vagrantfile, but its setup is unclear to me - it is too PRO. So I wanted to publish my simplified version which uses OS image named scotch/box and you can use it also for non-yii PHP projects. (It has some advantages, the disadvantage is older PHP in the free version)
The Vagrantfile is stored in the root-folder of your demo-project. My Vagrantfile contains only following commands.
Vagrant.configure("2") do |config|
config.vm.box = "scotch/box"
config.vm.network "private_network", ip: "11.22.33.44"
config.vm.hostname = "scotchbox"
config.vm.synced_folder ".", "/var/www/public", :mount_options => ["dmode=777", "fmode=777"]
config.vm.provision "shell", path: "./vagrant/vagrant.sh", privileged: false
end
# Virtual machine will be available on IP A.B.C.D (in our case 11.22.33.44, see above)
# Virtual can access your host machine on IP A.B.C.1 (this rule is given by Vagrant)
It requires file vagrant/vagrant.sh, because I wanted to enhance the server a bit. It contains following:
# Composer:
# (In case of composer errors, it can help to delete the vendor-folder and composer.lock file)
cd /var/www/public/
composer install
# You can automatically import your SQL (root/root, dbname scotchbox)
#mysql -u root -proot scotchbox < /var/www/public/vagrant/db.sql
# You can run migrations:
#php /var/www/public/protected/yiic.php migrate --interactive=0
# You can create folder and set 777 rights:
#mkdir /var/www/public/assets
#sudo chmod -R 777 /var/www/public/assets
# You can copy a file:
#cp /var/www/public/from.php /var/www/public/to.php
# Installing Xdebug v2 (Xdebug v3 has renamed config params!):
sudo apt-get update
sudo apt-get install php-xdebug
# Configuring Xdebug in php.ini:
# If things do not work, disable your firewall and restart IDE. It might help.
echo "" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "[XDebug]" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_enable=1" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_port=9000" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_autostart=1" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_log=/var/www/public/xdebug.log" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.remote_connect_back=1" | sudo tee -a /etc/php/7.0/apache2/php.ini
echo "xdebug.idekey=netbeans-xdebug" | sudo tee -a /etc/php/7.0/apache2/php.ini
# Important: Make sure that your IDE has identical settings: idekey and remote_port.
# NetBeans: Make sure your project is correctly setup. Right-click the project and select Properties / Run Cofigurations. "Project URL" and "Index file" must have correct values.
# Note:
# Use this if remote_connect_back does not work.
# IP must correspond to the Vagrantfile, only the last number must be 1
#echo "xdebug.remote_handler=dbgp" | sudo tee -a /etc/php/7.0/apache2/php.ini
#echo "xdebug.remote_host=11.22.33.1" | sudo tee -a /etc/php/7.0/apache2/php.ini
sudo service apache2 restart
... so create both files in your project ...
If you want to manually open php.ini and paste this text, you can copy it from here:
// sudo nano /etc/php/7.0/apache2/php.ini
// (Xdebug v3 has renamed config params!)
[XDebug]
xdebug.remote_enable=1
xdebug.remote_port=9000
xdebug.remote_autostart=1
xdebug.remote_log=/var/www/public/xdebug.log
xdebug.remote_connect_back=1
xdebug.idekey=netbeans-xdebug
// Important: Make sure that your IDE has identical settings: idekey and remote_port.
// NetBeans: Make sure your project is correctly setup. Right-click the project and select Properties / Run Cofigurations. "Project URL" and "Index file" must have correct values.
To debug in PhpStorm check this video.
To connect to MySQL via PhpStorm check this comment by MilanG
Installing and using Vagrant:
First install Vagrant and VirtualBox, please.
Note: Sadly, these days VirtualBox does not work on the ARM-based Macs with the M1 chip. Use Docker in that case.
Important: If command "vagrant ssh" wants a password, enter "vagrant".
Now just open your command line, navigate to your project and you can start:
Once virtual is running, you can call also these:
In the Linux shell you can call any command you want.
In "scotch/box" I do not use PhpMyAdmin , but Adminer. It is one simple PHP script and it will run without any installations. Just copy the adminer.php script to your docroot and access it via browser. Use the same login as in configurafion of Yii. Server will be localhost.
Note: I am showing the advanced application. Basic application will not be too different I think. Great Docker tutorial is here
Yii projects are already prepared for Docker. To start you only have to install Docker from www.docker.com and you can go on with this manual.
Note: init and composer can be called locally, not necessarily via Docker. They only add files to your folder.
Now you will be able to open URLs:
Open common/config/main-local.php and set following DB connection:
Run migrations using one of following commands:
Now go to Frontend and click "signup" in the right upper corner
Second way is to directly modify table in DB:
Now you have your account and you can log in to Backend
Just add section environment to docker-compose.yml like this:
services:
frontend:
build: frontend
ports:
- 20080:80
volumes:
# Re-use local composer cache via host-volume
- ~/.composer-docker/cache:/root/.composer/cache:delegated
# Mount source-code for development
- ./:/app
environment:
PHP_ENABLE_XDEBUG: 1
XDEBUG_CONFIG: "client_port=9000 start_with_request=yes idekey=netbeans-xdebug log_level=1 log=/app/xdebug.log discover_client_host=1"
XDEBUG_MODE: "develop,debug"
This will allow you to see nicely formatted var_dump values and to debug your application in your IDE.
Note: You can/must specify the idekey and client_port based on your IDE settings. Plus your Yii project must be well configured in the IDE as well. In NetBeans make sure that "Project URL" and "index file" are correct in "Properties/Run Configuration" (right click the project)
Note 2: Please keep in mind that xDebug2 and xDebug3 have different settings. Details here.
I spent on this approximately 8 hours. Hopefully someone will enjoy it :-) Sadly, this configuration is not present in docker-compose.yml. It would be soooo handy.
Add into section "volumes" this line:
- ./myphp.ini:/usr/local/etc/php/conf.d/custom.ini
And create file myphp.ini the root of your Yii application. You can enter for example html_errors=on and html_errors=off to test if the file is loaded. Restart docker and check results using method phpinfo() in a PHP file.
Navigate in command line to the folder of your docker-project and run command:
The last column of the list is NAMES. Pick one and copy its name. Then run command:
To findout what Linux is used, you can call cat /etc/os-release. (or check the Vagrant chapter for other commands)
If you want to locate the php.ini, type php --ini. Once you find it you can copy it to your yii-folder like this:
cp path/to/php.ini /app/myphp.ini
AdminLTE is one of available admin themes. It currently has 2 versions:
* Upgrading Yii2 from Bootstrap3 to Bootstrap4: https://www.youtube.com/watch?v=W1xxvngjep8
Documentation for AdminLTE <= 2.3, v2.4, v3.0 Note that some AdminLTE functionalities are only 3rd party dependencies. For example the map.
There are also many other admin themes:
There are also more Yii2 extensions for integration of AdminLTE into Yii project:
I picked AdminLTE v2 (because it uses the same Bootstrap as Yii2 demos) and I tested some extensions which should help with implementation.
But lets start with quick info about how to use AdminLTE v2 without extensions in Yii2 demo application.
Manual integration of v2.4 - Asset File creation
Also delete all SCRIPT and LINK tags. We will add them using the AssetBundle later.
We only need to create the Asset file to link all SCRIPTs and LINKs:
namespace app\assets;
use yii\web\AssetBundle;
class LteAsset extends AssetBundle
{
public $sourcePath = '@vendor/almasaeed2010/adminlte/';
public $jsOptions = ['position' => \yii\web\View::POS_HEAD]; // POS_END cause conflict with YiiAsset
public $css = [
'bower_components/font-awesome/css/font-awesome.min.css',
'https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic',
// etc
];
public $js = [
'bower_components/jquery-ui/jquery-ui.min.js',
// etc
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapAsset',
];
}
This error can appear: "Headers already sent"
Now you are done, you can start using HTML and JS stuff from AdminLTE. So lets check extensions which will do it for us
Insolita extension
Works good for many UI items: Boxes, Tile, Callout, Alerts and Chatbox. You only have to prepare the main layout file and Asset bundle, see above. It hasn't been updated since 2018.
Check its web for my comment. I showed how to use many widgets.
Imperfections in the sources:
vendor\insolita\yii2-adminlte-widgets\LteConst.php
vendor\insolita\yii2-adminlte-widgets\CollapseBox.php
LteBox
<div class="overlay"><i class="fa fa-refresh fa-spin"></i></div>
Yiister
Its web explains everything. Very usefull: http://adminlte.yiister.ru You only need the Asset File from this article and then install Yiister. Sadly it hasn't been updated since 2015. Provides widgets for rendering Menu, GridView, Few boxes, Fleshalerts and Callouts. Plus Error page.
dmstr/yii2-adminlte-asset
Officially mentioned on AdminLTE web. Renders only Menu and Alert. Provides mainly the Asset file and Gii templates. Gii templates automatically fix the GridView design, but you can find below how to do it manually.
Other enhancements
AdminLTE is using font Source Sans Pro. If you want a different one, pick it on Google Fonts and modify the layout file like this:
<link href="https://fonts.googleapis.com/css2?family=Palanquin+Dark:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Palanquin Dark', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1,h2,h3,h4,h5,h6,
.h1,.h2,.h3,.h4,.h5,.h6 {
font-family: 'Palanquin Dark', sans-serif;
}
</style>
To display GridView as it should be, wrap it in this HTML code:
<div class="box box-primary">
<div class="box-header">
<h3 class="box-title"><i class="fa fa-table"></i> Grid caption</h3>
</div>
<div class="box-body"
... grid view ...
</div>
</div>
You can also change the glyphicon in web/css/site.css:
a.asc:after {
content: "\e155";
}
a.desc:after {
content: "\e156";
}
And this is basically it. Now we know how to use AdminLTE and fix the GridView. At least one extension will be needed to render widgets, see above.
See official reading about Widgets or this explanation. I am presenting this example, but I added 3 rows. Both types of Widgets can be coded like this:
namespace app\components;
use yii\base\Widget;
use yii\helpers\Html;
class HelloWidget extends Widget{
public $message;
public function init(){
parent::init();
if($this->message===null){
$this->message= 'Welcome User';
}else{
$this->message= 'Welcome '.$this->message;
}
// ob_start();
// ob_implicit_flush(false);
}
public function run(){
// $content = ob_get_clean();
return Html::encode($this->message); // . $content;
}
}
// This widget is called like this:
echo HelloWidget::widget(['message' => ' Yii2.0']);
// After uncommenting my 4 comments you can use this
HelloWidget::begin(['message' => ' Yii2.0']);
echo 'My content';
HelloWidget::end();
It is easy to run tests as both demo-applications are ready. Use command line and navigate to your project. Then type:
php ./vendor/bin/codecept run
This will run Unit and Functional tests. They are defined in folder tests/unit and tests/functional. Functional tests run in a hidden browser and do not work with JavaScript I think. In order to test complex JavaScript, you need Acceptance Tests. How to run them is to be found in file README.md or in documentation in both demo applications. If you want to run these tests in your standard Chrome or Firefox browser, you will need Java JDK and file selenium-server*.jar. See links in README.md. Once you have the JAR file, place is to your project and run it:
java -jar selenium-server-4.0.0.jar standalone
Now you can rerun your tests. Make sure that you have working URL of your project in file acceptance.suite.yml, section WebDriver. For example http://localhost/yii-basic/web. It depends on your environment. Also specify browser. For me works well setting "browser: chrome". If you receive error "WebDriver is not installed", you need to call this composer command:
composer require codeception/module-webdriver --dev
PS: There is also this file ChromeDriver but I am not really sure if it is an alternative to "codeception/module-webdriver" or when to use it. I havent studied it yet.
If you want to see the code coverage, do what is described in the documentation (link above). Plus make sure that your PHP contains xDebug! And mind the difference in settings of xDebug2 and xDebug3! If xDebug is missing, you will receive error "No code coverage driver available".
Under Linux I haven't suceeded, but when I install a web server on Windows (for example XAMPP Server) I am able to install "Microsoft Access Database Engine 2016 Redistributable" and use *.mdb file.
So first of all you should install the web server with PHP and you should know wheather you are installing 64 or 32bit versions. Probably 64. Then go to page Microsoft Access Database Engine 2016 Redistributable (or find newer if available) and install corresponding package (32 vs 64bit).
Note: If you already have MS Access installed in the identical bit-version, you might not need to install the engine.
Then you will be able to use following DSN string in DB connection. (The code belongs to file config/db.php):
<?php
$file = "C:\\xampp\\htdocs\\Database1.mdb";
return [
'class' => 'yii\db\Connection',
'dsn' => "odbc:DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=$file;Uid=;Pwd=;",
'username' => '',
'password' => '',
'charset' => 'utf8',
//'schemaMap' => [
// 'odbc'=> [
// 'class'=>'yii\db\pgsql\Schema',
// 'defaultSchema' => 'public' //specify your schema here
// ]
//],
// Schema cache options (for production environment)
//'enableSchemaCache' => true,
//'schemaCacheDuration' => 60,
//'schemaCache' => 'cache',
];
Then use this to query a table:
$data = Yii::$app->db->createCommand("SELECT * FROM TableX")->queryAll();
var_dump($data);
Note: If you already have MS Access installed in different bit-version then your PHP, you will not be able to install the engine in the correct bit-version. You must uninstall MS Access in that case.
Note2: If you do not know what your MDB file contains, Google Docs recommended me MDB, ACCDB Viewer and Reader and it worked.
Note3: There are preinstalled applications in Windows 10 named:
Open the one you need, go to tab "System DSN" and click "Add". You will see what drivers are available - only these drivers can be used in the DSN String!!
If only "SQL Server" is present, then you need to install the Access Engine (or MS Access) with drivers for your platform. You need driver named cca "Microsoft Access Driver (*.mdb, *.accdb)"
In my case the Engine added following 64bit drivers:
And how about Linux ?
You need the MS Access Drivers as well, but Microsoft does not provide them. There are some 3rd party MdbTools or EasySoft, but their are either not-perfect or expensive. Plus there is Unix ODBC.
For Java there are Java JDBC, Jackcess and Ucanaccess.
And how about Docker ? As far as I know you cannot run Windows images under Linux so you will not be able to use the ODBC-advantage of Windows in this case. You can use Linux images under Windows, but I think there is no way how to access the ODBC drivers from virtual Linux. You would have to try it, I haven't tested it yet.
If you want to import CSV into your DB in Yii2 migrations, you can create this "migration base class" and use it as a parent of your actual migration. Then you can use method batchInsertCsv().
<?php
namespace app\components;
use yii\db\Migration;
class BaseMigration extends Migration
{
/**
* @param $filename Example: DIR_ROOT . DIRECTORY_SEPARATOR . "file.csv"
* @param $table The target table name
* @param $csvToSqlColMapping [csvColName => sqlColName] (if $containsHeaderRow = true) or [csvColIndex => sqlColName] (if $containsHeaderRow = false)
* @param bool $containsHeaderRow If the header with CSV col names is present
* @param int $batchSize How many rows will be inserted in each batch
* @throws Exception
*/
public function batchInsertCsv($filename, $table, $csvToSqlColMapping, $containsHeaderRow = false, $batchSize = 10000, $separator = ';')
{
if (!file_exists($filename)) {
throw new \Exception("File " . $filename . " not found");
}
// If you see number 1 in first inserted row and column, most likely BOM causes this.
// Some Textfiles begin with 239 187 191 (EF BB BF in hex)
// bite order mark https://en.wikipedia.org/wiki/Byte_order_mark
// Let's trim it on the first row.
$bom = pack('H*', 'EFBBBF');
$handle = fopen($filename, "r");
$lineNumber = 1;
$header = [];
$rows = [];
$sqlColNames = array_values($csvToSqlColMapping);
$batch = 0;
if ($containsHeaderRow) {
if (($raw_string = fgets($handle)) !== false) {
$header = str_getcsv(trim($raw_string, $bom), $separator);
}
}
// Iterate over every line of the file
while (($raw_string = fgets($handle)) !== false) {
$dataArray = str_getcsv(trim($raw_string, $bom), $separator);
if ($containsHeaderRow) {
$dataArray = array_combine($header, $dataArray);
}
$tmp = [];
foreach ($csvToSqlColMapping as $csvCol => $sqlCol) {
$tmp[] = trim($dataArray[$csvCol]);
}
$rows[] = $tmp;
$lineNumber++;
$batch++;
if ($batch >= $batchSize) {
$this->batchInsert($table, $sqlColNames, $rows);
$rows = [];
$batch = 0;
}
}
fclose($handle);
$this->batchInsert($table, $sqlColNames, $rows);
}
}
]]>\yii\mail\BaseMailer::useFileTransport is a great tool. If you activate it, all
emails sent trough this mailer will be saved (by default) on @runtime/mail
instead of being sent, allowing the devs to inspect thre result.
But what happens if we want to actually receive the emails on our inboxes. When
all emails are suppose to go to one account, there is no problem: setup it as
a param and the modify it in the params-local.php (assuming advaced
application template).
The big issue arises when the app is supposed to send emails to different
accounts and make use of replyTo, cc and bcc fields. It's almost impossible try
to solve it with previous approach and without using a lot of if(YII_DEBUG).
Well, next there is a solution:
'useFileTransport' => true,
'fileTransportCallback' => function (\yii\mail\MailerInterface $mailer, \yii\mail\MessageInterface $message) {
$message->attachContent(json_encode([
'to' => $message->getTo(),
'cc' => $message->getCc(),
'bcc' => $message->getBcc(),
'replyTo' => $message->getReplyTo(),
]), ['fileName' => 'metadata.json', 'contentType' => 'application/json'])
->setTo('[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.
useFileTransportuseFileTransportThis way we both receive all the emails on the specified account and get them stored
on @runtime/mail.
Pretty simple helper to review emails on Yii2 applications.
Originally posted on: https://glpzzz.github.io/2020/10/02/yii2-redirect-all-emails.html
]]>After getting lot's of error and don't know how to perform multiple images api in yii2 finally I get it today
This is my question I asked on forum and it works for me https://forum.yiiframework.com/t/multiple-file-uploading-api-in-yii2/130519
Implement this code in model for Multiple File Uploading
public function rules()
{
return [
[['post_id', 'media'], 'required'],
[['post_id'], 'integer'],
[['media'], 'file', 'maxFiles' => 10],//here is my file field
[['created_at'], 'string', 'max' => 25],
[['post_id'], 'exist', 'skipOnError' => true, 'targetClass' => Post::className(), 'targetAttribute' => ['post_id' => 'id']],
];
}
You can add extension or any skiponempty method also in model.
And this is my controller action where I performed multiple file uploading code.
public function actionMultiple(){
$model = new Media;
$model->post_id = '2';
if (Yii::$app->request->ispost) {
$model->media = UploadedFile::getInstances($model, 'media');
if ($model->media) {
foreach ($model->media as $value) {
$model = new Media;
$model->post_id = '2';
$BasePath = Yii::$app->basePath.'/../images/post_images';
$filename = time().'-'.$value->baseName.'.'.$value->extension;
$model->media = $filename;
if ($model->save()) {
$value->saveAs($BasePath.$filename);
}
}
return array('status' => true, 'message' => 'Image Saved');
}
}
return array('status' => true, 'data' => $model);
}
If any query or question I will respond.
]]>Logging is a very important feature of the application. It let's you know what is happening in every moment. By default, Yii2 basic and advanced application have just a \yii\log\FileTarget target configured.
To receive emails with messages from the app, setup the log component to email (or Telegram, or slack) transport instead (or besides) of file transport:
'components' => [
// ...
'log' => [
'targets' => [
[
'class' => 'yii\log\EmailTarget',
'mailer' => 'mailer',
'levels' => ['error', 'warning'],
'message' => [
'from' => ['[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).
]]>