Вы находитесь на странице: 1из 217

Магазин на Yii2, часть 1.

Установка фрейморка и
внедрение верстки

Установка фреймворка
Устанавливать Yii2 будем через Composer, переходим в корневую директорию проекта и выполняем
команду:

> composer create-project yiisoft/yii2-app-basic ./


Эта команда установит последнюю версию Yii2 в текущую директорию. Наш сайт теперь доступен по
адресу server.com/web, поскольку именно в директории web находится публичная часть приложения.

Изменяем DocumentRoot
Следующий шаг — изменить корневую директорию в настройках веб-сервера так, чтобы та указывала на
директорию web. Или насторить перенаправление из директории проекта в директорию web с
помощью .htaccess:
RewriteEngine on
RewriteRule ^(.+)?$ /web/$1

Человекопонятные URL
Создаем файл файл .htaccess в директории web и добавляем в него четыре строчка кода:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php
Добавляем в файл конфигурации config/web.php настройку baseUrl и раскомментируем настройки
компонента UrlManager. Заодно включаем поддержку русского языка:
$config = [
/*.....*/
'language' => 'ru-RU',
/*.....*/
'components' => [
/*.....*/
'request' => [
'baseUrl' => '',
'cookieValidationKey' => '.....',
],
'urlManager' => [
// включаем поддержку SEF URL
'enablePrettyUrl' => true,
// не добавлять в URL index.php
'showScriptName' => false,
// правила преобразования адресов
'rules' => [],
],
/*.....*/
],
'params' => $params,
];

Внедрение верстки
Смотрим верстку на предмет того, какие css и js файлы нужно подключить:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<title>Home | E-Shopper</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/font-awesome.min.css" rel="stylesheet">
<link href="css/main.css" rel="stylesheet">
<!--[if lte IE 9]>
<script src="js/html5shiv.js"></script>
<script src="js/respond.min.js"></script>
<![endif]-->
</head>
<body>
<!-- ..... -->
<script src="js/jquery.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/main.js"></script>
</body>
</html>
Скопируем директории css, js, fonts и images из верстки в директорию web.
Файлы bootstrap.min.css, jquery.js и bootstrap.min.js удаляем — они уже есть в Yii2. Теперь
редактируем файл assets/AppAsset.php:
<?php
namespace app\assets;

use yii\web\AssetBundle;

/**
* Main application asset bundle
*/
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/font-awesome.min.css',
'css/main.css'
];
public $js = [
'js/main.js'
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapPluginAsset',
];
}
Обратите внимание, что в качестве зависимости указан
класс BootstrapPluginAsset (вместо BootstrapAsset) — он включает в
себя bootstrap.css и bootstrap.js. В этом нетрудно убедиться, если заглянуть в исходники:
<?php
namespace yii\bootstrap;
use yii\web\AssetBundle;

class BootstrapAsset extends AssetBundle {


public $sourcePath = '@bower/bootstrap/dist';
public $css = [
'css/bootstrap.css',
];
}
<?php
namespace yii\bootstrap;
use yii\web\AssetBundle;

class BootstrapPluginAsset extends AssetBundle {


public $sourcePath = '@bower/bootstrap/dist';
public $js = [
'js/bootstrap.js',
];
public $depends = [
'yii\web\JqueryAsset',
'yii\bootstrap\BootstrapAsset',
];
}
Хорошо, теперь копируем html-код верстки в файл views/layout/main.php и добавляем php-код:
<?php
use yii\helpers\Html;
use app\assets\AppAsset;

AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
<meta charset="<?= Yii::$app->charset ?>">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<?php $this->registerCsrfMetaTags() ?>
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<!-- ..... -->
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

Осталось еще подключить js-файлы для старых версий MS IE. Для этого создаем еще один комплект
ресурсов:

<?php
namespace app\assets;

use yii\web\AssetBundle;
use yii\web\View;

/**
* Комплект ресурсов для старых версий MS IE
*/
class OldIeAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $js = [
'js/html5shiv.js',
'js/respond.min.js',
];
public $jsOptions = [
// скрипты будут подключены по условию [if lte IE 9]...[endif]
'condition' => 'lte IE 9',
// скрипты будут подключены в <head>-секции документа
'position' => View::POS_HEAD,
];
}
И подключаем его в layout-шаблоне main.php:
<?php
use yii\helpers\Html;
use app\assets\AppAsset;

AppAsset::register($this);
OldIeAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<!-- ..... -->
Подключить js-файлы для старых версий MS IE можно иначе. Для этого изменяем класс AppAsset:
<?php
namespace app\assets;

use yii\web\AssetBundle;
use yii\web\View;

/**
* Main application asset bundle
*/
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/font-awesome.min.css',
'css/main.css'
];
public $js = [
'js/main.js'
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapPluginAsset',
];

public function registerAssetFiles($view) {

parent::registerAssetFiles($view);

$manager = $view->getAssetManager();
$view->registerJsFile(
$manager->getAssetUrl(
$this,
'js/html5shiv.min.js'
),
[
'condition' => 'lte IE9',
'position'=>View::POS_HEAD
]
);
$view->registerJsFile(
$manager->getAssetUrl(
$this,
'js/respond.min.js'
),
[
'condition' => 'lte IE9',
'position'=>View::POS_HEAD
]
);
}
}

И последенее, что нужно сделать — перенести основной контент страницы из layout-


шаблона views/layout/main.php в view-шаблон views/site/index.php:
<?php
use yii\helpers\Html;
use app\assets\AppAsset;

AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>.....</head>
<body>
<?php $this->beginBody() ?>
<header>.....</header>

<?= $content; ?>

<footer>.....</footer>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>
<?php
/* @var $this yii\web\View */
$this->title = 'Интернет-магазин';
?>

<section>
<div class="container">
<!-- Слайдер из трех элементов -->
<div id="slider" class="carousel slide" data-ride="carousel">
..........
</div>
</div>
</section>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<!-- Меню каталога -->
</div>

<h2>Бренды</h2>
<div class="brand-products">
<!-- Популярные бренды -->
</div>
</div>

<div class="col-sm-9">
<h2>Лидеры продаж</h2>
<div class="row">
<div class="col-sm-4">
..........
</div>
<div class="col-sm-4">
..........
</div>
<div class="col-sm-4">
..........
</div>
</div>
<h2>Новинки</h2>
<div class="row">
<div class="col-sm-4">
..........
</div>
<div class="col-sm-4">
..........
</div>
<div class="col-sm-4">
..........
</div>
</div>
<h2>Распродажа</h2>
<div class="row">
<div class="col-sm-4">
..........
</div>
<div class="col-sm-4">
..........
</div>
<div class="col-sm-4">
..........
</div>
</div>
</div>
</div>
</div>
</section>

Магазин на Yii2, часть 2. Создаем базу данных и


классы моделей
Теперь создаем базу данных eshop и две таблицы — product и category. Таблица category описывает
разделы каталога, а таблица product — товары каталога. Редактируем файл config/db.php, изменяем
имя базы данных на eshop. И создаем классы моделей Category и Product в директории models.
--
-- Структура таблицы `category`
--
CREATE TABLE `category` (
`id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Уникальный
идентификатор',
`parent_id` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Родительская
категория',
`name` varchar(255) NOT NULL COMMENT 'Наименование категории',
`content` varchar(255) DEFAULT NULL COMMENT 'Описание категории',
`keywords` varchar(255) DEFAULT NULL COMMENT 'Мета-тег keywords',
`description` varchar(255) DEFAULT NULL COMMENT 'Мета-тег description',
`image` varchar(255) DEFAULT NULL COMMENT 'Имя файла изображения'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Структура таблицы `product`
--
CREATE TABLE `product` (
`id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Уникальный
идентификатор',
`category_id` int(10) UNSIGNED NOT NULL COMMENT 'Родительская категория',
`brand_id` int(10) UNSIGNED NOT NULL COMMENT 'Идентификатор бренда',
`name` varchar(255) NOT NULL COMMENT 'Наименование товара',
`content` text COMMENT 'Описание товара',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT 'Цена товара',
`keywords` varchar(255) DEFAULT NULL COMMENT 'Мета-тег keywords',
`description` varchar(255) DEFAULT NULL COMMENT 'Мета-тег description',
`image` varchar(255) DEFAULT NULL COMMENT 'Имя файла изображения',
`hit` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Лидер продаж?',
`new` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Новый товар?',
`sale` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Распродажа?'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Файл config/db.php:
<?php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=eshop',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
];
Файл models/Category.php:
<?php
namespace app\models;

use yii\db\ActiveRecord;

class Category extends ActiveRecord {

/**
* Возвращает имя таблицы БД
*/
public static function tableName() {
return 'category';
}

/**
* Возвращает товары категории
*/
public function getProducts() {
// связь таблицы БД `category` с таблицей `product`
return $this->hasMany(Product::class, ['category_id' => 'id']);
}

/**
* Возвращает родительскую категорию
*/
public function getParent() {
// связь таблицы БД `category` с таблицей `category`
return $this->hasOne(self::class, ['id' => 'parent_id']);
}

/**
* Возвращает дочерние категории
*/
public function getChildren() {
// связь таблицы БД `category` с таблицей `category`
return $this->hasMany(self::class, ['parent_id' => 'id']);
}
}
Файл models/Product.php:
<?php
namespace app\models;

use yii\db\ActiveRecord;

class Product extends ActiveRecord {

/**
* Возвращает имя таблицы БД
*/
public static function tableName() {
return 'product';
}

/**
* Возвращает родительскую категорию
*/
public function getCategory() {
// связь таблицы БД `product` с таблицей `category`
return $this->hasOne(Category::class, ['id' => 'category_id']);
}
}

Магазин на Yii2, часть 3. Виджет для вывода меню


каталога
Теперь нам нужно как-то выводить меню каталога в левой колонке. Поскольку это меню показывается на
многих страницах сайта, реализуем его в виде виджета. И тогда сможем вставить меню в любом месте
шаблона одной строкой кода. Для создания виджета нам потребуется директория components, внутри
нее создаем файл класса TreeWidget.php.
<?php
namespace app\components;

use yii\base\Widget;
use app\models\Category;
use Yii;

/**
* Виджет для вывода дерева разделов каталога товаров
*/
class TreeWidget extends Widget {

/**
* Выборка категорий каталога из базы данных
*/
protected $data;

/**
* Массив категорий каталога в виде дерева
*/
protected $tree;

public function run() {


// пробуем извлечь данные из кеша
$html = Yii::$app->cache->get('catalog-menu');
if ($html === false) {
// данных нет в кеше, получаем их заново
$this->data = Category::find()->indexBy('id')->asArray()->all();
$this->makeTree();
if ( ! empty($this->tree)) {
$html = $this->render('tree', ['tree' => $this->tree]);
} else {
$html = '';
}
// сохраняем полученные данные в кеше
Yii::$app->cache->set('catalog-menu', $html, 60);
}
return $html;
}

/**
* Функция принимает на вход линейный массив элеменов, связанных
* отношениями parent-child, и возвращает массив в виде дерева
*/
protected function makeTree() {
if (empty($this->data)) {
return;
}
foreach ($this->data as $id => &$node) {
if ( ! $node['parent_id']) {
$this->tree[$id] = &$node;
} else {
$this->data[$node['parent_id']]['childs'][$id] = &$node;
}
}
}
}
При вызове метода all() возвращается массив строк, индексированный последовательными целыми
числами. Чтобы сделать индекс по указанному столбцу, нужно вызвать метод indexBy() перед вызовом
метода all().

Теперь создаем шаблон menu.php для меню каталога в директории components/views:


<?php
/*
* Файл components/views/menu.php
*/
use yii\helpers\Html;
use yii\helpers\Url;
?>

<ul id="accordion">
<?php foreach ($tree as $item1): ?>
<li><a href="<?= Url::to(['catalog/category', 'id' => $item1['id']]); ?>">
<?= Html::encode($item1['name']); ?>
<?php if (isset($item1['childs'])): ?>
</a>
<span class="badge pull-right"><i class="fa fa-plus"></i></span>
<ul>
<?php foreach ($item1['childs'] as $item2): ?>
<li><a href="<?= Url::to(['catalog/category', 'id' =>
$item2['id']]); ?>">
<?= Html::encode($item2['name']); ?>
<?php if (isset($item2['childs'])): ?>
</a>
<span class="badge pull-right"><i class="fa fa-
plus"></i></span>
<ul>
<?php foreach ($item2['childs'] as $item3): ?>
<li><a href="<?= Url::to(['catalog/category', 'id' =>
$item3['id']]); ?>">
<?= Html::encode($item3['name']); ?>
<?php if (isset($item3['childs'])): ?>
</a>
<span class="badge pull-right"><i class="fa fa-
plus"></i></span>
<ul>
<?php foreach ($item3['childs'] as $item4): ?>
<li><a href="<?=
Url::to(['catalog/category', 'id' => $item4['id']]); ?>">
<?= Html::encode($item4['name']); ?>
<?php if (isset($item4['childs'])): ?>
</a>
<span class="badge pull-right"><i
class="fa fa-plus"></i></span>
<ul>
<?php foreach ($item4['childs'] as
$item5): ?>
<li><a href="<?=
Url::to(['catalog/category', 'id' => $item5['id']]); ?>">
<?=
Html::encode($item5['name']); ?></a></li>
<?php endforeach; ?>
</ul>
<?php else: ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
Осталось только добавить вызов виджета в view-шаблоне views/site/index.php:
<h2>Каталог</h2>
<?= TreeWidget::widget(); ?>
Для того, чтобы элементы меню можно было свернуть и развернуть, создадим файл accordion.js и
сохраним его в директории web/js. Для стилевого оформления меню создадим файл accordion.css и
сохраним его в директории web/css.
jQuery(document).ready(function($) {
$('#accordion ul').hide();
$('#accordion .badge').on('click', function () {
var $badge = $(this);
var closed = $badge.siblings('ul') &&
!$badge.siblings('ul').is(':visible');

if (closed) {
$badge.siblings('ul').slideDown('normal', function () {
$badge.children('i').removeClass('fa-plus').addClass('fa-minus');
});
} else {
$badge.siblings('ul').slideUp('normal', function () {
$badge.children('i').removeClass('fa-minus').addClass('fa-plus');
});
}
});
});
#accordion {
list-style: none;
padding-left: 0;
}
#accordion ul {
list-style: none;
}
#accordion .badge {
cursor: pointer;
}

И подключим эти два файла подключаем к нашему комплекту ресурсов:

<?php
namespace app\assets;

use yii\web\AssetBundle;
use yii\web\View;

/**
* Main application asset bundle
*/
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/font-awesome.min.css',
'css/accordion.css',
'css/main.css'
];
public $js = [
'js/accordion.js',
'js/main.js'
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapPluginAsset',
];

public function registerAssetFiles($view) {


/*.....*/
}
}
Мы использовали метод хелпера Url::to() для создания ссылок на разделы каталога. Сейчас ссылка
имеет вид
http://www.server.com/catalog/category?id=123

Давайте изменим ее на

http://www.server.com/catalog/category/123
Для этого изменяем правила роутинга в файле конфигурации config/web.php:
$config = [
/*...*/
'components' => [
/*...*/
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'catalog/category/<id:\d+>' => 'catalog/category'
],
],
/*...*/
],
/*...*/
];

Магазин на Yii2, часть 4. Добавляем новую сущность


— бренд
Создадим в базе данных таблицу brand для хранения брендов. И добавим в таблицу product внешний
ключ brand_id. Создадим модель для этой сущности, и добавим два action-а в контроллер — которые
будут отвечать за показ списка всех брендов и за показ списка товаров выбранного бренда.
--
-- Структура таблицы `brand`
--
CREATE TABLE `brand` (
`id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Уникальный
идентификатор',
`name` varchar(255) NOT NULL COMMENT 'Наименование',
`content` varchar(255) DEFAULT NULL COMMENT 'Краткое описание',
`keywords` varchar(255) DEFAULT NULL COMMENT 'Мета-тег keywords',
`description` varchar(255) DEFAULT NULL COMMENT 'Мета-тег description',
`image` varchar(255) DEFAULT NULL COMMENT 'Имя файла изображения'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
<?php
namespace app\models;

use yii\db\ActiveRecord;

class Brand extends ActiveRecord {

/**
* Метод возвращает имя таблицы БД
*/
public static function tableName() {
return 'brand';
}

/**
* Метод возвращает массив товаров бренда
*/
public function getProducts() {
// связь таблицы БД `brand` с таблицей `product`
return $this->hasMany(Product::class, ['brand_id' => 'id']);
}

/**
* Возвращает информацию о бренде с идентификатором $id
*/
public function getBrand($id) {
$id = (int)$id;
return self::findOne($id);
}

/**
* Возвращает массив всех брендов каталога и
* количество товаров для каждого бренда
*/
/**
* Возвращает массив всех брендов каталога и
* количество товаров для каждого бренда
*/
public function getAllBrands() {
$query = self::find();
$brands = $query
->select([
'id' => 'brand.id',
'name' => 'brand.name',
'content' => 'brand.content',
'image' => 'brand.image',
'count' => 'COUNT(*)'
])
->innerJoin(
'product',
'product.brand_id = brand.id'
)
->groupBy([
'brand.id', 'brand.name', 'brand.content', 'brand.image'
])
->orderBy(['name' => SORT_ASC])
->asArray()
->all();
return $brands;
}
}
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use Yii;

class CatalogController extends AppController {


/**
* Главная страница каталога товаров
*/
public function actionIndex() {
// получаем корневые категории
$root = Category::find()->where(['parent_id' => 0])->all();
// получаем популярные бренды
$brands = (new Brand())->getPopularBrands();
return $this->render('index', compact('root', 'brands'));
}

/**
* Категория каталога товаров
*/
public function actionCategory($id) {
$id = (int)$id;
$temp = new Category();
// товары категории
list($products, $pages) = $temp->getCategoryProducts($id);
// данные о категории
$category = $temp->getCategory($id);
// устанавливаем мета-теги для страницы
$this->setMetaTags(
$category->name . ' | ' . Yii::$app->params['shopName'],
$category->keywords,
$category->description
);
return $this->render(
'category',
compact('category', 'products', 'pages')
);
}

/**
* Список всех брендов каталога товаров
*/
public function actionBrands() {
$brands = (new Brand())->getAllBrands();
return $this->render(
'brands',
compact('brands')
);
}

/**
* Список товаров бренда с идентификатором $id
*/
public function actionBrand($id) {
$id = (int)$id;
$brand = (new Brand())->getBrand($id);
return $this->render(
'brand',
compact('brand')
);
}
}

Магазин на Yii2, часть 5. Виджет для вывода


популярных брендов
В левой колонке, под меню каталога, предусмотрен блок популярных брендов. Этот блок показывается
на всех страницах сайта, так что оформим его в виде виджета. Все по аналогии с виджетом меню
каталога — создаем в директории components файл BrandsWidget.php и view-шаблон в
поддиректории views.

Добавим в модель еще один метод, который возвращает массив популярных брендов:

<?php
namespace app\models;

use yii\db\ActiveRecord;

class Brand extends ActiveRecord {

/*...*/

/**
* Возвращает массив популярных брендов и
* количество товаров для каждого бренда
*/
public function getPopularBrands() {
// получаем бренды с наибольшим кол-вом товаров
$brands = self::find()
->select([
'id' => 'brand.id',
'name' => 'brand.name',
'count' => 'COUNT(*)'
])
->innerJoin(
'product',
'product.brand_id = brand.id'
)
->groupBy([
'brand.id', 'brand.name'
])
->orderBy(['count' => SORT_DESC])
->limit(10)
->asArray()
// для дальнейшей сортировки
->indexBy('name')
->all();
// теперь нужно отсортировать бренды по названию
ksort($brands);
return $brands;
}

/*...*/
}
<?php
namespace app\components;

use app\models\Brand;
use yii\base\Widget;

/**
* Виджет для вывода списка брендов каталога
*/
class BrandsWidget extends Widget {

public function run() {


// пробуем извлечь данные из кеша
$html = Yii::$app->cache->get('widget-brands');
if ($html === false) {
// данных нет в кеше, получаем их заново
$brands = (new Brand())->getPopularBrands();
$html = $this->render('brands', ['brands' => $brands]);
// сохраняем полученные данные в кеше
Yii::$app->cache->set('widget-brands', $html);
}
return $html;
}

}
<?php
/*
* Файл components/views/brands.php
*/
use yii\helpers\Html;
use yii\helpers\Url;
?>

<ul>
<?php foreach ($brands as $brand): ?>
<li>
<a href="<?= Url::to(['catalog/brand', 'id' => $brand['id']]); ?>">
<span class="badge pull-right"><?= $brand['count']; ?></span>
<?= Html::encode($brand['name']); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
Вставляем вызов виджета в view-шаблоны views/page/index.php (главная страница сайта)
и views/catalog/category.php (категория каталога):
<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<!-- основной контент страницы -->
</div>
</div>
</div>
</section>

Магазин на Yii2, часть 6. Показываем на главной хиты,


новинки и распродажи
Давайте создадим контроллер PageController, который будет отвечать за показ страниц сайта, не
связанных с каталогом товаров. Вообще, такой контроллер уже есть сразу после установки фреймворка
— это SiteController, но мы его оставим без изменений, как образец. В первую очередь, надо
переопределить контроллер по умолчанию с site на page:
/*
* Файл config/web.php
*/
$config = [
/* ... */
'defaultRoute' => 'page',
/* ... */
];
Потом создаем контроллер PageController:
<?php
namespace app\controllers;

use app\models\Product;

class PageController extends AppController {


/*
* Главная страница сайта
*/
public function actionIndex() {
// получаем лидеров продаж
$hitProducts = Product::find()->where(['hit' => 1])->limit(3)->asArray()-
>all();
// получаем новые товары
$newProducts = Product::find()->where(['new' => 1])->limit(3)->asArray()-
>all();
// получаем товары распродажи
$saleProducts = Product::find()->where(['sale' => 1])->limit(3)->asArray()-
>all();

return $this->render(
'index',
compact('hitProducts', 'newProducts', 'saleProducts')
);
}
}
Контроллер наследует класс AppController, где у нас будут методы, общие для всех контроллеров:
<?php
namespace app\controllers;

use yii\web\Controller;

class AppController extends Controller {

}
Теперь создаем view-шаблон для действия по умолчанию, т.е. файл views/page/index.php. И
переносим весь html и php код из views/site/index.php в views/page/index.php.

Находим место в html-коде, где идет вывод лидеров продаж, новинок и товаров распродажи. На это
место вставляем циклы:

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($hitProducts)): ?>
<h2>Лидеры продаж</h2>
<div class="row">
<?php foreach ($hitProducts as $item): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$item['image'],
['alt' => $item['name'], 'class' => 'img-
responsive']
);
?>
<h2><?= $item['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $item['id']]); ?>">
<?= Html::encode($item['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($newProducts)): ?>
<h2>Новинки</h2>
<div class="row">
<?php foreach ($newProducts as $item): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$item['image'],
['alt' => $item['name'], 'class' => 'img-
responsive']
);
?>
<h2><?= $item['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $item['id']]); ?>">
<?= Html::encode($item['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($saleProducts)): ?>
<h2>Распродажа</h2>
<div class="row">
<?php foreach ($saleProducts as $item): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$item['image'],
['alt' => $item['name'], 'class' => 'img-
responsive']
);
?>
<h2><?= $item['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $item['id']]); ?>">
<?= Html::encode($item['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>

Магазин на Yii2, часть 7. Показываем список товаров


категории
Итак, мы вывели на главной странице список лидеров продаж. Теперь покажем список товаров
выбранной категории. Нам нужно добавить в модель два метода: один — для получения информации о
категории и второй — для получения списка товаров. Или, сделать еще проще — добавить только
первый метод, а потом использовать ленивую загрузку, чтобы получить в view-шаблоне массив товаров
категории.

Но, обо всем по порядку. Мы добавляем метод, позволяющий получить данные о категории. И у нас
описана связь таблицы category с таблицей product.
class Category extends ActiveRecord {
/**
* Метод описывает связь таблицы БД `category` с таблицей `product`
*/
public function getProducts() {
return $this->hasMany(Product::class, ['category_id' => 'id']);
}
/*
* Метод возвращает информацию о категории с идентификатором $id
*/
public function getCategory($id) {
return self::findOne($id);
}
}

В контроллере нам достаточно получить только данные о категории и передать их в view-шаблон:

class CatalogController extends AppController {


public function actionCategory($id) {
$id = (int)$id;
$category = (new Category())->getCategory($id);
return $this->render('category', compact('category'));
}
}
А данные о товарах категории можно получить прямо в шаблоне views/catalog/category.php:
// данные о товарах категории с использованием ленивой загрузки
$products = $category->products;
if (!empty($products)) {
echo '<ul>';
foreach ($products as $product) {
echo '<li>', $product->name, '</li>';
}
echo '</ul>';
} else {
echo '<p>Нет товаров в этой категории</p>';
}
Но у нас ситуация несколько сложнее — нужно получить товары не только текущей категории, но и
товары во всех потомках этой категории. Т.е. товары дочерних категорий, дочерних дочерних и так
далее. Сейчас это стандарт де-факто для каталогов товаров. Поэтому добавим в класс модели метод,
возвращающий массив товаров категории с учетом всех ее потомков:

<?php
namespace app\models;

use yii\db\ActiveRecord;

class Category extends ActiveRecord {

/**
* Метод возвращает имя таблицы БД
*/
public static function tableName() {
return 'category';
}

/**
* Метод описывает связь таблицы БД `category` с таблицей `product`
*/
public function getProducts() {
return $this->hasMany(Product::class, ['category_id' => 'id']);
}

/**
* Метод описывает связь таблицы БД `category` с таблицей `category`
*/
public function getParent() {
return $this->hasOne(self::class, ['id' => 'parent_id']);
}

/**
* Метод описывает связь таблицы БД `category` с таблицей `category`
*/
public function getChildren() {
return $this->hasMany(self::class, ['parent_id' => 'id']);
}

/**
* Возвращает информацию о категории с иденификатором $id
*/
public function getCategory($id) {
return self::findOne($id);
}

/**
* Возвращает массив товаров в категории с идентификатором $id и во
* всех ее потомках, т.е. в дочерних, дочерних-дочерних и так далее
*/
public function getCategoryProducts($id) {
// получаем массив идентификаторов всех потомков категории
$ids = $this->getAllChildIds($id);
$ids[] = $id;
return Product::find()->where(['in', 'category_id', $ids])->all();
}

/**
* Возвращает массив идентификаторов всех потомков категории $id,
* т.е. дочерние, дочерние дочерних и так далее
*/
protected function getAllChildIds($id) {
$children = [];
$ids = $this->getChildIds($id);
foreach ($ids as $item) {
$children[] = $item;
$c = $this->getAllChildIds($item);
foreach ($c as $v) {
$children[] = $v;
}
}
return $children;
}

/**
* Возвращает массив идентификаторов дочерних категорий (прямых
* потомков) категории с уникальным идентификатором $id
*/
protected function getChildIds($id) {
$children = self::find()->where(['parent_id' => $id])->asArray()->all();
$ids = [];
foreach ($children as $child) {
$ids[] = $child['id'];
}
return $ids;
}
}

А контроллер у нас будет таким:

<?php
namespace app\controllers;

use app\models\Category;
use app\models\Product;
use Yii;

class CatalogController extends AppController {

/* ... */

/*
* Категория каталога товаров
*/
public function actionCategory($id) {
$id = (int)$id;
$temp = new Category();
// товары категории
$products = $temp->getCategoryProducts($id);
// данные о категории
$category = $temp->getCategory($id);
// устанавливаем мета-теги для страницы
$this->setMetaTags(
$category->name . ' | ' . Yii::$app->params['shopName'],
$category->keywords,
$category->description
);
return $this->render('category', compact('category', 'products'));
}
}
Файл view-шаблона views/catalog/category.php. В нем ничего нового — используем два виджета
(меню каталога и популярные бренды) и цикл для показа товаров категории:
<?php
/*
* Страница раздела каталога, файл views/catalog/category.php
*/
use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;

$this->title = $category['name'];
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($products)): ?>
<h2><?= Html::encode($category['name']); ?></h2>
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$product['image'],
['alt' => $product['name'], 'class' =>
'img-responsive']
);
?>
<h2><?= $product['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $product['id']]); ?>">
<?= Html::encode($product['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($product['new']) { // новинка?
echo Html::tag(
'span',
'Новинка',
['class' => 'new']
);
}
if ($product['hit']) { // лидер продаж?
echo Html::tag(
'span',
'Лидер продаж',
['class' => 'hit']
);
}
if ($product['sale']) { // распродажа?
echo Html::tag(
'span',
'Распродажа',
['class' => 'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>Нет товаров в этой категории.</p>
<?php endif; ?>
</div>
</div>
</div>
</section>

Магазин на Yii2, часть 8. Список товаров бренда и все


бренды
Следующий шаг — вывести список товаров отдельного бренда. И создать страницу всех брендов.
Методы в модели для получения списка товаров бренда и получения списка всех брендов у нас уже есть.
Методы actionBrand() и actionBrands() в контроллере тоже определены. Осталось только создать
файлы view-шаблонов brand.php и brands.php .
<?php
namespace app\models;

use yii\db\ActiveRecord;

class Brand extends ActiveRecord {

/*...*/

/**
* Возвращает информацию о бренде с идентификатором $id
*/
public function getBrand($id) {
$id = (int)$id;
return self::findOne($id);
}

/**
* Возвращает массив всех брендов каталога и
* количество товаров для каждого бренда
*/
public function getAllBrands() {
return self::find()
->select([
'id' => 'brand.id',
'name' => 'brand.name',
'content' => 'brand.content',
'image' => 'brand.image',
'count' => 'COUNT(*)'
])
->innerJoin(
'product',
'product.brand_id = brand.id'
)
->groupBy([
'brand.id', 'brand.name', 'brand.content', 'brand.image'
])
->orderBy(['name' => SORT_ASC])
->asArray()
->all();
}

/**
* Возвращает массив всех товаров бренда с идентификатором $id
*/
public function getBrandProducts($id) {
return Product::find()->where(['brand_id' => $id])->asArray()->all();
}
}
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use Yii;

class CatalogController extends AppController {

/*...*/

/**
* Список всех брендов каталога товаров
*/
public function actionBrands() {
$brands = (new Brand())->getAllBrands();
return $this->render(
'brands',
compact('brands')
);
}

/**
* Список товаров бренда с идентификатором $id
*/
public function actionBrand($id) {
$id = (int)$id;
$temp = new Brand();
// товары бренда
$products = $temp->getBrandProducts($id);
// данные о бренде
$brand = $temp->getBrand($id);

return $this->render(
'brand',
compact('brand', 'products')
);
}
}
Файлы views/catalog/brand.php и views/catalog/brands.php:
<?php
/*
* Страница списка товаров бренда, файл views/catalog/brand.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($products)): /* выводим товары бренда */ ?>
<h2><?= Html::encode($brand['name']); ?></h2>
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$product['image'],
['alt' => $product['name'], 'class' =>
'img-responsive']
);
?>
<h2><?= $product['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $product['id']]); ?>">
<?= Html::encode($product['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($product['new']) { // новинка?
echo Html::tag(
'span',
'Новинка',
['class' => 'new']
);
}
if ($product['hit']) { // лидер продаж?
echo Html::tag(
'span',
'Лидер продаж',
['class' => 'hit']
);
}
if ($product['sale']) { // распродажа?
echo Html::tag(
'span',
'Распродажа',
['class' => 'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>Нет товаров у этого бренда.</p>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php
/*
* Страница списка всех брендов, файл views/catalog/brands.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9 padding-right">


<h1>Все бренды</h1>
<?php if (!empty($brands)): ?>
<div class="row">
<?php foreach ($brands as $brand): ?>
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<?=
Html::img(
'@web/images/brands/'.$brand['image'],
['alt' => $brand['name']]
);
?>
<div class="caption">
<h2>
<a href="<?= Url::to(['catalog/brand',
'id' => $brand['id']]); ?>">
<?= Html::encode($brand['name']);
?>
</a>
</h2>
<p><?= Html::encode($brand['content']);
?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
И внесем изменения в файл конфигурации config/web.php:
<?php
/*...*/
$config = [
/*...*/
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'catalog/category/<id:\d+>' => 'catalog/category',
'catalog/brand/<id:\d+>' => 'catalog/brand',
],
],
],
/*...*/
];
/*...*/

Магазин на Yii2, часть 9. Добавляем мета-теги


keywords и description
Давайте немного подумаем о SEO-оптимизации и установим для страниц нашего сайта мета-теги. Мета-
теги влияют на то, как отображатся и какую позицию занимает страница сайта в поисковой выдаче
Yandex и Google. При прочих равных условиях поисковики отдают предпочтение сайту с грамотно
сформированными мета-тегами.

В базе данных для таблиц category и product у нас есть поля keywords и description. Эти поля будут
заполняться при добавлении новых разделов и товаров. На случай, если это поля будут не заполнены,
предусмотрим некоторое значения по умолчанию. Для этого открываем на редактирование
файл config/params.php:
<?php
return [
'adminEmail' => 'admin@example.com',
'senderEmail' => 'noreply@example.com',
'senderName' => 'Example.com mailer',
/*
* Название магазина, например «Lamoda» или «WildBerries»
*/
'shopName' => 'Магазин одежды и обуви',
/*
* Значения по умолчанию для мета-тегов title, keywords и description
*/
'defaultTitle' => 'Интернет-магазин модной одежды и обуви',
'defaultKeywords' => 'одежда, обувь, мужская, женская, детская, зимняя,
летняя',
'defaultDescription' => 'Коллекции женской, мужской, детской одежды и обуви',
];
Поскольку мета-теги нужно устанавливать для всех страниц сайта, добавим метод в общий
контроллер AppController:
<?php
namespace app\controllers;

use yii\web\Controller;
use Yii;

class AppController extends Controller {

/**
* Метод устанавливает мета-теги для страницы сайта
* @param string $title
* @param string $keywords
* @param string $description
*/
protected function setMetaTags($title = '', $keywords = '', $description = '')
{
$this->view->title = $title ?: Yii::$app->params['defaultTitle'];
$this->view->registerMetaTag([
'name' => 'keywords',
'content' => $keywords ?: Yii::$app->params['defaultKeywords']
]);
$this->view->registerMetaTag([
'name' => 'description',
'content' => $description ?: Yii::$app->params['defaultDescription']
]);
}
}
Добавим вызов этого метода в контроллеры PageController и CategoryController:
<?php
namespace app\controllers;

use app\models\Product;
use Yii;

class PageController extends AppController {


/*
* Главная страница сайта
*/
public function actionIndex() {
// получаем лидеров продаж
$hitProducts = Product::find()->where(['hit' => 1])->limit(3)->asArray()-
>all();
// получаем новые товары
$newProducts = Product::find()->where(['new' => 1])->limit(3)->asArray()-
>all();
// получаем товары распродажи
$saleProducts = Product::find()->where(['sale' => 1])->limit(3)->asArray()-
>all();

// устанавливаем мета-теги для страницы


$this->setMetaTags();

return $this->render(
'index',
compact('hitProducts', 'newProducts', 'saleProducts')
);
}
}
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Product;
use Yii;

class CatalogController extends AppController {

public function actionCategory($id) {


$id = (int)$id;
$temp = new Category();
// товары категории
$products = $temp->getCategoryProducts($id);
// данные о категории
$category = $temp->getCategory($id);
// устанавливаем мета-теги для страницы
$this->setMetaTags(
$category->name . ' | ' . Yii::$app->params['shopName'],
$category->keywords,
$category->description
);
return $this->render('category', compact('category', 'products'));
}
}
Удаляем из view-шаблонов views/page/index.php и views/catalog/category.php код, отвечающий
за установку заголовка страницы:
$this->title = 'Интернет-магазин';
$this->title = $category['name'];

Теперь установка заголовка страницы и мета-тегов у нас централизована, а не размазана по всем


шаблонам.

Магазин на Yii2, часть 10. Добавляем постраничную


навигацию
Сейчас на странице категории показываются все товары этой категории и всех ее потомков. Это
подходит для небольшого каталога, но когда товаров много, страница будет очень большой. Давайте
добавим постраничную навигацию и используем для этого класс Pagination.

Сначала изменяем метод модели, отвечающий за получение списка товаров категории:

<?php
namespace app\models;

use yii\data\Pagination;
use yii\db\ActiveRecord;

class Category extends ActiveRecord {

/*...*/

/**
* Возвращает массив всех товаров в категории с идентификатором $id
* и в ее потомках, т.е. в дочерних, дочерних-дочерних и так далее
*/
public function getCategoryProducts($id) {
$id = (int)$id;
// получаем массив идентификаторов всех потомков категории
$ids = $this->getAllChildIds($id);
$ids[] = $id;
// для постаничной навигации получаем только часть товаров
$query = Product::find()->where(['in', 'category_id', $ids]);
$pages = new Pagination([
'totalCount' => $query->count(),
'pageSize' => 10, // кол-во товаров на странице
]);
$products = $query->offset($pages->offset)
->limit($pages->limit)
->asArray()
->all();
return [$products, $pages];
}

/*...*/
}
Потом изменяем action в контроллере, отвечающий за страницу списка товаров категории:
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use Yii;

class CatalogController extends AppController {

/*...*/

/**
* Категория каталога товаров
*/
public function actionCategory($id) {
$id = (int)$id;
$temp = new Category();
// товары категории
list($products, $pages) = $temp->getCategoryProducts($id);
// данные о категории
$category = $temp->getCategory($id);
// устанавливаем мета-теги для страницы
$this->setMetaTags(
$category->name . ' | ' . Yii::$app->params['shopName'],
$category->keywords,
$category->description
);
return $this->render(
'category',
compact('category', 'products', 'pages')
);
}

/*...*/
}
И последнее — используем виджет LinkPager в view-шаблоне views/catalog/category.php для
показа постраничной навигации:
<?php
/*
* Страница раздела каталога, файл views/catalog/category.php
*/
use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($products)): ?>
<h2><?= Html::encode($category['name']); ?></h2>
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$product['image'],
['alt' => $product['name'], 'class' =>
'img-responsive']
);
?>
<h2><?= $product['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $product['id']]); ?>">
<?= Html::encode($product['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($product['new']) { // новинка?
echo Html::img(
'@web/images/home/new.png',
['alt' => 'Новинка', 'class' => 'new']
);
}
if ($product['sale']) { // распродажа?
echo Html::img(
'@web/images/home/sale.png',
['alt' => 'Распродажа', 'class' =>
'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?= LinkPager::widget(['pagination' => $pages]); /*
постраничная навигация */ ?>
<?php else: ?>
<p>Нет товаров в этой категории.</p>
<?php endif; ?>
</div>
</div>
</div>
</section>

Сейчас ссылки постраничной навигации имеют вид:

http://server.com/catalog/category/12?page=3&per-page=10

Давайте изменим это, чтобы ссылки имели вид:

http://server.com/catalog/category/12/page/3
Для этого добавим еще пару параметров к настройкам Pagination:
$pages = new Pagination([
'totalCount' => $query->count(),
'pageSize' => 10,
'forcePageParam' => false,
'pageSizeParam' => false
]);
И еще одно правило для маршрутов в файл config/web.php:
$config = [
/*...*/
'components' => [
/*...*/
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'catalog/category/<id:\d+>/page/<page:\d+>' => 'catalog/category',
'catalog/category/<id:\d+>' => 'catalog/category',
'catalog/brand/<id:\d+>' => 'catalog/brand',
],
],
],
/*...*/
];

Магазин на Yii2, часть 11. Пагинация и мета-теги для


брендов
Мы добавили мета-теги и постраничную навигацию для страниц категорий. Теперь сделаем это для
страниц брендов. Пагинация для товаров бренда мало чем отличается от пагинации для товаров
категории. А в таблице brand базы данных предусмотрены поля keywords и description, которые будут
заполняться при создании нового бренда через панель управления.
<?php
namespace app\models;

use yii\data\Pagination;
use yii\db\ActiveRecord;
use Yii;

class Brand extends ActiveRecord {

/*...*/

/**
* Возвращает массив всех товаров бренда с идентификатором $id
*/
public function getBrandProducts($id) {
$id = (int)$id;
// для постаничной навигации получаем только часть товаров
$query = Product::find()->where(['brand_id' => $id]);
$pages = new Pagination([
'totalCount' => $query->count(),
// количество товаров на странице теперь в настройках
'pageSize' => Yii::$app->params['pageSize'],
'forcePageParam' => false,
'pageSizeParam' => false
]);
$products = $query->offset($pages->offset)
->limit($pages->limit)
->asArray()
->all();
return [$products, $pages];
}
}
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use Yii;

class CatalogController extends AppController {

/*...*/

/**
* Список товаров бренда с идентификатором $id
*/
public function actionBrand($id) {
$id = (int)$id;
$temp = new Brand();
// товары бренда
list($products, $pages) = $temp->getBrandProducts($id);
// данные о бренде
$brand = $temp->getBrand($id);
// устанавливаем мета-теги
$this->setMetaTags(
$brand->name . ' | ' . Yii::$app->params['shopName'],
$brand->keywords,
$brand->description
);
return $this->render(
'brand',
compact('brand', 'products', 'pages')
);
}
}

Добавляем в view-шаблон виджет постраничной навигации:

<?php
/*
* Страница списка товаров бренда, файл views/catalog/brand.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($products)): /* выводим товары бренда */ ?>
<h2><?= Html::encode($brand['name']); ?></h2>
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$product['image'],
['alt' => $product['name'], 'class' =>
'img-responsive']
);
?>
<h2><?= $product['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $product['id']]); ?>">
<?= Html::encode($product['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($product['new']) { // новинка?
echo Html::img(
'@web/images/home/new.png',
['alt' => 'Новинка', 'class' => 'new']
);
}
if ($product['sale']) { // распродажа?
echo Html::img(
'@web/images/home/sale.png',
['alt' => 'Распродажа', 'class' =>
'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?= LinkPager::widget(['pagination' => $pages]); /*
постраничная навигация */ ?>
<?php else: ?>
<p>Нет товаров у этого бренда.</p>
<?php endif; ?>
</div>
</div>
</div>
</section>
Выносим в настройки количество товаров на странице при пагинации, файл config/params.php
<?php
return [
'adminEmail' => 'admin@example.com',
'senderEmail' => 'noreply@example.com',
'senderName' => 'Example.com mailer',
/*
* Название магазина, например «Lamoda» или «WildBerries»
*/
'shopName' => 'Магазин одежды и обуви',
/*
* Значения по умолчанию для мета-тегов title, keywords и description
*/
'defaultTitle' => 'Интернет-магазин модной одежды и обуви',
'defaultKeywords' => 'одежда, обувь, мужская, женская, детская, зимняя,
летняя',
'defaultDescription' => 'Коллекции женской, мужской, детской одежды и обуви',
/*
* Количество товаров на странице для постраничной навигации
*/
'pageSize' => 10,
];
И добавляем еще одно правило в config/web.php:
$config = [
/* ... */
'components' => [
/* ... */
'urlManager' => [
/* ... */
'rules' => [
'catalog/category/<id:\d+>/page/<page:\d+>' => 'catalog/category',
'catalog/category/<id:\d+>' => 'catalog/category',
'catalog/brand/<id:\d+>/page/<page:\d+>' => 'catalog/brand',
'catalog/brand/<id:\d+>' => 'catalog/brand',
],
],
],
/* ... */
];
Магазин на Yii2, часть 12. Промежуточные итоги и
рефакторинг кода
Некий промежуточный итог — здесь все, что было сделано на текущий момент. Исправлены ошибки,
допущенные ранее. Добавлены новые поля в таблицы базы данных. Переписаны некоторые фрагменты
кода, которые оказались неудачными. Добавлено кеширование тяжелых фрагментов кода, связанных с
выборкой данных из БД.

Контроллеры AppController, PageController и CatalogController

<?php
namespace app\controllers;

use yii\web\Controller;
use Yii;

class AppController extends Controller {

/**
* Метод устанавливает мета-теги для страницы сайта
* @param string $title
* @param string $keywords
* @param string $description
*/
protected function setMetaTags($title = '', $keywords = '', $description = '')
{
$this->view->title = $title ?: Yii::$app->params['defaultTitle'];
$this->view->registerMetaTag([
'name' => 'keywords',
'content' => $keywords ?: Yii::$app->params['defaultKeywords']
]);
$this->view->registerMetaTag([
'name' => 'description',
'content' => $description ?: Yii::$app->params['defaultDescription']
]);
}
}
<?php
namespace app\controllers;

use app\models\Product;
use Yii;

class PageController extends AppController {


/*
* Главная страница сайта
*/
public function actionIndex() {
// получаем лидеров продаж
$hitProducts = Yii::$app->cache->get('hit-products');
if ($hitProducts === false) {
$hitProducts = Product::find()->where(['hit' => 1])->limit(3)-
>asArray()->all();
Yii::$app->cache->set('hit-products', $hitProducts);
}
// получаем новые товары
$newProducts = Yii::$app->cache->get('new-products');
if ($newProducts === false) {
$newProducts = Product::find()->where(['new' => 1])->limit(3)-
>asArray()->all();
Yii::$app->cache->set('new-products', $newProducts);
}
// получаем товары распродажи
$saleProducts = Yii::$app->cache->get('sale-products');
if ($saleProducts === false) {
$saleProducts = Product::find()->where(['sale' => 1])->limit(3)-
>asArray()->all();
Yii::$app->cache->set('sale-products', $saleProducts);
}

// устанавливаем мета-теги для страницы


$this->setMetaTags();

return $this->render(
'index',
compact('hitProducts', 'newProducts', 'saleProducts')
);
}
}
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use Yii;

class CatalogController extends AppController {


/**
* Главная страница каталога товаров
*/
public function actionIndex() {
// получаем корневые категории
$roots = Yii::$app->cache->get('root-categories');
if ($roots === false) {
$roots = Category::find()->where(['parent_id' => 0])->asArray()->all();
Yii::$app->cache->set('root-categories', $roots);
}
// получаем популярные бренды
$brands = Yii::$app->cache->get('popular-brands');
if ($brands === false) {
$brands = (new Brand())->getPopularBrands();
Yii::$app->cache->set('popular-brands', $brands);
}
return $this->render('index', compact('roots', 'brands'));
}

/**
* Категория каталога товаров
*/
public function actionCategory($id, $page = 1) {
$id = (int)$id;
$page = (int)$page;
// пробуем извлечь данные из кеша
$data = Yii::$app->cache->get('category-'.$id.'-page-'.$page);
if ($data === false) {
// данных нет в кеше, получаем их заново
$temp = new Category();
// товары категории
list($products, $pages) = $temp->getCategoryProducts($id);
// данные о категории
$category = $temp->getCategory($id);
// сохраняем полученные данные в кеше
$data = [$products, $pages, $category];
Yii::$app->cache->set('category-'.$id.'-page-'.$page, $data);
}
list($products, $pages, $category) = $data;
// устанавливаем мета-теги для страницы
$this->setMetaTags(
$category['name'] . ' | ' . Yii::$app->params['shopName'],
$category['keywords'],
$category['description']
);
return $this->render(
'category',
compact('category', 'products', 'pages')
);
}

/**
* Список всех брендов каталога товаров
*/
public function actionBrands() {
// пробуем извлечь данные из кеша
$brands = Yii::$app->cache->get('all-brands');
if ($brands === false) {
// данных нет в кеше, получаем их заново
$brands = (new Brand())->getAllBrands();
// сохраняем полученные данные в кеше
Yii::$app->cache->set('all-brands', $brands);
}
return $this->render(
'brands',
compact('brands')
);
}

/**
* Список товаров бренда с идентификатором $id
*/
public function actionBrand($id, $page = 1) {
$id = (int)$id;
// пробуем извлечь данные из кеша
$data = Yii::$app->cache->get('brand-'.$id.'-page-'.$page);
if ($data === false) {
// данных нет в кеше, получаем их заново
$temp = new Brand();
// товары бренда
list($products, $pages) = $temp->getBrandProducts($id);
// данные о бренде
$brand = $temp->getBrand($id);
// сохраняем полученные данные в кеше
$data = [$products, $pages, $brand];
Yii::$app->cache->set('brand-'.$id.'-page-'.$page, $data);
}
list($products, $pages, $brand) = $data;
// устанавливаем мета-теги
$this->setMetaTags(
$brand['name'] . ' | ' . Yii::$app->params['shopName'],
$brand['keywords'],
$brand['description']
);
return $this->render(
'brand',
compact('brand', 'products', 'pages')
);
}
}
Модели Brand и Category

<?php
namespace app\models;

use yii\data\Pagination;
use yii\db\ActiveRecord;
use Yii;

class Brand extends ActiveRecord {

/**
* Метод возвращает имя таблицы БД
*/
public static function tableName() {
return 'brand';
}

/**
* Метод возвращает массив товаров бренда
*/
public function getProducts() {
// связь таблицы БД `brand` с таблицей `product`
return $this->hasMany(Product::class, ['brand_id' => 'id']);
}

/**
* Возвращает информацию о бренде с идентификатором $id
*/
public function getBrand($id) {
return self::find()->where(['id' => $id])->asArray()->one();
}

/**
* Возвращает массив популярных брендов и
* количество товаров для каждого бренда
*/
public function getPopularBrands() {
// получаем бренды с наибольшим кол-вом товаров
$brands = self::find()
->select([
'id' => 'brand.id',
'name' => 'brand.name',
'content' => 'brand.content',
'image' => 'brand.image',
'count' => 'COUNT(*)'
])
->innerJoin(
'product',
'product.brand_id = brand.id'
)
->groupBy([
'brand.id', 'brand.name', 'brand.content', 'brand.image'
])
->orderBy(['count' => SORT_DESC])
->limit(10)
->asArray()
// для дальнейшей сортировки
->indexBy('name')
->all();
// теперь нужно отсортировать бренды по названию
ksort($brands);
return $brands;
}
/**
* Возвращает массив всех брендов каталога и
* количество товаров для каждого бренда
*/
public function getAllBrands() {
return self::find()
->select([
'id' => 'brand.id',
'name' => 'brand.name',
'content' => 'brand.content',
'image' => 'brand.image',
'count' => 'COUNT(*)'
])
->innerJoin(
'product',
'product.brand_id = brand.id'
)
->groupBy([
'brand.id', 'brand.name', 'brand.content', 'brand.image'
])
->orderBy(['name' => SORT_ASC])
->asArray()
->all();
}

/**
* Возвращает массив всех товаров бренда с идентификатором $id
*/
public function getBrandProducts($id) {
// для постаничной навигации получаем только часть товаров
$query = Product::find()->where(['brand_id' => $id]);
$pages = new Pagination([
'totalCount' => $query->count(),
'pageSize' => Yii::$app->params['pageSize'],
'forcePageParam' => false,
'pageSizeParam' => false
]);
$products = $query
->offset($pages->offset)
->limit($pages->limit)
->asArray()
->all();
return [$products, $pages];
}
}
<?php
namespace app\models;

use yii\data\Pagination;
use yii\db\ActiveRecord;
use Yii;

class Category extends ActiveRecord {

/**
* Метод возвращает имя таблицы БД
*/
public static function tableName() {
return 'category';
}

/**
* Метод описывает связь таблицы БД `category` с таблицей `product`
*/
public function getProducts() {
// связь таблицы БД `category` с таблицей `product`
return $this->hasMany(Product::class, ['category_id' => 'id']);
}

/**
* Метод описывает связь таблицы БД `category` с таблицей `category`
*/
public function getParent() {
return $this->hasOne(self::class, ['id' => 'parent_id']);
}

/**
* Метод описывает связь таблицы БД `category` с таблицей `category`
*/
public function getChildren() {
return $this->hasMany(self::class, ['parent_id' => 'id']);
}

/**
* Возвращает информацию о категории с иденификатором $id
*/
public function getCategory($id) {
return self::find()->where(['id' => $id])->asArray()->one();
}

/**
* Возвращает массив всех товаров в категории с идентификатором $id
* и в ее потомках, т.е. в дочерних, дочерних-дочерних и так далее
*/
public function getCategoryProducts($id) {
// получаем массив идентификаторов всех потомков категории
$ids = $this->getAllChildIds($id);
$ids[] = $id;
// для постаничной навигации получаем только часть товаров
$query = Product::find()->where(['in', 'category_id', $ids]);
$pages = new Pagination([
'totalCount' => $query->count(),
'pageSize' => Yii::$app->params['pageSize'],
'forcePageParam' => false,
'pageSizeParam' => false
]);
$products = $query
->offset($pages->offset)
->limit($pages->limit)
->asArray()
->all();
return [$products, $pages];
}

/**
* Возвращает массив идентификаторов всех потомков категории $id,
* т.е. дочерние, дочерние дочерних и так далее
*/
protected function getAllChildIds($id) {
$children = [];
$ids = $this->getChildIds($id);
foreach ($ids as $item) {
$children[] = $item;
$c = $this->getAllChildIds($item);
foreach ($c as $v) {
$children[] = $v;
}
}
return $children;
}

/**
* Возвращает массив идентификаторов дочерних категорий (прямых
* потомков) категории с уникальным идентификатором $id
*/
protected function getChildIds($id) {
$children = self::find()->where(['parent_id' => $id])->asArray()->all();
$ids = [];
foreach ($children as $child) {
$ids[] = $child['id'];
}
return $ids;
}
}

Шаблоны в директории views/catalog

<?php
/*
* Главная страница сайта, файл views/page/index.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Url;
use yii\helpers\Html;
?>
<section>
<div class="container">
<!-- Слайдер из трех элементов -->
<div id="slider" class="carousel slide" data-ride="carousel">
<!-- Индикатор текущего элемента -->
<ol class="carousel-indicators">
<!-- Активный элемент -->
<li data-target="#slider" data-slide-to="0" class="active"></li>
<li data-target="#slider" data-slide-to="1"></li>
<li data-target="#slider" data-slide-to="2"></li>
</ol>

<!-- Обертка для слайдов -->


<div class="carousel-inner" role="listbox">
<!-- Активный элемент -->
<div class="item active">
<img src="/images/slider/1.jpg" alt="...">
<div class="carousel-caption">Первый элемент слайдера</div>
</div>
<div class="item">
<img src="/images/slider/2.jpg" alt="...">
<div class="carousel-caption">Второй элемент слайдера</div>
</div>
<div class="item">
<img src="/images/slider/3.jpg" alt="...">
<div class="carousel-caption">Третий элемент слайдера</div>
</div>
</div>

<!-- Элементы управления -->


<a class="left carousel-control" href="#carousel-example"
role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left" aria-
hidden="true"></span>
<span class="sr-only">Предыдущий</span>
</a>
<a class="right carousel-control" href="#carousel-example"
role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right" aria-
hidden="true"></span>
<span class="sr-only">Следующий</span>
</a>
</div>
</div>
</section>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($hits)): ?>
<h2>Лидеры продаж</h2>
<div class="row">
<?php foreach ($hits as $hit): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$hit['image'],
['alt' => $hit['name'], 'class' => 'img-
responsive']
);
?>
<h2><?= $hit['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $hit['id']]); ?>">
<?= Html::encode($hit['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($hit['new']) { // новинка?
echo Html::img(
'@web/images/home/new.png',
['alt' => 'Новинка', 'class' => 'new']
);
}
if ($hit['sale']) { // распродажа?
echo Html::img(
'@web/images/home/sale.png',
['alt' => 'Распродажа', 'class' =>
'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php
/*
* Главная страница каталога, файл views/catalog/index.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($roots)): ?>
<h2>Одежда и обувь</h2>
<div class="row">
<?php foreach ($roots as $root): ?>
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<?=
Html::img(
'@web/images/roots/'.$root['image'],
['alt' => $root['name']]
);
?>
<div class="caption">
<h2>
<a href="<?=
Url::to(['catalog/category', 'id' => $root['id']]); ?>">
<?= Html::encode($root['name']); ?>
</a>
</h2>
<p><?= Html::encode($root['content']);
?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>

<?php if (!empty($brands)): ?>


<h2>Популярные бренды</h2>
<div class="row">
<?php foreach ($brands as $brand): ?>
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<?=
Html::img(
'@web/images/brands/'.$brand['image'],
['alt' => $brand['name']]
);
?>
<div class="caption">
<h2>
<a href="<?= Url::to(['catalog/brand',
'id' => $brand['id']]); ?>">
<?= Html::encode($brand['name']);
?>
</a>
</h2>
<p><?= Html::encode($brand['content']);
?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php
/*
* Страница раздела каталога, файл views/catalog/category.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($products)): ?>
<h2><?= Html::encode($category['name']); ?></h2>
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$product['image'],
['alt' => $product['name'], 'class' =>
'img-responsive']
);
?>
<h2><?= $product['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $product['id']]); ?>">
<?= Html::encode($product['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($product['new']) { // новинка?
echo Html::img(
'@web/images/home/new.png',
['alt' => 'Новинка', 'class' => 'new']
);
}
if ($product['sale']) { // распродажа?
echo Html::img(
'@web/images/home/sale.png',
['alt' => 'Распродажа', 'class' =>
'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?= LinkPager::widget(['pagination' => $pages]); /*
постраничная навигация */ ?>
<?php else: ?>
<p>Нет товаров в этой категории.</p>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php
/*
* Страница списка всех брендов, файл views/catalog/brands.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9 padding-right">


<h1>Все бренды</h1>
<?php if (!empty($brands)): ?>
<div class="row">
<?php foreach ($brands as $brand): ?>
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<?=
Html::img(
'@web/images/brands/'.$brand['image'],
['alt' => $brand['name']]
);
?>
<div class="caption">
<h2>
<a href="<?= Url::to(['catalog/brand',
'id' => $brand['id']]); ?>">
<?= Html::encode($brand['name']);
?>
</a>
</h2>
<p><?= Html::encode($brand['content']);
?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php
/*
* Страница списка товаров бренда, файл views/catalog/brand.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($products)): /* выводим товары бренда */ ?>
<h2><?= Html::encode($brand['name']); ?></h2>
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$product['image'],
['alt' => $product['name'], 'class' =>
'img-responsive']
);
?>
<h2><?= $product['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $product['id']]); ?>">
<?= Html::encode($product['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($product['new']) { // новинка?
echo Html::img(
'@web/images/home/new.png',
['alt' => 'Новинка', 'class' => 'new']
);
}
if ($product['sale']) { // распродажа?
echo Html::img(
'@web/images/home/sale.png',
['alt' => 'Распродажа', 'class' =>
'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?= LinkPager::widget(['pagination' => $pages]); /*
постраничная навигация */ ?>
<?php else: ?>
<p>Нет товаров у этого бренда.</p>
<?php endif; ?>
</div>
</div>
</div>
</section>

Магазин на Yii2, часть 13. Страница товара и хлебные


крошки
Следующий этап — создаем модель, контроллер и представление для показа страницы товара. Для
снижения нагрузки полученные от модели данные кешируем в контроллере. Заодно добавим хлебные
крошки для страницы товара и для раздела каталога. Для этого создадим виджет ChainWidget и будем
вызывать его в view-шаблонах товара и категории.

Контроллер, модель и представление для товара


<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use Yii;

class CatalogController extends AppController {

/*...*/

/**
* Страница товара с идентификатором $id
*/
public function actionProduct($id) {
$id = (int)$id;
// пробуем извлечь данные из кеша
$data = Yii::$app->cache->get('product-'.$id);
if ($data === false) {
// данных нет в кеше, получаем их заново
$product = (new Product())->getProduct($id);
$brand = (new Brand())->getBrand($product['brand_id']);
$data = [$product, $brand];
// сохраняем полученные данные в кеше
Yii::$app->cache->set('product-'.$id, $data);
}
list($product, $brand) = $data;
// устанавливаем мета-теги
$this->setMetaTags(
$product['name'] . ' | ' . Yii::$app->params['shopName'],
$product['keywords'],
$product['description']
);
// получаем товары, похожие на текущий
$similar = Yii::$app->cache->get('similar-'.$product['id']);
if ($similar === false) {
// товары из той же категории того же бренда
$similar = Product::find()
->where([
'category_id' => $product['category_id'],
'brand_id' => $product['brand_id']
])
->andWhere(['NOT IN', 'id', $product['id']])
->limit(3)
->asArray()
->all();
Yii::$app->cache->set('similar-'.$product['id'], $similar);
}
return $this->render(
'product',
compact('product', 'brand', 'similar')
);
}
}
<?php
namespace app\models;

use yii\db\ActiveRecord;

class Product extends ActiveRecord {

/**
* Возвращает имя таблицы БД
*/
public static function tableName() {
return 'product';
}

/**
* Возвращает родительскую категорию
*/
public function getCategory() {
// связь таблицы БД `product` с таблицей `category`
return $this->hasOne(Category::class, ['id' => 'category_id']);
}

/**
* Возвращает информацию о товаре с иденификатором $id
*/
public function getProduct($id) {
return self::find()->where(['id' => $id])->asArray()->one();
}

}
<?php
/*
* Страница товара, файл views/catalog/product.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use app\components\ChainWidget;
use yii\helpers\Url;
use yii\helpers\Html;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>
<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<h1><?= Html::encode($product['name']); ?></h1>
<div class="row">
<div class="col-sm-5">
<div class="product-image">
<?=
Html::img(
'@web/images/products/large/'.$product['image'],
['alt' => $product['name'], 'class' => 'img-
responsive']
);
?>
</div>
</div>
<div class="col-sm-7">
<div class="product-info">
<p class="product-price">
Цена: <span><?= $product['price']; ?></span> руб.
</p>
<form>
<label>Количество</label>
<input name="count" type="text" value="1" />
<button type="button" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</button>
</form>
<p>Артикул: 1234567</p>
<p>Наличие: На складе</p>
<p>
Бренд:
<a href="<?= Url::to(['catalog/brand', 'id' =>
$brand['id']]); ?>">
<?= Html::encode($brand['name']); ?>
</a>
</p>

</div>
</div>
</div>
<div class="product-descr">
<?= $product['content']; ?>
</div>
<?php if (!empty($similar)): /* похожие товары */ ?>
<h2>Похожие товары</h2>
<div class="row">
<?php foreach ($similar as $item): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$item['image'],
['alt' => $item['name'], 'class' => 'img-
responsive']
);
?>
<h2><?= $item['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $item['id']]); ?>">
<?= Html::encode($item['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($product['new']) { // новинка?
echo Html::tag(
'span',
'Новинка',
['class' => 'new']
);
}
if ($product['hit']) { // лидер продаж?
echo Html::tag(
'span',
'Лидер продаж',
['class' => 'hit']
);
}
if ($product['sale']) { // распродажа?
echo Html::tag(
'span',
'Распродажа',
['class' => 'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>

Хлебные крошки
Создаем файл components/ChainWidget.php:
<?php
namespace app\components;

use app\models\Category;
use yii\base\Widget;
use Yii;

/**
* Виджет для вывода цепочки навигации (хлебные крошки)
*/
class ChainWidget extends Widget {

/**
* Идентификатор текущей категории
*/
public $itemCurrent;
/**
* Показывать текущую категорию?
*/
public $showCurrent = true;

public function run() {


if (empty($this->itemCurrent)) {
return '';
}
// пробуем извлечь данные из кеша
$show = $this->showCurrent ? 'true' : 'false';
$key = 'widget-chain-'.$this->itemCurrent.'-show-'.$show;
$html = Yii::$app->cache->get($key);
if ($html === false) {
// данных нет в кеше, получаем их заново
$chain = (new Category())->getParents($this->itemCurrent);
if (!$this->showCurrent) {
array_pop($chain);
}
$html = $this->render('chain', ['chain' => $chain]);
// сохраняем полученные данные в кеше
Yii::$app->cache->set($key, $html);
}
return $html;
}

}
И создаем файл components/views/chain.php
<?php
/*
* Файл components/views/brands.php
*/
use yii\helpers\Html;
use yii\helpers\Url;

if (empty($chain)) {
return;
}
?>

<ol class="breadcrumb">
<?php foreach ($chain as $item): ?>
<li>
<a href="<?= Url::to(['catalog/category', 'id' => $item->id]); ?>">
<?= Html::encode($item->name); ?>
</a>
</li>
<?php endforeach; ?>
</ol>

Вызов виджета для страницы категории каталога:

<?= ChainWidget::widget(['itemCurrent' => $category['id'], 'showCurrent' =>


false]); ?>

Вызов виджета для страницы товара каталога:

<?= ChainWidget::widget(['itemCurrent' => $product['category_id']]); ?>


Магазин на Yii2, часть 14. Показываем страницу 404
Not Found
После установки Yii2 уже имеется файл view-шаблона views/site/error.php. Чтобы получить свою
страницу ошибки, можно просто отредактировать этот файл. В нём доступны три
переменные: $name, $message, $exception. Признак ошибки 404 — это значение
свойства statusCode объекта $exception. Таким образом можно написать свою проверку этого
значения и для разных ошибок показывать пользователю разный ответ.
Почему для показа сообщения об ошибках используется шаблон views/site/error.php? Это задается
в настройках приложения, в файле config/web.php:
$config = [
/* ... */
'components' => [
/* ... */
'errorHandler' => [
'errorAction' => 'site/error'
],
/* ... */
],
/* ... */
];
Для обработки ошибок будет использован метод actionError() класса SiteController:
class SiteController extends Controller {
/* ... */
public function actionError() {
$exception = Yii::$app->errorHandler->exception;
if ($exception !== null) {
return $this->render('error', ['exception' => $exception]);
}
}
/* ... */
}
Но если мы заглянем в исходный код controllers/SiteController.php, то обнаружим, что такого
метода там нет. А вместо него есть метод actions():
class SiteController extends Controller {
/* ... */
public function actions()
{
return [
'error' => [
// объявляем действие error и задаем имя класса
'class' => 'yii\web\ErrorAction',
],
];
}
/* ... */
}
Все действия контроллера можно разделить на встроенные и отдельные. Встроенные действия определены
как методы контроллера, например actionError(). Отдельные действия указываются в карте действий
контроллера, в методе actions(). Сам же функционал таких действий описан в отдельных классах.

Мы договорились ранее использовать класс SiteController только как образец. Давайте зададим свой
обработчик ошибки в файле конфигурации config/web.php:
$config = [
/* ... */
'components' => [
/* ... */
'errorHandler' => [
'errorAction' => 'app/error'
],
/* ... */
],
/* ... */
];
Добавим метод actions() в класс AppController:
class AppController extends Controller {
/* ... */
public function actions() {
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
];
}
/* ... */
}
Создадим view-шаблон views/app/error.php:
<?php
/* @var $this yii\web\View */
/* @var $name string */
/* @var $message string */
/* @var $exception Exception */

use yii\helpers\Html;

$this->title = $name;
?>
<div class="container">
<h1><?= Html::encode($this->title) ?></h1>
<div class="alert alert-danger">
<?= nl2br(Html::encode($message)) ?>
</div>
</div>
И внесем изменения в класс CatalogController, чтобы он выбрасывал исключение, когда в базе
данных не удается найти категорию, бренд или товар каталога:
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use yii\web\HttpException;
use Yii;

class CatalogController extends AppController {


/**
* Главная страница каталога товаров
*/
public function actionIndex() {
// получаем корневые категории
$roots = Yii::$app->cache->get('root-categories');
if ($roots === false) {
$roots = Category::find()->where(['parent_id' => 0])->asArray()->all();
Yii::$app->cache->set('root-categories', $roots);
}
// получаем популярные бренды
$brands = Yii::$app->cache->get('popular-brands');
if ($brands === false) {
$brands = (new Brand())->getPopularBrands();
Yii::$app->cache->set('popular-brands', $brands);
}
return $this->render('index', compact('roots', 'brands'));
}

/**
* Категория каталога товаров
*/
public function actionCategory($id, $page = 1) {
$id = (int)$id;
$page = (int)$page;
// пробуем извлечь данные из кеша
$data = Yii::$app->cache->get('category-'.$id.'-page-'.$page);
if ($data === null) {
// данные есть в кеше, но такой категории не существует
throw new HttpException(
404,
'Запрошенная страница не найдена'
);
}
if ($data === false) {
// данных нет в кеше, получаем их заново
$temp = new Category();
// данные о категории
$category = $temp->getCategory($id);
if (!empty($category)) { // такая категория существует
// товары категории
list($products, $pages) = $temp->getCategoryProducts($id);
// сохраняем полученные данные в кеше
$data = [$products, $pages, $category];
Yii::$app->cache->set('category-' . $id . '-page-' . $page, $data);
} else { // такая категория не существует
Yii::$app->cache->set('category-' . $id . '-page-' . $page, null);
throw new HttpException(
404,
'Запрошенная страница не найдена'
);
}
}
list($products, $pages, $category) = $data;
// устанавливаем мета-теги для страницы
$this->setMetaTags(
$category['name'] . ' | ' . Yii::$app->params['shopName'],
$category['keywords'],
$category['description']
);
return $this->render(
'category',
compact('category', 'products', 'pages')
);
}

/**
* Список всех брендов каталога товаров
*/
public function actionBrands() {
// пробуем извлечь данные из кеша
$brands = Yii::$app->cache->get('all-brands');
if ($brands === false) {
// данных нет в кеше, получаем их заново
$brands = (new Brand())->getAllBrands();
// сохраняем полученные данные в кеше
Yii::$app->cache->set('all-brands', $brands);
}
return $this->render(
'brands',
compact('brands')
);
}

/**
* Список товаров бренда с идентификатором $id
*/
public function actionBrand($id, $page = 1) {
$id = (int)$id;
$page = (int)$page;
// пробуем извлечь данные из кеша
$data = Yii::$app->cache->get('brand-'.$id.'-page-'.$page);
if ($data === null) {
// данные есть в кеше, но такого бренда не существует
throw new HttpException(
404,
'Запрошенная страница не найдена'
);
}
if ($data === false) {
// данных нет в кеше, получаем их заново
$temp = new Brand();
// данные о бренде
$brand = $temp->getBrand($id);
if (!empty($brand)) { // такой бренд существует
// товары бренда
list($products, $pages) = $temp->getBrandProducts($id);
// сохраняем полученные данные в кеше
$data = [$products, $pages, $brand];
Yii::$app->cache->set('brand-'.$id.'-page-'.$page, $data);
} else { // такой бренд не существует
Yii::$app->cache->set('brand-'.$id.'-page-'.$page, null);
throw new HttpException(
404,
'Запрошенная страница не найдена'
);
}
}
list($products, $pages, $brand) = $data;
// устанавливаем мета-теги
$this->setMetaTags(
$brand['name'] . ' | ' . Yii::$app->params['shopName'],
$brand['keywords'],
$brand['description']
);
return $this->render(
'brand',
compact('brand', 'products', 'pages')
);
}

/**
* Страница товара с идентификатором $id
*/
public function actionProduct($id) {
$id = (int)$id;
// пробуем извлечь данные из кеша
$data = Yii::$app->cache->get('product-'.$id);
if ($data === null) {
// данные есть в кеше, но такого товара не существует
throw new HttpException(
404,
'Запрошенная страница не найдена'
);
}
if ($data === false) {
// данных нет в кеше, получаем их заново
$product = (new Product())->getProduct($id);
if (!empty($product)) { // такой товар существует
$brand = (new Brand())->getBrand($product['brand_id']);
$data = [$product, $brand];
// сохраняем полученные данные в кеше
Yii::$app->cache->set('product-' . $id, $data);
} else { // такого товара не существует
Yii::$app->cache->set('product-' . $id, null);
throw new HttpException(
404,
'Запрошенная страница не найдена'
);
}
}
list($product, $brand) = $data;
// устанавливаем мета-теги
$this->setMetaTags(
$product['name'] . ' | ' . Yii::$app->params['shopName'],
$product['keywords'],
$product['description']
);
// получаем популярные товары, похожие на текущий
$similar = Yii::$app->cache->get('similar-'.$product['id']);
if ($similar === false) {
// товары из той же категории того же бренда
$similar = Product::find()
->where([
'hit' => 1,
'category_id' => $product['category_id'],
'brand_id' => $product['brand_id']
])
->andWhere(['NOT IN', 'id', $product['id']])
->limit(3)
->asArray()
->all();
Yii::$app->cache->set('similar-'.$product['id'], $similar);
}
return $this->render(
'product',
compact('product', 'brand', 'similar')
);
}
}

Магазин на Yii2, часть 15. Поиск по каталогу товаров,


часть первая
Какой каталог товаров без поиска? Тем более, что и форма в шаблоне предусмотрена. Давайте для
начала реализуем самый простой вариант с использованием LIKE. А потом немного усложним —
добавим в SQL-запрос расчет релевантности и выполним редирект после отправки формы — чтобы
сформировать краcивые URL.

Простой вариант
Начнем с layout-шаблона views/layouts/main.php, где у нас расположена форма поиска:
<div class="col-sm-4">
<form method="get" action="<?= Url::to(['catalog/search']); ?>" class="pull-
right">
<div class="input-group">
<input type="text" name="query" class="form-control" placeholder="Поиск
по каталогу">
<div class="input-group-btn">
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-search"></span>
</button>
</div>
</div>
</form>
</div>
Добавим метод actionSearch() в контроллер CatalogController:
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use yii\web\HttpException;
use Yii;

class CatalogController extends AppController {

/* ... */

/**
* Результаты поиска по каталогу товаров
*/
public function actionSearch($query = '', $page = 1) {

$page = (int)$page;

// получаем результаты поиска с постраничной навигацией


list($products, $pages) = (new Product())->getSearchResult($query, $page);

// устанавливаем мета-теги для страницы


$this->setMetaTags('Поиск по каталогу');

return $this->render(
'search',
compact('products', 'pages')
);
}

}
В класс модели Product добавим метод getSearchResult():
<?php
namespace app\models;

use Yii;
use yii\data\Pagination;
use yii\db\ActiveRecord;

class Product extends ActiveRecord {

/* ... */

/**
* Результаты поиска по каталогу товаров
*/
public function getSearchResult($search, $page) {
$search = $this->cleanSearchString($search);
if (empty($search)) {
return [null, null];
}

// пробуем извлечь данные из кеша


$key = 'search-'.md5($search).'-page-'.$page;
$data = Yii::$app->cache->get($key);

if ($data === false) {


// данных нет в кеше, получаем их заново
$query = self::find()->where(['like', 'name', $search]);
// постраничная навигация
$pages = new Pagination([
'totalCount' => $query->count(),
'pageSize' => Yii::$app->params['pageSize'],
'forcePageParam' => false,
'pageSizeParam' => false
]);
$products = $query
->offset($pages->offset)
->limit($pages->limit)
->asArray()
->all();
// сохраняем полученные данные в кеше
$data = [$products, $pages];
Yii::$app->cache->set($key, $data);
}

return $data;
}

/**
* Вспомогательная функция, очищает строку поискового запроса с сайта
* от всякого мусора
*/
protected function cleanSearchString($search) {
$search = iconv_substr($search, 0, 64);
// удаляем все, кроме букв и цифр
$search = preg_replace('#[^0-9a-zA-ZА-Яа-яёЁ]#u', ' ', $search);
// сжимаем двойные пробелы
$search = preg_replace('#\s+#u', ' ', $search);
$search = trim($search);
return $search;
}

}
Теперь создадим view-шаблон views/catalog/search.php:
<?php
/*
* Страница результатов поиска по каталогу, файл views/catalog/search.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9">
<?php if (!empty($products)): ?>
<h2>Результаты поиска по каталогу</h2>
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$product['image'],
['alt' => $product['name'], 'class' =>
'img-responsive']
);
?>
<h2><?= $product['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $product['id']]); ?>">
<?= Html::encode($product['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($product['new']) { // новинка?
echo Html::img(
'@web/images/home/new.png',
['alt' => 'Новинка', 'class' => 'new']
);
}
if ($product['sale']) { // распродажа?
echo Html::img(
'@web/images/home/sale.png',
['alt' => 'Распродажа', 'class' =>
'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?= LinkPager::widget(['pagination' => $pages]); /*
постраничная навигация */ ?>
<?php else: ?>
<p>По вашему запросу ничего не найдено.</p>
<?php endif; ?>
</div>
</div>
</div>
</section>

Сейчас URL страниц результатов поиска у нас выглядит так:

http://www.server.com/catalog/search?query=мужская+летняя+одежда

http://www.server.com/catalog/search?query=мужская+летняя+одежда&page=2

http://www.server.com/catalog/search?query=мужская+летняя+одежда&page=3

..........

Добавим правила маршрутизации в файл конфигурации <="" code="" style="margin: 0px; padding: 0px;
max-height: 1e+06px;">:

$config = [
/* ... */
'components' => [
/* ... */
'urlManager' => [
/* ... */
'rules' => [
'catalog/category/<id:\d+>/page/<page:\d+>' => 'catalog/category',
'catalog/category/<id:\d+>' => 'catalog/category',
'catalog/brand/<id:\d+>/page/<page:\d+>' => 'catalog/brand',
'catalog/brand/<id:\d+>' => 'catalog/brand',
'catalog/product/<id:\d+>' => 'catalog/product',
'catalog/search/page/<page:\d+>' => 'catalog/search',
'catalog/search' => 'catalog/search',
],
],
],
/* ... */
];

Теперь URL страниц результатов поиска будут выглядеть так:

http://www.server.com/catalog/search?query=мужская+летняя+одежда

http://www.server.com/catalog/search/page/2?query=мужская+летняя+одежда

http://www.server.com/catalog/search/page/3?query=мужская+летняя+одежда

..........

Сейчас наш код формирует вот такой поисковый запрос:

SELECT * FROM `product` WHERE `name` LIKE '%мужская летняя одежда%'

Чтобы запрос что-то вернул, все три слова должны быть в названии товара. И именно в том порядке, в
каком они встречаются в поисковом запросе. Если название товара «Одежда летняя мужская» — то
такой товар не попадет в результаты поиска. Давайте это исправим.

<?php
namespace app\models;

use Yii;
use yii\data\Pagination;
use yii\db\ActiveRecord;

class Product extends ActiveRecord {

/* ... */

/**
* Результаты поиска по каталогу товаров
*/
public function getSearchResult($search, $page) {
$search = $this->cleanSearchString($search);
if (empty($search)) {
return [null, null];
}

// пробуем извлечь данные из кеша


$key = 'search-'.md5($search).'-page-'.$page;
$data = Yii::$app->cache->get($key);

if ($data === false) { // данных нет в кеше, получаем их заново


// разбиваем поисковый запрос на отдельные слова
$words = explode(' ', $search);
$query = self::find()->where(['like', 'name', $words[0]]);
for ($i = 1; $i < count($words); $i++) {
$query = $query->andWhere(['like', 'name', $words[$i]]);
// $query = $query->orWhere(['like', 'name', $words[$i]]);
}
// постраничная навигация
$pages = new Pagination([/*...*/]);
$products = $query
->offset($pages->offset)
->limit($pages->limit)
->asArray()
->all();
// сохраняем полученные данные в кеше
$data = [$products, $pages];
Yii::$app->cache->set($key, $data);
}

return $data;
}

}
Мы можем сформировать один из двух вариантов запроса — используя andWhere() или orWhere():
SELECT * FROM `product` WHERE (`name` LIKE '%мужская%') AND (`name` LIKE
'%летняя%') AND (`name` LIKE '%одежда%')
SELECT * FROM `product` WHERE (`name` LIKE '%мужская%') OR (`name` LIKE '%летняя%')
OR (`name` LIKE '%одежда%')

В первом случае, чтобы товар попал в выборку, нужно, чтобы он содержал в названии все три слова. Во
втором случае — хотя бы одно слово. Оба варианта — так себе.

Сложный вариант
Давайте добавим в поисковый запрос расчет релевантности. И искать будем не только в названии
товара, но и в описании. А результаты поиска отсортируем по убыванию релевантности. В итоге получим
такой SQL-запрос:

SELECT

*,
IF (`name` LIKE '%мужская%', 2, 0) +
IF (`content` LIKE '%мужская%', 1, 0) +
IF (`name` LIKE '%летняя%', 2, 0) +
IF (`content` LIKE '%летняя%', 1, 0) +
IF (`name` LIKE '%одежда%', 2, 0) +
IF (`content` LIKE '%одежда%', 1, 0)
AS `relevance`
FROM
`product`
WHERE
`name` LIKE '%мужская%' OR
`content` LIKE '%мужская%' OR
`name` LIKE '%летняя%' OR
`content` LIKE '%летняя%' OR
`name` LIKE '%одежда%' OR
`content` LIKE '%одежда%'
ORDER BY
`relevance` DESC
<?php
namespace app\models;

use Yii;
use yii\data\Pagination;
use yii\db\ActiveRecord;
use yii\db\Query;

class Product extends ActiveRecord {

/* ... */

/**
* Результаты поиска по каталогу товаров
*/
public function getSearchResult($search, $page) {
$search = $this->cleanSearchString($search);
if (empty($search)) {
return [null, null];
}

// пробуем извлечь данные из кеша


$key = 'search-'.md5($search).'-page-'.$page;
$data = Yii::$app->cache->get($key);

if ($data === false) { // данных нет в кеше, получаем их заново


// разбиваем поисковый запрос на отдельные слова
$words = explode(' ', $search);
// рассчитываем релевантность для каждого товара
$relevance = "IF (`name` LIKE '%" . $words[0] . "%', 2, 0)";
$relevance .= " + IF (`content` LIKE '%" . $words[0] . "%', 1, 0)";
for ($i = 1; $i < count($words); $i++) {
$relevance .= " + IF (`name` LIKE '%" . $words[$i] . "%', 2, 0)";
$relevance .= " + IF (`content` LIKE '%" . $words[$i] . "%', 1,
0)";
}
$query = (new Query())
->select(['*', 'relevance' => $relevance])
->from('product')
->where(['like', 'name', $words[0]])
->orWhere(['like', 'content', $words[0]]);
for ($i = 1; $i < count($words); $i++) {
$query = $query->orWhere(['like', 'name', $words[$i]]);
$query = $query->orWhere(['like', 'content', $words[$i]]);
}
// сортируем разультаты по убыванию релевантности
$query = $query->orderBy(['relevance' => SORT_DESC]);
// постраничная навигация
$pages = new Pagination([
'totalCount' => $query->count(),
'pageSize' => Yii::$app->params['pageSize'],
'forcePageParam' => false,
'pageSizeParam' => false
]);
$products = $query
->offset($pages->offset)
->limit($pages->limit)
->all();
// сохраняем полученные данные в кеше
$data = [$products, $pages];
Yii::$app->cache->set($key, $data);
}

return $data;
}

И последнее, что хотелось бы сделать — изменить URL страниц результатов поиска, чтобы они имели
вид:

http://www.server.com/catalog/search/query/мужская+летняя+одежда

http://www.server.com/catalog/search/query/мужская+летняя+одежда/page/2

http://www.server.com/catalog/search/query/мужская+летняя+одежда/page/3

..........
Для этого изменим метод отправки данных с GET на POST. И добавим в форму скрытое поле со
значением CSRF токена, чтобы не получить ошибку
Bad Request (#400): Не удалось проверить переданные данные.
<div class="col-sm-4">
<form method="post" action="<?= Url::to(['catalog/search']); ?>" class="pull-
right">
<?=
Html::hiddenInput(
Yii::$app->request->csrfParam,
Yii::$app->request->csrfToken
);
?>
<div class="input-group">
<input type="text" name="query" class="form-control" placeholder="Поиск
по каталогу">
<div class="input-group-btn">
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-search"></span>
</button>
</div>
</div>
</form>
</div>
А в контроллере будем делать редирект на красивый URL результатов поиска, если данные пришли
методом POST:
<?php
namespace app\controllers;

use app\models\Category;
use app\models\Brand;
use app\models\Product;
use yii\web\HttpException;
use Yii;

class CatalogController extends AppController {

/* ... */

/**
* Результаты поиска по каталогу товаров
*/
public function actionSearch($query = '', $page = 1) {
/*
* Чтобы получить ЧПУ, выполняем редирект на catalog/search/query/одежда
* после отправки поискового запроса из формы методом POST. Если строка
* поискового запроса пустая, выполняем редирект на catalog/search.
*/
if (Yii::$app->request->isPost) {
$query = Yii::$app->request->post('query');
if (is_null($query)) {
return $this->redirect(['catalog/search']);
}
$query = trim($query);
if (empty($query)) {
return $this->redirect(['catalog/search']);
}
$query = urlencode(Yii::$app->request->post('query'));
return $this->redirect(['catalog/search/query/'.$query]);
}

$page = (int)$page;

// получаем результаты поиска с постраничной навигацией


list($products, $pages) = (new Product())->getSearchResult($query, $page);

// устанавливаем мета-теги для страницы


$this->setMetaTags('Поиск по каталогу');

return $this->render(
'search',
compact('products', 'pages')
);
}

И изменим правила маршрутизации в файле конфигурации <="" code="" style="margin: 0px; padding: 0px;
max-height: 1e+06px;">:

$config = [
/* ... */
'components' => [
/* ... */
'urlManager' => [
/* ... */
'rules' => [
'catalog/category/<id:\d+>/page/<page:\d+>' => 'catalog/category',
'catalog/category/<id:\d+>' => 'catalog/category',
'catalog/brand/<id:\d+>/page/<page:\d+>' => 'catalog/brand',
'catalog/brand/<id:\d+>' => 'catalog/brand',
'catalog/product/<id:\d+>' => 'catalog/product',
// правило для 2, 3, 4 страницы результатов поиска
'catalog/search/query/<query:.*?>/page/<page:\d+>' =>
'catalog/search',
// правило для первой страницы результатов поиска
'catalog/search/query/<query:.*?>' => 'catalog/search',
// правило для первой страницы с пустым запросом
'catalog/search' => 'catalog/search',
],
],
],
/* ... */
];

Магазин на Yii2, часть 16. Поиск по каталогу товаров,


часть вторая
У нашего поиска есть серьезная проблема — окончания слов. Например, в каталоге есть товар «Мужские
зимние ботинки», а пользователь ищет «зимняя обувь». Этот товар не попадет в результаты поиска,
потому что нет точного совпадения: в поисковом запросе используется слово «зимняя», а названии
товара используется слово «зимние».

Для решения этой проблемы используем стеммер Портера, который отсекает окончания и суффиксы
слова, оставляя только корень. Мне удалось найти на packagist.org готовый класс стеммера для русского
языка. Его и будем использовать.
Стеммер Портера — алгоритм стемминга, опубликованный Мартином Портером в 1980 году. Оригинальная
версия стеммера была предназначена для английского языка. Впоследствии Мартин создал проект «Snowball»
и, используя основную идею алгоритма, написал стеммеры для распространённых индоевропейских языков, в
том числе для русского.
Алгоритм не использует морфологический словарь, а только применяя последовательно ряд правил, отсекает
окончания и суффиксы, основываясь на особенностях языка, в связи с чем работает быстро, но не всегда
безошибочно.
Итак, устанавливаем пакет с использованием composer:
> composer require ladamalina/lingua-stem-ru

Но установка завершилась ошибкой:

[InvalidArgumentException]

Could not find a version of package ladamalina/lingua-stem-ru matching your


minimum-stability (stable).

Require it with an explicit version constraint allowing its desired stability.

require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [-


-no-update] [--no-scripts]

[--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--


ignore-platform-reqs]

[--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-


a|--classmap-authoritative]

[--apcu-autoloader] [--] [<packages>]...


Это потому, что в composer.json параметр minimum-stability имеет значение stable. Так что еще
одна попытка:
> composer require ladamalina/lingua-stem-ru:dev-master
Все, теперь нам доступен класс LinguaStemRu. Примеры использования класса:
$stemmer = new LinguaStemRu();
echo $stemmer->stem_word('Автомобиль') . "<br/>";
echo $stemmer->stem_word('Автомобилем') . "<br/>";
echo $stemmer->stem_word('Автомобиля') . "<br/>";
$stemmer = new LinguaStemRu();
echo $stemmer->stem_text('Любовь к Родине – это очень сильное чувство.');
любов к родин – это очен сильн чувство.

Искать будем по следующим полям таблиц базы данных:

1. поле name таблицы product (название товара)


2. поле keywords таблицы product (ключевые слова)
3. поле name таблицы category (название категории)
4. поле name таблицы brand (название бренда)
Вносим изменения в класс модели Product:
<?php
namespace app\models;

use Yii;
use yii\data\Pagination;
use yii\db\ActiveRecord;
use yii\db\Query;
use Stem\LinguaStemRu;

class Product extends ActiveRecord {


/*.....*/
public function getSearchResult($search, $page) {
$search = $this->cleanSearchString($search);
if (empty($search)) {
return [null, null];
}

// пробуем извлечь данные из кеша


$key = 'search-'.md5($search).'-page-'.$page;
$data = Yii::$app->cache->get($key);

if ($data === false) { // данных нет в кеше, получаем их заново


// разбиваем поисковый запрос на отдельные слова
$temp = explode(' ', $search);
$words = [];
$stemmer = new LinguaStemRu();
foreach ($temp as $item) {
if (iconv_strlen($item) > 3) {
// получаем корень слова
$words[] = $stemmer->stem_word($item);
} else {
$words[] = $item;
}
}
$relevance = "IF (`product`.`name` LIKE '%" . $words[0] . "%', 3, 0)";
$relevance .= " + IF (`product`.`keywords` LIKE '%" . $words[0] . "%',
2, 0)";
$relevance .= " + IF (`category`.`name` LIKE '%" . $words[0] . "%', 1,
0)";
$relevance .= " + IF (`brand`.`name` LIKE '%" . $words[0] . "%', 1,
0)";
for ($i = 1; $i < count($words); $i++) {
$relevance .= " + IF (`product`.`name` LIKE '%" . $words[$i] . "%',
3, 0)";
$relevance .= " + IF (`product`.`keywords` LIKE '%" . $words[$i] .
"%', 2, 0)";
$relevance .= " + IF (`category`.`name` LIKE '%" . $words[$i] .
"%', 1, 0)";
$relevance .= " + IF (`brand`.`name` LIKE '%" . $words[$i] . "%',
1, 0)";
}
$query = (new Query())
->select([
'id' => 'product.id',
'name' => 'product.name',
'price' => 'product.price',
'image' => 'product.image',
'hit' => 'product.hit',
'new' => 'product.new',
'sale' => 'product.sale',
'relevance' => $relevance
])
->from('product')
->join('INNER JOIN', 'category', 'category.id =
product.category_id')
->join('INNER JOIN', 'brand', 'brand.id = product.brand_id')
->where(['like', 'product.name', $words[0]])
->orWhere(['like', 'product.keywords', $words[0]])
->orWhere(['like', 'category.name', $words[0]])
->orWhere(['like', 'brand.name', $words[0]]);
for ($i = 1; $i < count($words); $i++) {
$query = $query->orWhere(['like', 'product.name', $words[$i]]);
$query = $query->orWhere(['like', 'product.keywords', $words[$i]]);
$query = $query->orWhere(['like', 'category.name', $words[$i]]);
$query = $query->orWhere(['like', 'brand.name', $words[$i]]);
}
$query = $query->orderBy(['relevance' => SORT_DESC]);

// посмотрим, какой SQL-запрос был сформирован


// print_r($query->createCommand()->getRawSql());

// постраничная навигация
$pages = new Pagination([
'totalCount' => $query->count(),
'pageSize' => Yii::$app->params['pageSize'],
'forcePageParam' => false,
'pageSizeParam' => false
]);
$products = $query
->offset($pages->offset)
->limit($pages->limit)
->all();
// сохраняем полученные данные в кеше
$data = [$products, $pages];
Yii::$app->cache->set($key, $data);
}

return $data;
}
/*.....*/
}

Для поискового запроса «зимняя обувь» будет сформирован SQL-запрос:

SELECT

`product`.`id` AS `id`,
`product`.`name` AS `name`,
`product`.`price` AS `price`,
`product`.`image` AS `image`,
`product`.`hit` AS `hit`,
`product`.`new` AS `new`,
`product`.`sale` AS `sale`,
IF (`product`.`name` LIKE '%зимн%', 3, 0) +
IF (`product`.`keywords` LIKE '%зимн%', 2, 0) +
IF (`category`.`name` LIKE '%зимн%', 1, 0) +
IF (`brand`.`name` LIKE '%зимн%', 1, 0) +
IF (`product`.`name` LIKE '%обув%', 3, 0) +
IF (`product`.`keywords` LIKE '%обув%', 2, 0) +
IF (`category`.`name` LIKE '%обув%', 1, 0) +
IF (`brand`.`name` LIKE '%обув%', 1, 0) AS `relevance`
FROM
`product`
INNER JOIN `category` ON `category`.`id` = `product`.`category_id`
INNER JOIN `brand` ON `brand`.`id` = `product`.`brand_id`
WHERE
`product`.`name` LIKE '%зимн%' OR
`product`.`keywords` LIKE '%зимн%' OR
`category`.`name` LIKE '%зимн%' OR
`brand`.`name` LIKE '%зимн%' OR
`product`.`name` LIKE '%обув%' OR
`product`.`keywords` LIKE '%обув%' OR
`category`.`name` LIKE '%обув%' OR
`brand`.`name` LIKE '%обув%'
ORDER BY
`relevance` DESC

Магазин на Yii2, часть 17. Корзина покупателя, часть


первая
Ну вот, добрались и до корзины. Перед тем, как писать код — несколько слов о том, что будем делать.
Для начала создадим форму для добавления товара в корзину из списка товаров категории, бренда,
результатов поиска. Контроллер будет принимать POST-данные и обращаться к модели, чтобы добавить
товар в корзину. Саму корзину будем хранить в сессии в виде массива.

Итак, создаем класс контроллера BasketController:


<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {


public function actionIndex() {
$basket = (new Basket())->getBasket();
return $this->render('index', ['basket' => $basket]);
}

public function actionAdd() {

$basket = new Basket();

/*
* Данные должны приходить методом POST; если это не
* так — просто показываем корзину
*/
if (!Yii::$app->request->isPost) {
return $this->redirect(['basket/index']);
}

$data = Yii::$app->request->post();
if (!isset($data['id'])) {
return $this->redirect(['basket/index']);
}
if (!isset($data['count'])) {
$data['count'] = 1;
}

// добавляем товар в корзину и перенаправляем покупателя


// на страницу корзины
$basket->addToBasket($data['id'], $data['count']);

return $this->redirect(['basket/index']);
}
}
Создаем класс модели Basket:
<?php
namespace app\models;

use yii\base\Model;
use Yii;

class Basket extends Model {


/**
* Метод добавляет товар в корзину
*/
public function addToBasket($id, $count = 1) {
$count = (int)$count;
if ($count < 1) {
return;
}
$id = abs((int)$id);
$product = Product::findOne($id);
if (empty($product)) {
return;
}
if ($count > 10) {
$count = 10;
}
$session = Yii::$app->session;
$session->open();
if (!$session->has('basket')) {
$session->set('basket', []);
$basket = [];
} else {
$basket = $session->get('basket');
}
if (isset($basket['products'][$product->id])) { // такой товар уже есть?
$count = $basket['products'][$product->id]['count'] + $count;
if ($count > 100) {
$count = 100;
}
$basket['products'][$product->id]['count'] = $count;
} else { // такого товара еще нет
$basket['products'][$product->id]['name'] = $product->name;
$basket['products'][$product->id]['price'] = $product->price;
$basket['products'][$product->id]['count'] = $count;
}
$amount = 0.0;
foreach ($basket['products'] as $item) {
$amount = $amount + $item['price'] * $item['count'];
}
$basket['amount'] = $amount;
$session->set('basket', $basket);
}

/**
* Метод удаляет товар из корзины
*/
public function removeFromBasket($id) {
$id = abs((int)$id);
$session = Yii::$app->session;
$session->open();
if (!$session->has('basket')) {
return;
}
$basket = $session->get('basket');
if (!isset($basket['products'][$id])) {
return;
}
unset($basket['products'][$id]);
if (count($basket['products']) == 0) {
$session->set('basket', []);
return;
}
$amount = 0.0;
foreach ($basket['products'] as $item) {
$amount = $amount + $item['price'] * $item['count'];
}
$basket['amount'] = $amount;

$session->set('basket', $basket);
}

/**
* Метод возвращает содержимое корзины
*/
public function getBasket() {
$session = Yii::$app->session;
$session->open();
if (!$session->has('basket')) {
$session->set('basket', []);
return [];
} else {
return $session->get('basket');
}
}

/**
* Метод удаляет все товары из корзины
*/
public function clearBasket() {
$session = Yii::$app->session;
$session->open();
$session->set('basket', []);
}

Создаем view-шаблон для страницы корзины:

<?php
/*
* Страница корзины покупателя, файл views/basket/index.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
?>
<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<h1>Корзина</h1>
<?php if (!empty($basket)): ?>
<table class="table table-bordered">
<tr>
<th>Наименование</th>
<th>Количество</th>
<th>Цена, руб.</th>
<th>Сумма, руб.</th>
</tr>
<?php foreach ($basket['products'] as $item): ?>
<tr>
<td><?= $item['name']; ?></td>
<td class="text-right"><?= $item['count']; ?></td>
<td class="text-right"><?= $item['price']; ?></td>
<td class="text-right"><?= $item['price'] *
$item['count']; ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td colspan="3" class="text-right">Итого</td>
<td class="text-right"><?= $basket['amount']; ?></td>
</tr>
</table>
<?php else: ?>
<p>Ваша корзина пуста</p>
<?php endif; ?>
</div>
</div>
</div>
</section>
Формат корзины, которую мы храним в сессии (123 и 456 — идентификаторы товаров):
Array (
[products] => Array(
[123] => Array (
[name] => Мужская рубашка
[price] => 1000
[count] => 2
)
[456] => Array(
[name] => Мужская футблока
[price] => 1200
[count] => 1
)
)
[amount] => 3200
)
Кроме всего прочего, изменяем файлы view-шаблонов, где показывается список товаров — добавляем
форму. Например, вот как будет выглядеть файл views/catalog/category.php:
<?php
/*
* Страница раздела каталога, файл views/catalog/category.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use app\components\ChainWidget;
use yii\helpers\Html;
use yii\helpers\Url;
use yii\widgets\LinkPager;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9">
<?= ChainWidget::widget(['itemCurrent' => $category['id'],
'showCurrent' => false]); ?>
<?php if (!empty($products)): ?>
<h2><?= Html::encode($category['name']); ?></h2>
<div class="row">
<?php foreach ($products as $product): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$product['image'],
['alt' => $product['name'], 'class' =>
'img-responsive']
);
?>
<h2><?= $product['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $product['id']]); ?>">
<?= Html::encode($product['name']); ?>
</a>
</p>
<form method="post"
action="<?= Url::to(['basket/add']); ?>">
<input type="hidden" name="id"
value="<?= $product['id']; ?>">
<?=
Html::hiddenInput(
Yii::$app->request->csrfParam,
Yii::$app->request->csrfToken
);
?>
<button type="submit" class="btn btn-
warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</button>
</form>
<?php
if ($product['new']) { // новинка?
echo Html::img(
'@web/images/home/new.png',
['alt' => 'Новинка', 'class' => 'new']
);
}
if ($product['sale']) { // распродажа?
echo Html::img(
'@web/images/home/sale.png',
['alt' => 'Распродажа', 'class' =>
'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>

<?= LinkPager::widget(['pagination' => $pages]); /*


постраничная навигация */ ?>
<?php else: ?>
<p>Нет товаров в этой категории.</p>
<?php endif; ?>
</div>
</div>
</div>
</section>

Обратите внимание на скрытое поле со значением CSRF токена. Это нужно, чтобы не получить ошибку:

Bad Request (#400). Не удалось проверить переданные данные.

На этом все, корзина для списка товаров уже работает.

Магазин на Yii2, часть 18. Корзина покупателя, часть


вторая
Теперь надо изменить форму добавления в корзину на странице товара. Но вот что плохо — после
добавления товара в корзину происходит редирект на страницу корзины. Это не очень удобно, поэтому
будем отправлять POST-запрос с использованием AJAX. И после добавления товара в корзину будем
показывать модальное окно с содержимым корзины. Кроме того, в модальном окне будут две кнопки —
«Продолжить покупки» и «Оформить заказ».

Итак, изменяем форму на странице товара:

<?php
/*
* Страница товара, файл views/catalog/product.php
*/
use app\components\TreeWidget;
use app\components\BrandsWidget;
use app\components\ChainWidget;
use yii\helpers\Url;
use yii\helpers\Html;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<?= ChainWidget::widget(['itemCurrent' =>
$product['category_id']]); ?>
<h1><?= Html::encode($product['name']); ?></h1>
<div class="row">
<div class="col-sm-5">
<div class="product-image">
<?=
Html::img(
'@web/images/products/large/'.$product['image'],
['alt' => $product['name']]
);
?>
</div>
</div>
<div class="col-sm-7">
<div class="product-info">
<p class="product-price">
Цена: <span><?= $product['price']; ?></span> руб.
</p>
<form method="post"
action="<?= Url::to(['basket/add']); ?>"
class="add-to-basket">
<label>Количество</label>
<input name="count" type="text" value="1" />
<input type="hidden" name="id"
value="<?= $product['id']; ?>">
<?=
Html::hiddenInput(
Yii::$app->request->csrfParam,
Yii::$app->request->csrfToken
);
?>
<button type="submit"
class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</button>
</form>
<p>Артикул: 1234567</p>
<p>Наличие: На складе</p>
<p>
Бренд:
<a href="<?= Url::to(['catalog/brand', 'id' =>
$brand['id']]); ?>">
<?= Html::encode($brand['name']); ?>
</a>
</p>

</div>
</div>
</div>
<div class="product-descr">
<?= $product['content']; ?>
</div>
<?php if (!empty($similar)): /* популярные товары */ ?>
<h2>Популярные товары</h2>
<div class="row">
<?php foreach ($similar as $item): ?>
<div class="col-sm-4">
<div class="product-wrapper text-center">
<?=
Html::img(

'@web/images/products/medium/'.$item['image'],
['alt' => $item['name'], 'class' => 'img-
responsive']
);
?>
<h2><?= $item['price']; ?> руб.</h2>
<p>
<a href="<?= Url::to(['catalog/product',
'id' => $item['id']]); ?>">
<?= Html::encode($item['name']); ?>
</a>
</p>
<a href="#" class="btn btn-warning">
<i class="fa fa-shopping-cart"></i>
Добавить в корзину
</a>
<?php
if ($item['new']) { // новинка?
echo Html::img(
'@web/images/home/new.png',
['alt' => 'Новинка', 'class' => 'new']
);
}
if ($item['sale']) { // распродажа?
echo Html::img(
'@web/images/home/sale.png',
['alt' => 'Распродажа', 'class' =>
'sale']
);
}
?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
Отправляем ajax-запрос
Теперь добавим css-класс add-to-basket для всех форм добавления товара в корзину. Он нам
потребуется, когда будем писать js-код отправки ajax-запроса. Для начала все сделаем предельно
просто — при отправке запроса будем отправлять обратно массив содержимого корзины.
<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {

public function actionIndex() {


$basket = (new Basket())->getBasket();
return $this->render('index', ['basket' => $basket]);
}

public function actionAdd() {

$basket = new Basket();

/*
* Данные должны приходить методом POST; если это не
* так — просто показываем корзину
*/
if (!Yii::$app->request->isPost) {
return $this->redirect(['basket/index']);
}

$data = Yii::$app->request->post();
if (!isset($data['id'])) {
return $this->redirect(['basket/index']);
}
if (!isset($data['count'])) {
$data['count'] = 1;
}

// добавляем товар и получаем содержимое корзины


$basket->addToBasket($data['id'], $data['count']);

if (Yii::$app->request->isAjax) { // с использованием AJAX


$content = $basket->getBasket();
return print_r($content, true);
} else { // без использования AJAX
return $this->redirect(['basket/index']);
}
}
}
jQuery(document).ready(function($) {
/*
* Добавление товара в корзину с использованием AJAX
*/
$('.add-to-basket').on('submit', function (event) {
var action = $(this).attr('action');
var method = $(this).attr('method');
if (method == undefined) {
method = 'get';
}
var data = $(this).serialize();
$.ajax({
url: action,
type: method,
data: data,
dataType: 'text',
success: function (response) {
console.dir(response);
},
error: function () {
alert('Произошла ошибка при добавлении товара в корзину');
}
});
event.preventDefault();
});
});

Добавляем модальное окно


Будем использовать компонент Bootstrap «Модальное окно», тем более, что в Yii2 есть готовый виджет.
Открываем на редактирование файл views/layouts/main.php и добавляем перед закрывающим
тегом </body>:
<footer>
<div class="container">
Copyright © 2018 E-SHOPPER Inc. All rights reserved.
</div>
</footer>

<?php
$footer =
<<<FOOTER
<button type="button" class="btn btn-default" data-dismiss="modal">
Продолжить покупки
</button>
<button type="button" class="btn btn-warning">
Оформить заказ
</button>
FOOTER;
Modal::begin([
'header' => '<h2>Корзина</h2>',
'id' => 'basket-modal',
'size'=>'modal-lg',
'footer' => $footer
]);
Modal::end();
unset($footer);
?>

<?php $this->endBody() ?>


</body>
</html>
<?php $this->endPage() ?>

Этот виджет добавит на все страницы следующий html-код:

<div id="basket-modal" class="fade modal" role="dialog" tabindex="-1">


<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-hidden="true">&times;</button>
<h2>Корзина</h2>
</div>
<div class="modal-body">

</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Продолжить покупки
</button>
<button type="button" class="btn btn-primary">
Оформить заказ
</button>
</div>
</div>
</div>
</div>
И нам теперь после получения ответа сервера надо будет вставить внутрь modal-body html-код корзины
и показать модальное окно.

Показываем модальное окно, первый вариант


Давайте изменим в js-коде тип ответа сервера на json и на стороне сервера будем отправлять ответ в
формате json:
jQuery(document).ready(function($) {
/*
* Аккордеон для меню каталога в левой колонке
*/
$('#accordion').dcAccordion({
speed: 'fast'
});

/*
* Добавление товара в корзину с использованием AJAX
*/
$('.add-to-basket').on('submit', function (event) {
var action = $(this).attr('action');
var method = $(this).attr('method');
if (method == undefined) {
method = 'get';
}
var data = $(this).serialize();
$.ajax({
url: action,
type: method,
data: data,
dataType: 'json',
success: function (response) {
/*
* Создаем таблицу товаров корзины, помещаем внутрь
* модального окна, а потом показываем это окно
*/
// создаем таблицу корзины
var table = $('<table>')
.addClass('table')
.addClass('table-bordered')
.appendTo('#basket-modal .modal-body');
var thead = $('<thead>').appendTo(table);
$('<tr>')
.append($('<th>').text('Наименование'))
.append($('<th>').text('Количество'))
.append($('<th>').text('Цена, руб.'))
.append($('<th>').text('Сумма, руб.'))
.appendTo(thead);
var tbody = $('<tbody>').appendTo(table);
var products = response.products;
for (var key in products) {
var cost = Number(products[key].count) *
Number(products[key].price);
$('<tr>')
.append($('<td>').text(products[key].name))
.append($('<td>').text(products[key].count))
.append($('<td>').text(products[key].price))
.append($('<td>').text(cost))
.appendTo(tbody);
}
$('<tr>')
.append($('<td>').attr('colspan', 3).text('Итого'))
.append($('<td>').text(response.amount))
.appendTo(tbody);
// показываем модальное окно
$('#basket-modal').modal();
},
error: function () {
alert('Произошла ошибка при добавлении товара в корзину');
}
});
event.preventDefault();
});
});
<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {

/* ... */

public function actionAdd() {

/* ... */

if (Yii::$app->request->isAjax) { // с использованием AJAX


$content = $basket->getBasket();
return $this->asJson($content);
} else { // без использования AJAX
return $this->redirect(['basket/index']);
}
}
}
{
"products": {
"123": {
"name": "Мужская рубашка",
"price": 1000,
"count": 2
},
"456": {
"name": "Мужская футблока",
"price": 1200,
"count": 1
}
},
"amount": 3200
}

Показываем модальное окно, второй вариант


Мне не нравится формирование таблицы корзины с помощью JavaScript, поэтому давайте перенесем
формирование html-кода на сервер. Для этого изменим в js-коде тип ответа сервера на html и создадим
view-шаблон view/basket/modal.php:
jQuery(document).ready(function($) {
/*
* Аккордеон для меню каталога в левой колонке
*/
$('#accordion').dcAccordion({
speed: 'fast'
});

/*
* Добавление товара в корзину с использованием AJAX
*/
$('.add-to-basket').on('submit', function (event) {
var action = $(this).attr('action');
var method = $(this).attr('method');
if (method == undefined) {
method = 'get';
}
var data = $(this).serialize();
$.ajax({
url: action,
type: method,
data: data,
dataType: 'html',
success: function (response) {
$('#basket-modal .modal-body').html(response);
$('#basket-modal').modal();
},
error: function () {
alert('Произошла ошибка при добавлении товара в корзину');
}
});
event.preventDefault();
});
});
<?php
/*
* Корзина покупателя в модальном окне, файл views/basket/modal.php
*/
use yii\helpers\Html;
use yii\helpers\Url;
?>

<?php if (!empty($basket)): ?>


<div class="table-responsive">
<table class="table table-bordered">
<tr>
<th>Наименование</th>
<th>Количество</th>
<th>Цена, руб.</th>
<th>Сумма, руб.</th>
</tr>
<?php foreach ($basket['products'] as $item): ?>
<tr>
<td><?= $item['name']; ?></td>
<td class="text-right"><?= $item['count']; ?></td>
<td class="text-right"><?= $item['price']; ?></td>
<td class="text-right"><?= $item['price'] * $item['count'];
?></td>
</tr>
<?php endforeach; ?>
<tr>
<td colspan="3" class="text-right">Итого</td>
<td class="text-right"><?= $basket['amount']; ?></td>
</tr>
</table>
</div>
<?php else: ?>
<p>Ваша корзина пуста</p>
<?php endif; ?>
<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {

/* ... */

public function actionAdd() {

/* ... */

if (Yii::$app->request->isAjax) { // с использованием AJAX


// layout-шаблон нам не нужен, только view-шаблон
$this->layout = false;
$content = $basket->getBasket();
return $this->render('modal', ['basket' => $content]);
} else { // без использования AJAX
return $this->redirect(['basket/index']);
}
}
}

Магазин на Yii2, часть 19. Корзина покупателя, часть


третья
Наша корзина сейчас довольно примитивная — не хватает возможности удалить товар и обновить
содержимое. Давайте это исправим и начнем с обновления view-шаблонов страницы корзины и
содержимого модального окна. Добавим ссылки для удаления товаров из корзины и форму — чтобы
можно было изменить количество.

Итак, вносим изменения в view-шаблон страницы корзины:

<?php
/*
* Страница корзины покупателя, файл views/basket/index.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\helpers\Html;
use yii\helpers\Url;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<h1>Корзина</h1>
<div id="basket-content">
<?php if (!empty($basket)): ?>
<p class="text-right">
<a href="<?= Url::to(['basket/clear']); ?>"
class="text-danger">
Очистить корзину
</a>
</p>
<div class="table-responsive">
<form action="<?= Url::to(['basket/update']); ?>"
method="post">
<?=
Html::hiddenInput(
Yii::$app->request->csrfParam,
Yii::$app->request->csrfToken
);
?>
<table class="table table-bordered">
<tr>
<th>Наименование</th>
<th>Кол-во, шт.</th>
<th>Цена, руб.</th>
<th>Сумма, руб.</th>
<th></th>
</tr>
<?php foreach ($basket['products'] as $id =>
$item): ?>
<tr>
<td>
<a href="<?=
Url::to(['catalog/product', 'id' => $id]); ?>">
<?=
Html::encode($item['name']); ?>
</a>
</td>
<td class="text-right">
<?=
Html::input(
'text',
'count['.$id.']',
$item['count'],
['style' => 'width: 100%; text-
align: right;']
);
?>
</td>
<td class="text-right"><?=
$item['price']; ?></td>
<td class="text-right"><?=
$item['price'] * $item['count']; ?></td>
<td>
<a href="<?=
Url::to(['basket/remove', 'id' => $id]); ?>" class="text-danger">
<i class="fa fa-times" aria-
hidden="true"></i>
</a>
</td>
</tr>
<?php endforeach; ?>
<tr>
<td>
<button type="submit"
class="btn btn-primary">
<i class="fa fa-refresh" aria-
hidden="true"></i>
Пересчитать
</button>
</td>
<td colspan="2" class="text-
right">Итого</td>
<td class="text-right"><?=
$basket['amount']; ?></td>
<td></td>
</tr>
</table>
</form>
</div>
<?php else: ?>
<p>Ваша корзина пуста</p>
<?php endif; ?>
</div>
<?php if (!empty($basket)): ?>
<a href="<?= Url::to(['order/checkout']); ?>"
class="btn btn-warning pull-right">
Оформить заказ
</a>
<?php endif; ?>
</div>
</div>
</div>
</section>

Изменяем view-шаблон корзины в модальном окне:

<?php
/*
* Корзина покупателя в модальном окне, файл views/basket/modal.php
*/

use yii\helpers\Html;
use yii\helpers\Url;
?>

<?php if (!empty($basket)): ?>


<p class="text-right clear">
<a href="<?= Url::to(['basket/clear']); ?>" class="text-danger">
Очистить корзину
</a>
</p>
<div class="table-responsive">
<form action="<?= Url::to(['basket/update']); ?>" method="post">
<?=
Html::hiddenInput(
Yii::$app->request->csrfParam,
Yii::$app->request->csrfToken
);
?>
<table class="table table-bordered">
<tr>
<th>Наименование</th>
<th>Кол-во, шт.</th>
<th>Цена, руб.</th>
<th>Сумма, руб.</th>
<th></th>
</tr>
<?php foreach ($basket['products'] as $id => $item): ?>
<tr>
<td>
<a href="<?= Url::to(['catalog/product', 'id' => $id]);
?>">
<?= Html::encode($item['name']); ?>
</a>
</td>
<td class="text-right">
<?=
Html::input(
'text',
'count['.$id.']',
$item['count'],
['style' => 'width: 100%; text-align: right;']
);
?>
</td>
<td class="text-right"><?= $item['price']; ?></td>
<td class="text-right"><?= $item['price'] * $item['count'];
?></td>
<td>
<a href="<?= Url::to(['basket/remove', 'id' => $id]);
?>"
class="text-danger">
<i class="fa fa-times" aria-hidden="true"></i>
</a>
</td>
</tr>
<?php endforeach; ?>
<tr>
<td>
<button type="submit"
class="btn btn-primary">
<i class="fa fa-refresh" aria-hidden="true"></i>
Пересчитать
</button>
</td>
<td colspan="2" class="text-right">Итого</td>
<td class="text-right"><?= $basket['amount']; ?></td>
<td></td>
</tr>
</table>
</form>
</div>
<?php else: ?>
<p>Ваша корзина пуста</p>
<?php endif; ?>

Удаление товара из корзины


Добавляем новый метод в контроллер BasketController:
<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {

public function actionIndex() {


/* ... */
}

public function actionAdd() {


/* ... */
}

public function actionRemove($id) {


$basket = new Basket();
$basket->removeFromBasket($id);
return $this->redirect(['basket/index']);
}

Теперь удаление уже работает, но не слишком красиво. При удалении товара из корзины в модальном
окне присходит переход на страницу корзины. Давайте это исправим, будем перехватывать событие
клика по ссылке и отправлять GET-запрос с использованием AJAX:

jQuery(document).ready(function($) {
/*
* Добавление товара в корзину с использованием AJAX
*/
$('.add-to-basket').on('submit', function (event) {
/* ... */
});
/*
* Удаление товара из корзины в модальном окне
*/
$('#basket-modal .modal-body').on('click', 'table a.text-danger', function
(event) {
var href = $(this).attr('href');
$('#basket-modal .modal-body').load(href, function () {
// если корзина пуста, скрываем кнопку «Оформить заказ»
if ( ! $('#basket-modal .modal-body table').length) {
$('#basket-modal .modal-footer .btn-warning').hide();
}
});

event.preventDefault();
});
/*
* Удаление товара из корзины на странице корзины
*/
$('#basket-content').on('click', 'table a.text-danger', function (event) {
var href = $(this).attr('href');
$('#basket-content').load(href, function () {
// если корзина пуста, скрываем кнопку «Оформить заказ»
if ( ! $(this).find('table').length) {
$('#basket-content').next('.btn-warning').hide();
}
});
event.preventDefault();
});
});
Соответственно, изменим метод контроллера actionRemove(), чтобы он умел обрабатывать AJAX-
запросы:
<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {

public function actionIndex() {


/* ... */
}

public function actionAdd() {


/* ... */
}

public function actionRemove($id) {


$basket = new Basket();
$basket->removeFromBasket($id);
/*
* Тут возможны две ситуации: пришел просто GET-запрос
* или GET-запрос с использованием XmlHttpRequest
*/
if (Yii::$app->request->isAjax) { // с использованием AJAX
// layout-шаблон нам не нужен, только view-шаблон
$this->layout = false;
$content = $basket->getBasket();
return $this->render('modal', ['basket' => $content]);
} else { // без использования AJAX
return $this->redirect(['basket/index']);
}
}

Удаление всех товаров из корзины


Добавляем новый метод в контроллер BasketController:
<?php
namespace app\controllers;
use app\models\Basket;
use Yii;

class BasketController extends AppController {

public function actionIndex() {


/* ... */
}

public function actionAdd() {


/* ... */
}

public function actionRemove($id) {


/* ... */
}

public function actionClear() {


$basket = new Basket();
$basket->clearBasket();
return $this->redirect(['basket/index']);
}

По аналогии с удалением товара из корзины, будем отлавливать событие клика по ссылке «Очистить
корзину» и отправлять GET-запрос с использованием AJAX:

jQuery(document).ready(function($) {
/*
* Добавление товара в корзину с использованием AJAX
*/
$('.add-to-basket').on('submit', function (event) {
/* ... */
});
/*
* Удаление товара из корзины в модальном окне
*/
$('#basket-modal .modal-body').on('click', 'table a.text-danger', function
(event) {
/* ... */
});
/*
* Удаление товара из корзины на странице корзины
*/
$('#basket-content').on('click', 'table a.text-danger', function (event) {
/* ... */
});
/*
* Удаление всех товаров из корзины в модальном окне
*/
$('#basket-modal .modal-body').on('click', 'p a.text-danger', function (event)
{
var href = $(this).attr('href');
$('#basket-modal .modal-body').load(href);
// корзина пуста, скрываем кнопку «Оформить заказ»
$('#basket-modal .modal-footer .btn-warning').hide();
event.preventDefault();
});
/*
* Удаление всех товаров из корзины на странице корзины
*/
$('#basket-content').on('click', 'p a.text-danger', function (event) {
var href = $(this).attr('href');
$('#basket-content').load(href);
// корзина пуста, скрываем кнопку «Оформить заказ»
$('#basket-content').next('.btn-warning').hide();
event.preventDefault();
});
});
Вносим изменения в метод контроллера actionClear(), чтобы он умел обрабатывать AJAX-запросы:
<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {

public function actionIndex() {


/* ... */
}

public function actionAdd() {


/* ... */
}

public function actionRemove($id) {


/* ... */
}

public function actionClear() {


$basket = new Basket();
$basket->clearBasket();
/*
* Тут возможны две ситуации: пришел просто GET-запрос
* или GET-запрос с использованием XmlHttpRequest
*/
if (Yii::$app->request->isAjax) { // с использованием AJAX
// layout-шаблон нам не нужен, только view-шаблон
$this->layout = false;
$content = $basket->getBasket();
return $this->render('modal', ['basket' => $content]);
} else { // без использования AJAX
return $this->redirect(['basket/index']);
}
}

Изменение количества товаров


При нажатии на кнопку «Пересчитать» на сервер будет отправлен POST-запрос, содержащий
идентификаторы всех товаров в корзине и их количество:

Array

[_csrf] => .....

[count] => Array

[123] => 1
[456] => 2

)
Мы принимаем данные в методе actionUpdate() контроллера и вызываем
метод updateBasket() модели:
<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {

public function actionIndex() {


/* ... */
}

public function actionAdd() {


/* ... */
}

public function actionRemove($id) {


/* ... */
}

public function actionClear() {


/*...*/
}

public function actionUpdate() {


$basket = new Basket();

/*
* Данные должны приходить методом POST; если это не
* так — просто показываем корзину
*/
if (!Yii::$app->request->isPost) {
return $this->redirect(['basket/index']);
}

$data = Yii::$app->request->post();
if (!isset($data['count'])) {
return $this->redirect(['basket/index']);
}
if (!is_array($data['count'])) {
return $this->redirect(['basket/index']);
}

$basket->updateBasket($data);

return $this->redirect(['basket/index']);
}

}
<?php
namespace app\models;

use yii\base\Model;
use Yii;

class Basket extends Model {


/**
* Метод добавляет товар в корзину
*/
public function addToBasket($id, $count = 1) {
/* ... */
}

/**
* Метод удаляет товар из корзины
*/
public function removeFromBasket($id) {
/* ... */
}

/**
* Метод возвращает содержимое корзины
*/
public function getBasket() {
/* ... */
}

/**
* Метод удаляет все товары из корзины
*/
public function clearBasket() {
/* ... */
}

/**
* Метод обновляет содержимое корзины
*/
public function updateBasket($data) {
$this->clearBasket();
foreach ($data['count'] as $id => $count) {
$this->addToBasket($id, $count);
}
}
}

Обновление теперь работает, но не слишком красиво. Здесь та же проблема, как и при удалении товаров
из корзины. После отправки формы из модального окна происходит переход на страницу корзину. Но мы
уже знаем, что с этим делать — надо отправлять данные формы с использованием AJAX:

jQuery(document).ready(function($) {
/*
* Добавление товара в корзину с использованием AJAX
*/
$('.add-to-basket').on('submit', function (event) {
/* ... */
});
/*
* Удаление товара из корзины в модальном окне
*/
$('#basket-modal .modal-body').on('click', 'table a.text-danger', function
(event) {
/* ... */
});
/*
* Удаление товара из корзины на странице корзины
*/
$('#basket-content').on('click', 'table a.text-danger', function (event) {
/* ... */
});
/*
* Удаление всех товаров из корзины в модальном окне
*/
$('#basket-modal .modal-body').on('click', 'p a.text-danger', function (event)
{
/* ... */
});
/*
* Удаление всех товаров из корзины на странице корзины
*/
$('#basket-content').on('click', 'p a.text-danger', function (event) {
/* ... */
});
/*
* Обновление содержимого корзины в модальном окне
*/
$('#basket-modal').on('submit', 'form', function (event) {
var action = $(this).attr('action');
var method = $(this).attr('method');
if (method == undefined) {
method = 'get';
}
var data = $(this).serialize();
$.ajax({
url: action,
type: method,
data: data,
dataType: 'html',
success: function (response) {
$('#basket-modal .modal-body').html(response);
// если корзина пуста, скрываем кнопку «Оформить заказ»
if ( ! $('#basket-modal .modal-body table').length) {
$('#basket-modal .modal-footer .btn-warning').hide();
}
},
error: function () {
alert('Произошла ошибка при обновлении корзины');
}
});
event.preventDefault();
})
/*
* Обновление содержимого корзины на странице корзины
*/
$('#basket-content').on('submit', 'form', function (event) {
var action = $(this).attr('action');
var method = $(this).attr('method');
if (method == undefined) {
method = 'get';
}
var data = $(this).serialize();
$.ajax({
url: action,
type: method,
data: data,
dataType: 'html',
success: function (response) {
$('#basket-content').html(response);
// если корзина пуста, скрываем кнопку «Оформить заказ»
if ( ! $('#basket-content table').length) {
$('#basket-content').next('.btn-warning').hide();
}
},
error: function () {
alert('Произошла ошибка при обновлении корзины');
}
});
event.preventDefault();
});
});
И вносим изменения в метод контроллера actionUpdate(), чтобы он умел обрабатывать AJAX-запросы:
<?php
namespace app\controllers;

use app\models\Basket;
use Yii;

class BasketController extends AppController {

public function actionIndex() {


/* ... */
}

public function actionAdd() {


/* ... */
}

public function actionRemove($id) {


/* ... */
}

public function actionClear() {


/*...*/
}

public function actionUpdate() {


$basket = new Basket();

/*
* Данные должны приходить методом POST; если это не
* так — просто показываем корзину
*/
if (!Yii::$app->request->isPost) {
return $this->redirect(['basket/index']);
}

$data = Yii::$app->request->post();
if (!isset($data['count'])) {
return $this->redirect(['basket/index']);
}
if (!is_array($data['count'])) {
return $this->redirect(['basket/index']);
}

$basket->updateBasket($data);

/*
* Тут возможны две ситуации: пришли просто данные POST или
* пришли данные POST с использованием XmlHttpRequest
*/
if (Yii::$app->request->isAjax) { // с использованием AJAX
// layout-шаблон нам не нужен, только view-шаблон
$this->layout = false;
$content = $basket->getBasket();
return $this->render('modal', ['basket' => $content]);
} else { // без использования AJAX
return $this->redirect(['basket/index']);
}
}

Магазин на Yii2, часть 20. Оформление заказа, часть


первая
Итак, корзина готова, можно приступать к оформлению заказа. Для хранения заказов создадим две
таблицы в базе данных. Одну — для хранения всех заказов в магазине, другую — для хранения состава
каждого заказа. Для каждой таблицы создадим модель, добавим класс контроллера с единственным
действием и представление для этого действия.

-- Структура таблицы `order`


CREATE TABLE `order` (
`id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Идентификатор
заказа',
`user_id` int(11) NOT NULL DEFAULT '0' COMMENT 'Идентификатор пользователя',
`name` varchar(50) NOT NULL DEFAULT '' COMMENT 'Имя и фамилия покупателя',
`email` varchar(50) NOT NULL DEFAULT '' COMMENT 'Почта покупателя',
`phone` varchar(50) NOT NULL DEFAULT '' COMMENT 'Телефон покупателя',
`address` varchar(255) NOT NULL DEFAULT '' COMMENT 'Адрес доставки',
`comment` varchar(255) NOT NULL DEFAULT '' COMMENT 'Комментарий к заказу',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT 'Сумма заказа',
`status` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Статус заказа',
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Дата и время
создания',
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP COMMENT 'Дата и время обновления'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Структура таблицы `order_item`
CREATE TABLE `order_item` (
`id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Идентификатор
элемента',
`order_id` int(10) UNSIGNED NOT NULL COMMENT 'Идентификатор заказа',
`product_id` int(10) UNSIGNED NOT NULL COMMENT 'Идентификатор товара',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT 'Наименование товара',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT 'Цена товара',
`quantity` smallint(5) UNSIGNED NOT NULL DEFAULT '1' COMMENT 'Количество в
заказе',
`cost` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT 'Стоимость = Цена * Кол-во'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Таблицы созданы, создаем классы моделей для них:

<?php
namespace app\models;

use Yii;
use yii\db\ActiveRecord;
use yii\behaviors\TimestampBehavior;

class Order extends ActiveRecord {

/**
* Метод возвращает имя таблицы БД
*/
public static function tableName() {
return 'order';
}
/**
* Позволяет получить все товары этого заказа
*/
public function getItems() {
// связь таблицы БД `order` с таблицей `order_item`
return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
}

/**
* Правила валидации атрибутов класса при сохранении
*/
public function rules()
{
return [
// эти четыре поля обязательны для заполнения
[['name', 'email', 'phone', 'address'], 'required'],
// поле email должно быть корректным адресом почты
['email', 'email'],
// поле phone должно совпадать с шаблоном +7 (495) 123-45-67
[
'phone',
'match',
'pattern' => '~^\+7\s\([0-9]{3}\)\s[0-9]{3}-[0-9]{2}-[0-9]{2}$~',
'message' => 'Номер телефона должен соответствовать шаблону +7
(495) 123-45-67'
],
// эти три строки должны быть не более 50 символов
[['name', 'email', 'phone'], 'string', 'max' => 50],
// эти две строки должны быть не более 255 символов
[['address', 'comment'], 'string', 'max' => 255],
];
}

public function attributeLabels() {


return [
'name' => 'Ваше имя',
'email' => 'Адрес почты',
'phone' => 'Номер телефона',
'address' => 'Адрес доставки',
'comment' => 'Комментарий к заказу',
];
}
}
<?php
namespace app\models;

use Yii;
use yii\db\ActiveRecord;

class OrderItem extends ActiveRecord {

/**
* Возвращает имя таблицы БД
*/
public static function tableName() {
return 'order_item';
}

/**
* Позволяет получить заказ, в который входит этот элемент
*/
public function getOrder() {
// связь таблицы БД `order_item` с таблицей `order`
return $this->hasOne(Order::class, ['id' => 'order_id']);
}
}

Добавляем класс контроллера:

<?php
namespace app\controllers;

use app\models\Order;
use app\models\OrderItem;

class OrderController extends AppController {


public $defaultAction = 'checkout';

public function actionCheckout() {


$this->setMetaTags('Оформление заказа');
$order = new Order();
return $this->render('checkout', ['order' => $order]);
}
}

И файл view-шаблона:

<?php
/*
* Страница оформления заказа, файл views/order/checkout.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\widgets\ActiveForm;
use yii\helpers\Html;
use yii\helpers\Url;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9">
<h1>Оформление заказа</h1>
<div id="checkout">
<?php
$form = ActiveForm::begin(
['id' => 'checkout-form', 'class' => 'form-horizontal']
);
?>
<?= $form->field($order, 'name')->textInput(); ?>
<?= $form->field($order, 'email')->input('email'); ?>
<?= $form->field($order, 'phone')->textInput(); ?>
<?= $form->field($order, 'address')->textarea(['rows' => 2]);
?>
<?= $form->field($order, 'comment')->textarea(['rows' => 2]);
?>
<?= Html::submitButton('Отправить', ['class' => 'btn btn-
primary']); ?>
<?php ActiveForm::end(); ?>
</div>
</div>
</div>
</div>
</section>
Вот как теперь выглядит страница http://www.server.com/order/checkout:

Магазин на Yii2, часть 21. Оформление заказа, часть


вторая
Хорошо, форма для оформления заказа готова, правила для валидации полей заказа заданы. Осталось
только сохранить в таблицу БД order введенные пользователем данные. Поскольку у нас
поля created и updated должны сохранять текущую дату и время, добавим метод behaviors() для
класса Order.
<?php
namespace app\models;

use Yii;
use yii\db\ActiveRecord;
use yii\behaviors\TimestampBehavior;
use yii\db\Expression;

class Order extends ActiveRecord {

/*...*/

/**
* Метод расширяет возможности класса Order, внедряя дополительные
* свойства и методы. Кроме того, позволяет реагировать на события,
* создаваемые классом Order или его родителями
*/
public function behaviors()
{
return [
[
'class' => TimestampBehavior::class,
'attributes' => [
// при вставке новой записи присвоить атрибутам created
// и updated значение метки времени UNIX
ActiveRecord::EVENT_BEFORE_INSERT => ['created', 'updated'],
// при обновлении существующей записи присвоить атрибуту
// updated значение метки времени UNIX
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated'],
],
// если вместо метки времени UNIX используется DATETIME
'value' => new Expression('NOW()'),
],
];
}

/*...*/

}
Вообще, в методе behaviors() нет необходимости — при создании таблицы БД order мы указали, какие
значения устанавливать для полей created и updated при вставке новой записи и при обновлении
существующей. Но хотелось продемонстрировать такую возможность фреймворка.

Теперь займемся контроллером. При отправке данных формы оформления заказа, мы должны загрузить
их в модель и проверить с помощью заданных ранее правил валидации. Если данные прошли
валидацию — сохраняем их в базу данных. Если при валидации были обнаружены ошибки — сохраняем
в сессию введенные пользователем данные и массив сообщений об ошибках.

<?php
namespace app\controllers;

use Yii;
use app\models\Basket;
use app\models\Order;

class OrderController extends AppController {


public $defaultAction = 'checkout';

public function actionCheckout() {


$this->setMetaTags('Оформление заказа');
$order = new Order();
/*
* Если пришли post-данные, загружаем их в модель...
*/
if ($order->load(Yii::$app->request->post())) {
// ...и проверяем эти данные
if ( ! $order->validate()) {
// данные не прошли валидацию, отмечаем этот факт
Yii::$app->session->setFlash(
'checkout-success',
false
);
// сохраняем в сессии введенные пользователем данные
Yii::$app->session->setFlash(
'checkout-data',
[
'name' => $order->name,
'email' => $order->email,
'phone' => $order->phone,
'address' => $order->address,
'comment' => $order->comment
]
);
/*
* Сохраняем в сессии массив сообщений об ошибках. Массив имеет вид
* [
* 'name' => [
* 'Поле «Ваше имя» обязательно для заполнения',
* ],
* 'email' => [
* 'Поле «Ваш email» обязательно для заполнения',
* 'Поле «Ваш email» должно быть адресом почты'
* ]
* ]
*/
Yii::$app->session->setFlash(
'checkout-errors',
$order->getErrors()
);
} else {
/*
* Заполняем остальные поля модели — те которые приходят
* не из формы, а которые надо получить из корзины. Кроме
* того, поля created и updated будут заполнены с помощью
* метода Order::behaviors().
*/
$basket = new Basket();
$content = $basket->getBasket();
$order->amount = $content['amount'];
// сохраняем заказ в базу данных
$order->insert();
$order->addItems($content);
// очищаем содержимое корзины
$basket->clearBasket();
// данные прошли валидацию, заказ успешно сохранен
Yii::$app->session->setFlash(
'checkout-success',
true
);
}
// выполняем редирект, чтобы избежать повторной отправки формы
return $this->refresh();
}
return $this->render('checkout', ['order' => $order]);
}
}

В view-шаблоне формы оформления заказа мы проверяем — были отправлены данные формы? Если да
— проверяем, прошли ли данные валидацию? Если данные прошли валидацию — показываем
сообщение об успешном оформлении заказа. Если нет — выводим сообщения об ошибках, опять
показываем форму и заполняем ее введенными ранее данными.

<?php
/*
* Страница оформления заказа, файл views/order/checkout.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
use yii\widgets\ActiveForm;
use yii\helpers\Html;
use yii\helpers\Url;
/*
* Если данные формы не прошли валидацию, получаем из сессии сохраненные
* данные, чтобы заполнить ими поля формы, не заставляя пользователя
* заполнять форму повторно
*/
$name = '';
$email = '';
$phone = '';
$address = '';
$comment = '';
if (Yii::$app->session->hasFlash('checkout-data')) {
$data = Yii::$app->session->getFlash('checkout-data');
$name = Html::encode($data['name']);
$email = Html::encode($data['email']);
$phone = Html::encode($data['phone']);
$address = Html::encode($data['address']);
$comment = Html::encode($data['comment']);
}
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="left-sidebar">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>
</div>

<div class="col-sm-9">
<h1>Оформление заказа</h1>
<div id="checkout">
<?php
$success = false;
if (Yii::$app->session->hasFlash('checkout-success')) {
$success = Yii::$app->session->getFlash('checkout-
success');
}
?>

<?php if (!$success): ?>


<?php if (Yii::$app->session->hasFlash('checkout-errors')):
?>
<div class="alert alert-warning alert-dismissible"
role="alert">
<button type="button" class="close"
data-dismiss="alert" aria-label="Закрыть">
<span aria-hidden="true">&times;</span>
</button>
<p>При заполнении формы допущены ошибки</p>
<?php $allErrors = Yii::$app->session-
>getFlash('checkout-errors'); ?>
<ul>
<?php foreach ($allErrors as $errors): ?>
<?php foreach ($errors as $error): ?>
<li><?= $error; ?></li>
<?php endforeach; ?>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>

<?php
$form = ActiveForm::begin(
['id' => 'checkout-form', 'class' => 'form-
horizontal']
);
echo $form->field($order, 'name')
->textInput(['value' => $name]);
echo $form->field($order, 'email')
->input('email', ['value' => $email]);
echo $form->field($order, 'phone')
->textInput(['value' => $phone]);
echo $form->field($order, 'address')
->textarea(['rows' => 2, 'value' => $address]);
echo $form->field($order, 'comment')
->textarea(['rows' => 2, 'value' => $comment]);
echo Html::submitButton(
'Оформить заказ',
['class' => 'btn btn-primary']
);
ActiveForm::end();
?>
<?php else: ?>
<p>Ваш заказ успешно оформлен, спасибо за покупку.</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
</section>

В классе модели Order нам еще потребуется метод addItems(), который мы вызываем из контроллера.
<?php
namespace app\models;

use Yii;
use yii\db\ActiveRecord;
use yii\behaviors\TimestampBehavior;
use yii\db\Expression;

class Order extends ActiveRecord {

/*...*/

/**
* Добавляет записи в таблицу БД `order_item`
*/
public function addItems($basket) {
// получаем товары в корзине
$products = $basket['products'];
// добавляем товары по одному
foreach ($products as $product_id => $product) {
$item = new OrderItem();
$item->order_id = $this->id;
$item->product_id = $product_id;
$item->name = $product['name'];
$item->price = $product['price'];
$item->quantity = $product['count'];
$item->cost = $product['price'] * $product['count'];
$item->insert();
}
}
}

Магазин на Yii2, часть 22. Оформление заказа, часть


третья
Осталось только отправить письмо покупателю о заказе. Для отправки почты Yii2 предлагает
расширение swiftmailer, которое нужно просто настроить. Настраивается оно в
файле config/web.php. Обратите внимание на настройку useFileTransport: когда она имеет
значение true — письмо не отправляется реально, что нам и нужно.
$config = [
/*...*/
'components' => [
/*...*/
'mailer' => [
'class' => 'yii\swiftmailer\Mailer',
// send all mails to a file by default. You have to set
// 'useFileTransport' to false and configure a transport
// for the mailer to send real emails.
'useFileTransport' => true,
'htmlLayout' => 'layouts/html',
],
/*...*/
],
/*...*/
];
Давайте подготовим шаблон письма, для этого создадим файл order.php в директории mail:
<?php
use yii\helpers\Html;
$this->title = 'Заказ в магазине № ' . $order->id;
?>

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


<ul>
<li>Покупатель: <?= Html::encode($order->name); ?></li>
<li>E-mail: <?= Html::encode($order->email); ?></li>
<li>Телефон: <?= Html::encode($order->phone); ?></li>
</ul>

<table border="1" cellpadding="3" cellspacing="0">


<tr>
<th align="left">Наименование</th>
<th align="left">Кол-во, шт</th>
<th align="left">Цена, руб.</th>
<th align="left">Сумма, руб.</th>
</tr>
<?php foreach ($order->items as $product): ?>
<tr>
<td align="left"><?= Html::encode($product['name']); ?></td>
<td align="right"><?= $product['quantity']; ?></td>
<td align="right"><?= $product['price']; ?></td>
<td align="right"><?= $product['cost']; ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td colspan="3" align="right">Итого</td>
<td align="right"><?= $order['amount']; ?></td>
</tr>
</table>

<p>Адрес доставки: <?= Html::encode($order->address); ?></p>

<p>Комментарий: <?= Html::encode($order->comment); ?></p>

Теперь добавим в контроллер код отправки письма:

<?php
namespace app\controllers;

use Yii;
use app\models\Basket;
use app\models\Order;

class OrderController extends AppController {


public $defaultAction = 'checkout';

public function actionCheckout() {


// если в корзине нет товаров, здесь делать нечего
if (empty((new Basket())->getBasket())) {
return $this->redirect(['basket/index']);
}
$this->setMetaTags('Оформление заказа');
$order = new Order();
/*
* Если пришли post-данные, загружаем их в модель...
*/
if ($order->load(Yii::$app->request->post())) {
// ...и проверяем эти данные
if ( ! $order->validate()) {
// данные не прошли валидацию, отмечаем этот факт
Yii::$app->session->setFlash(
'checkout-success',
false
);
// сохраняем в сессии введенные пользователем данные
Yii::$app->session->setFlash(
'checkout-data',
[
'name' => $order->name,
'email' => $order->email,
'phone' => $order->phone,
'address' => $order->address,
'comment' => $order->comment
]
);
/*
* Сохраняем в сессии массив сообщений об ошибках. Массив имеет вид
* [
* 'name' => [
* 'Поле «Ваше имя» обязательно для заполнения',
* ],
* 'email' => [
* 'Поле «Ваш email» обязательно для заполнения',
* 'Поле «Ваш email» должно быть адресом почты'
* ]
* ]
*/
Yii::$app->session->setFlash(
'checkout-errors',
$order->getErrors()
);
} else {
/*
* Заполняем остальные поля модели — те которые приходят
* не из формы, а которые надо получить из корзины. Кроме
* того, поля created и updated будут заполнены с помощью
* метода Order::behaviors().
*/
$basket = new Basket();
$content = $basket->getBasket();
$order->amount = $content['amount'];
// сохраняем заказ в базу данных
$order->insert();
$order->addItems($content);
// отправляем письмо покупателю
$mail = Yii::$app->mailer->compose(
'order',
['order' => $order]
);
$mail->setFrom(Yii::$app->params['senderEmail'])
->setTo($order->email)
->setSubject('Заказ в магазине № ' . $order->id)
->send();
// очищаем содержимое корзины
$basket->clearBasket();
// данные прошли валидацию, заказ успешно сохранен
Yii::$app->session->setFlash(
'checkout-success',
true
);
}
// выполняем редирект, чтобы избежать повторной отправки формы
return $this->refresh();
}
return $this->render('checkout', ['order' => $order]);
}
}
Само письмо будет таким, его можно найти в директории runtime/mail:
Message-ID: <c0bdfc0f1823d566ec47f3b237295bb7@www.example.com>
Date: Sat, 17 Aug 2019 10:28:47 +0300
Subject: =?utf-8?Q?=D0=97=D0=B0=D0=BA=D0=B0=D0=B7_=D0=B2_?=
=?utf-8?Q?=D0=BC=D0=B0=D0=B3=D0=B0=D0=B7=D0=B8=D0=BD=D0=B5_=E2=84=96?= 7
From: noreply@example.com
To: ivanov@mail.ru
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="_=_swift_1566026927_e97faf701339ec540abb7bfef2e0f908_=_"

--_=_swift_1566026927_e97faf701339ec540abb7bfef2e0f908_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

=D0=97=D0=B0=D0=BA=D0=B0=D0=B7 =D0=B2 =D0=BC=D0=B0=D0=B3=D0=B0=D0=B7=D0=


=B8=D0=BD=D0=B5 =E2=84=96 7

=D0=9F=D0=BE=D0=BA=D1=83=D0=BF=D0=B0=D1=
=82=D0=B5=D0=BB=D1=8C: =D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9 =D0=98=D0=B2=
=D0=B0=D0=BD=D0=BE=D0=B2
E-mail: ivanov@mail.ru
=D0=A2=D0=B5=D0=BB=D0=B5=D1=84=D0=BE=D0=BD: +7 (926) 765-43-21

=D0=
=9D=D0=B0=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5
=D0=9A=D0=BE=D0=BB-=D0=B2=D0=BE, =D1=88=D1=82
=D0=A6=D0=B5=D0=BD=D0=B0, =D1=80=D1=83=D0=B1.
=D0=A1=D1=83=D0=BC=D0=BC=D0=B0, =D1=80=D1=83=D0=B1.

=D0=9C=D1=83=D0=
=B6=D1=81=D0=BA=D0=B0=D1=8F =D0=BB=D0=B5=D1=82=D0=BD=D1=8F=D1=8F =D0=BE=
=D0=B4=D0=B5=D0=B6=D0=B4=D0=B0 1
2
1000.23
2000.46

=D0=9C=D1=83=D0=B6=D1=81=D0=BA=D0=B0=D1=8F =D0=BB=D0=B5=D1=
=82=D0=BD=D1=8F=D1=8F =D0=BE=D0=B4=D0=B5=D0=B6=D0=B4=D0=B0 2
3
56.00
168.00

=D0=98=D1=82=D0=BE=D0=B3=D0=BE
2168.46

=D0=90=D0=B4=D1=80=D0=B5=D1=81 =D0=B4=D0=BE=D1=81=D1=82=D0=
=B0=D0=B2=D0=BA=D0=B8: =D0=9C=D0=BE=D1=81=D0=BA=D0=B2=D0=B0, =D0=A4=D0=
=BB=D0=BE=D1=82=D1=81=D0=BA=D0=B0=D1=8F =D1=83=D0=BB=D0=B8=D1=86=D0=B0, =
=D0=B4=D0=BE=D0=BC 12, =D0=BA=D0=B2.72

=D0=9A=D0=BE=D0=BC=D0=BC=D0=
=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B9: 2-=D0=BE=D0=B9 =D0=BF=D0=BE=D0=
=B4=D1=8A=D0=B5=D0=B7=D0=B4, 4-=D1=8B=D0=B9 =D1=8D=D1=82=D0=B0=D0=B6

--_=_swift_1566026927_e97faf701339ec540abb7bfef2e0f908_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org=


/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns=3D"http://www.w3.org/1999/=
xhtml">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/ht=
ml; charset=3DUTF-8" />
<title>=D0=97=D0=B0=D0=BA=D0=B0=D0=B7 =D0=
=B2 =D0=BC=D0=B0=D0=B3=D0=B0=D0=B7=D0=B8=D0=BD=D0=B5 =E2=84=96 7</title>
=
</head>
<body>
=20
<h1>=D0=97=D0=B0=D0=BA=D0=B0=D0=B7 =D0=B2 =D0=BC=D0=B0=D0=B3=D0=B0=D0=B7=
=D0=B8=D0=BD=D0=B5 =E2=84=96 7</h1>

<ul>
<li>=D0=9F=D0=BE=D0=BA=D1=83=D0=BF=D0=B0=D1=82=D0=B5=D0=BB=D1=8C: =
=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9 =D0=98=D0=B2=D0=B0=D0=BD=D0=BE=D0=
=B2</li>
<li>E-mail: ivanov@mail.ru</li>
<li>=D0=A2=D0=B5=D0=BB=D0=B5=D1=84=D0=BE=D0=BD: +7 (926) 765-43-21</li>
</ul>

<table border=3D"1" cellpadding=3D"3" cellspacing=3D"0">


<tr>
<th align=3D"left">=D0=9D=D0=B0=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=
=B2=D0=B0=D0=BD=D0=B8=D0=B5</th>
<th align=3D"left">=D0=9A=D0=BE=D0=BB-=D0=B2=D0=BE, =D1=88=D1=82</t=
h>
<th align=3D"left">=D0=A6=D0=B5=D0=BD=D0=B0, =D1=80=D1=83=D0=B1.</t=
h>
<th align=3D"left">=D0=A1=D1=83=D0=BC=D0=BC=D0=B0, =D1=80=D1=83=
=D0=B1.</th>
</tr>
<tr>
<td align=3D"left">=D0=9C=D1=83=D0=B6=D1=81=D0=BA=D0=B0=D1=
=8F =D0=BB=D0=B5=D1=82=D0=BD=D1=8F=D1=8F =D0=BE=D0=B4=D0=B5=D0=B6=D0=B4=
=D0=B0 1</td>
<td align=3D"right">2</td>
<td align=3D"right">1000.23</td>
<td align=3D"right">2000.46</td>
</tr>
<tr>
<td align=3D"left">=D0=9C=D1=83=D0=B6=D1=81=D0=BA=D0=B0=D1=
=8F =D0=BB=D0=B5=D1=82=D0=BD=D1=8F=D1=8F =D0=BE=D0=B4=D0=B5=D0=B6=D0=B4=
=D0=B0 2</td>
<td align=3D"right">3</td>
<td align=3D"right">56.00</td>
<td align=3D"right">168.00</td>
</tr>
<tr>
<td colspan=3D"3" align=3D"right">=D0=98=D1=82=D0=BE=D0=B3=D0=BE</t=
d>
<td align=3D"right">2168.46</td>
</tr>
</table>

<p>=D0=90=D0=B4=D1=80=D0=B5=D1=81 =D0=B4=D0=BE=D1=81=D1=82=D0=B0=D0=B2=
=D0=BA=D0=B8: =D0=9C=D0=BE=D1=81=D0=BA=D0=B2=D0=B0, =D0=A4=D0=BB=D0=BE=
=D1=82=D1=81=D0=BA=D0=B0=D1=8F =D1=83=D0=BB=D0=B8=D1=86=D0=B0, =D0=B4=D0=
=BE=D0=BC 12, =D0=BA=D0=B2.72</p>

<p>=D0=9A=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B9: 2-=
=D0=BE=D0=B9 =D0=BF=D0=BE=D0=B4=D1=8A=D0=B5=D0=B7=D0=B4, 4-=D1=8B=D0=B9 =
=D1=8D=D1=82=D0=B0=D0=B6</p>

</body>
</html>
--_=_swift_1566026927_e97faf701339ec540abb7bfef2e0f908_=_--
На этом с оформлением заказа мы закончили.

Магазин на Yii2, часть 22-1 Yii2. Отправка почты


Для отправки почты Yii2 предлагает расширение swiftmailer, которое нужно просто настроить.
Настраивается оно в файле config/web.php. Вот настройки класса swiftmailer по умолчанию:
$config = [
/*...*/,
'components' => [
/*...*/
'mailer' => [
'class' => 'yii\swiftmailer\Mailer',
// send all mails to a file by default. You have to set
// 'useFileTransport' to false and configure a transport
// for the mailer to send real emails.
'useFileTransport' => true,
],
/*...*/
],
/*...*/
];
Обратите внимание на настройку useFileTransport: когда она имеет значение true — письмо не
отправляется реально, его отправка просто эмулируется. Файл письма при этом будет сохранен в
папку runtime/mail.

Тестирование отправки почты


Давайте протестируем отправку почты. Для этого создадим форму обратной связи, которая будет
доступна на адресу /site/feedback.
<?php
namespace app\controllers;

use Yii;
use app\models\Feedback;
use yii\web\Controller;
class SiteController extends Controller {

/*...*/

public function actionFeedback() {


$model = new Feedback();
/*
* Если пришли post-данные, загружаем их в модель...
*/
if ($model->load(Yii::$app->request->post())) {
// ...и проверяем эти данные
if ( ! $model->validate()) {
/*
* Данные не прошли валидацию
*/
Yii::$app->session->setFlash(
'feedback-success',
false
);
// сохраняем в сессии введенные пользователем данные
Yii::$app->session->setFlash(
'feedback-data',
[
'name' => $model->name,
'email' => $model->email,
'body' => $model->body
]
);
/*
* Сохраняем в сессии массив сообщений об ошибках. Массив имеет вид
* [
* 'name' => [
* 'Поле «Ваше имя» обязательно для заполнения',
* ],
* 'email' => [
* 'Поле «Ваш email» обязательно для заполнения',
* 'Поле «Ваш email» должно быть адресом почты'
* ]
* ]
*/
Yii::$app->session->setFlash(
'feedback-errors',
$model->getErrors()
);
} else {
/*
* Данные прошли валидацию
*/

// отправляем письмо на почту администратора


$textBody = 'Имя: ' . strip_tags($model->name) . PHP_EOL;
$textBody .= 'Почта: ' . strip_tags($model->email) . PHP_EOL .
PHP_EOL;
$textBody .= 'Сообщение: ' . PHP_EOL . strip_tags($model->body);

$htmlBody = '<p><b>Имя</b>: ' . strip_tags($model->name) . '</p>';


$htmlBody .= '<p><b>Почта</b>: ' . strip_tags($model->email) .
'</p>';
$htmlBody .= '<p><b>Сообщение</b>:</p>';
$htmlBody .= '<p>' . nl2br(strip_tags($model->body)) . '</p>';

Yii::$app->mailer->compose()
->setFrom(Yii::$app->params['senderEmail'])
->setTo(Yii::$app->params['adminEmail'])
->setSubject('Заполнена форма обратной связи')
->setTextBody($textBody)
->setHtmlBody($htmlBody)
->send();

// данные прошли валидацию, отмечаем этот факт


Yii::$app->session->setFlash(
'feedback-success',
true
);
}
// выполняем редирект, чтобы избежать повторной отправки формы
return $this->refresh();
}
return $this->render('feedback', ['model' => $model]);
}

/*...*/

}
<?php
namespace app\models;

use yii\base\Model;

class Feedback extends Model {

public $name;
public $email;
public $body;

public function attributeLabels() {


return [
'name' => 'Ваше имя',
'email' => 'Ваш e-mail',
'body' => 'Ваше сообщение',
];
}

public function rules() {


return [
// удалить пробелы для всех трех полей формы
[['name', 'email', 'body'], 'trim'],
// поле name обязательно для заполнения
['name', 'required', 'message' => 'Поле «Ваше имя» обязательно для
заполнения'],
// поле email обязательно для заполнения
['email', 'required', 'message' => 'Поле «Ваш email» обязательно для
заполнения'],
// поле email должно быть корректным адресом почты
['email', 'email', 'message' => 'Поле «Ваш email» должно быть адресом
почты'],
// поле body обязательно для заполнения
['body', 'required', 'message' => 'Поле «Сообщение» обязательно для
заполнения'],
// поля name и email должны быть не более 50 символов
[
['name', 'email'],
'string',
'max' => 50,
'tooLong' => 'Поле должно быть длиной не более 50 символов'
],
// поле body должно быть не более 1000 символов
[
'body',
'string',
'max' => 1000,
'tooLong' => 'Сообщение должно быть длиной не более 1000 символов'
],
];
}
}
<?php
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;

$this->title = 'Обратная связь';

/*
* Если данные формы не прошли валидацию, получаем из сессии сохраненные
* данные, чтобы заполнить ими поля формы, не заставляя пользователя
* заполнять форму повторно
*/
$name = '';
$email = '';
$body = '';
if (Yii::$app->session->hasFlash('feedback-data')) {
$data = Yii::$app->session->getFlash('feedback-data');
$name = Html::encode($data['name']);
$email = Html::encode($data['email']);
$body = Html::encode($data['body']);
}
?>

<div class="container">
<?php
$success = false;
if (Yii::$app->session->hasFlash('feedback-success')) {
$success = Yii::$app->session->getFlash('feedback-success');
}
?>
<div id="response">
<?php if (!$success): ?>
<?php if (Yii::$app->session->hasFlash('feedback-errors')): ?>
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close"
data-dismiss="alert" aria-label="Закрыть">
<span aria-hidden="true">&times;</span>
</button>
<p>При заполнении формы допущены ошибки</p>
<?php $allErrors = Yii::$app->session->getFlash('feedback-
errors'); ?>
<ul>
<?php foreach ($allErrors as $errors): ?>
<?php foreach ($errors as $error): ?>
<li><?= $error; ?></li>
<?php endforeach; ?>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php else: ?>
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close"
data-dismiss="alert" aria-label="Закрыть">
<span aria-hidden="true">&times;</span>
</button>
<p>Ваше сообщение успешно отправлено</p>
</div>
<?php endif; ?>
</div>

<?php $form = ActiveForm::begin(['id' => 'feedback', 'class' => 'form-


horizontal']); ?>
<?= $form->field($model, 'name')->textInput(['value' => $name]); ?>
<?= $form->field($model, 'email')->input('email', ['value' => $email]); ?>
<?= $form->field($model, 'body')->textarea(['rows' => 5, 'value' =>
$body]); ?>
<div class="form-group">
<?= Html::submitButton('Отправить', ['class' => 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
Адрес почты отправителя и получателя мы берем из настроек приложения config/params.php:
<?php

return [
'adminEmail' => 'admin@example.com',
'senderEmail' => 'noreply@example.com',
'senderName' => 'Example.com mailer',
];
Файл письма в директории runtime/mail:
Message-ID: <3bcd3bb26e670ca057eaafb376d365d9@www.example.com>
Date: Wed, 14 Aug 2019 11:57:03 +0300
Subject: =?utf-8?Q?=D0=97=D0=B0=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?=
=?utf-8?Q?=D0=B0_=D1=84=D0=BE=D1=80=D0=BC=D0=B0_=D0=BE=D0=B1=D1=80=D0=B0?=
=?utf-8?Q?=D1=82=D0=BD=D0=BE=D0=B9_=D1=81=D0=B2=D1=8F=D0=B7=D0=B8?=
From: noreply@example.com
To: admin@example.com
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="_=_swift_1565773023_5daac4bcd4b10c645568b6655a1eb78a_=_"

--_=_swift_1565773023_5daac4bcd4b10c645568b6655a1eb78a_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

=D0=98=D0=BC=D1=8F: =D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9
=D0=9F=D0=BE=D1=87=D1=82=D0=B0: ivanov@mail.ru

=D0=A1=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5:=20
=D0=9A=D0=B0=D0=BA=D0=BE=D0=B5-=D1=82=D0=BE =D1=81=D0=BE=D0=BE=D0=B1=D1=
=89=D0=B5=D0=BD=D0=B8=D0=B5

--_=_swift_1565773023_5daac4bcd4b10c645568b6655a1eb78a_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<p><b>=D0=98=D0=BC=D1=8F</b>: =D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9</p=
><p><b>=D0=9F=D0=BE=D1=87=D1=82=D0=B0</b>: ivanov@mail.ru</p><p><b>=
=D0=A1=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5</b>:</p><p>=D0=9A=
=D0=B0=D0=BA=D0=BE=D0=B5-=D1=82=D0=BE =D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=
=B5=D0=BD=D0=B8=D0=B5</p>

--_=_swift_1565773023_5daac4bcd4b10c645568b6655a1eb78a_=_--

Шаблоны для писем


Способ отправки, который мы рассмотрели выше, подходит для небольших писем. Но если планируется
отправить большое письмо, удобнее использовать view-шаблоны, размещенные в директории mail.
Давайте создадим в этой директории файл feedback.php:
<?php
use yii\helpers\Html;
?>
<p><strong>Имя</strong>: <?= Html::encode($name); ?></p>
<p><strong>Email</strong>: <?= Html::encode($email); ?></p>
<p><strong>Сообщение</strong>:</p>
<p><?= nl2br(Html::encode($body)); ?></p>

А отправлять почту мы теперь будем так:

$mail = Yii::$app->mailer->compose(
'feedback',
[
'name' => strip_tags($model->name),
'email' => strip_tags($model->email),
'body' => strip_tags($model->body)
]
);
$mail->setFrom(Yii::$app->params['senderEmail'])
->setTo(Yii::$app->params['adminEmail'])
->setSubject('Заполнена форма обратной связи')
->send();
Наш view-шаблон можно обернуть layout-шаблоном, который расположен в
файле mail/layouts/html.php:
<?php
use yii\helpers\Html;

/* @var $this \yii\web\View view component instance */


/* @var $message \yii\mail\MessageInterface the message being composed */
/* @var $content string main view render result */
?>
<?php $this->beginPage() ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=<?= Yii::$app-
>charset ?>" />
<title><?= Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<?= $content ?>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

Для этого указываем в конфигурации путь к layout-шаблону:

$config = [
/*...*/,
'components' => [
/*...*/
'mailer' => [
'class' => 'yii\swiftmailer\Mailer',
'htmlLayout' => 'layouts/html',
'useFileTransport' => true,
],
/*...*/
],
/*...*/
];
И в view-шаблоне задаем title:
<?php
use yii\helpers\Html;
$this->title = 'Заполнена форма обратной связи';
?>
<p><strong>Имя</strong>: <?= Html::encode($name); ?></p>
<p><strong>Email</strong>: <?= Html::encode($email); ?></p>
<p><strong>Сообщение</strong>:</p>
<p><?= nl2br(Html::encode($body)); ?></p>

В итоге получим такое письмо:

Message-ID: <28000ba51fa0db3459bc8e51f8fab757@www.example.com>
Date: Thu, 15 Aug 2019 09:11:49 +0300
Subject: =?utf-8?Q?=D0=97=D0=B0=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?=
=?utf-8?Q?=D0=B0_=D1=84=D0=BE=D1=80=D0=BC=D0=B0_=D0=BE=D0=B1=D1=80=D0=B0?=
=?utf-8?Q?=D1=82=D0=BD=D0=BE=D0=B9_=D1=81=D0=B2=D1=8F=D0=B7=D0=B8?=
From: noreply@example.com
To: admin@example.com
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="_=_swift_1565849509_f5996ab5ab87f6be9c0a48cd7b75f89f_=_"

--_=_swift_1565849509_f5996ab5ab87f6be9c0a48cd7b75f89f_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

=D0=98=D0=BC=D1=8F: =D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9
Email: ivanov@mail.ru
=D0=A1=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5:
=D0=9A=D0=B0=D0=BA=D0=BE=D0=B5-=D1=82=D0=BE =D1=81=D0=BE=D0=BE=D0=B1=D1=
=89=D0=B5=D0=BD=D0=B8=D0=B5

--_=_swift_1565849509_f5996ab5ab87f6be9c0a48cd7b75f89f_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org=


/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns=3D"http://www.w3.org/1999/=
xhtml">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/ht=
ml; charset=3DUTF-8" />
<title>=D0=97=D0=B0=D0=BF=D0=BE=D0=BB=D0=
=BD=D0=B5=D0=BD=D0=B0 =D1=84=D0=BE=D1=80=D0=BC=D0=B0 =D0=BE=D0=B1=D1=80=
=D0=B0=D1=82=D0=BD=D0=BE=D0=B9 =D1=81=D0=B2=D1=8F=D0=B7=D0=B8</title>
=
</head>
<body>
<p><strong>=D0=98=D0=BC=D1=8F</strong>: =D0=
=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9</p>
<p><strong>Email</strong>: ivanov@mail.ru</p>
<p><strong>=D0=A1=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5</strong>:=
</p>
<p>=D0=9A=D0=B0=D0=BA=D0=BE=D0=B5-=D1=82=D0=BE =D1=81=D0=BE=D0=BE=D0=B1=
=D1=89=D0=B5=D0=BD=D0=B8=D0=B5</p>
</body>
</html>

--_=_swift_1565849509_f5996ab5ab87f6be9c0a48cd7b75f89f_=_--

Если не устраивает layout-шаблон, который предлагается по умолчанию, можно создать свой и указать
это в настройках:

$config = [
/*...*/,
'components' => [
/*...*/
'mailer' => [
'class' => 'yii\swiftmailer\Mailer',
'viewPath' => '@app/mail',
'htmlLayout' => 'layouts/main-html',
'textLayout' => 'layouts/main-text',
'messageConfig' => [
'charset' => 'UTF-8',
'from' => ['noreply@site.com' => 'Site Name'],
],
'useFileTransport' => true,
],
/*...*/
],
/*...*/
];

В этой конфигурации:

• viewPath — путь к шаблонам писем


• htmlLayout и textLayout — html и text макеты писем
• charset — кодировка писем по умолчанию
• from — адрес почты и имя отправителя по умолчанию

Реальная отправка почты


Вроде все прошло успешно, можно отправлять письма по-настоящему. Редактируем
файл config/web.php:
$config = [
/*...*/,
'components' => [
/*...*/
'mailer' => [
'class' => 'yii\swiftmailer\Mailer',
'htmlLayout' => 'layouts/html',
'useFileTransport' => false,
'transport' => [
'class' => 'Swift_SmtpTransport',
'host' => 'smtp.mail.ru',
'username' => 'имя_пользователя@mail.ru',
'password' => 'пароль_от_почты',
'port' => '465',
'encryption' => 'ssl',
],
],
/*...*/
],
/*...*/
];
И вносим изменения в файл config/params.php:
return [
'adminEmail' => 'реальный_адрес_получателя@mail.ru',
// должен совпадать с имя_пользователя@mail.ru
'senderEmail' => 'реальный_адрес_отправителя@mail.ru',
'senderName' => 'Example.com mailer',
];
При отправке писем через SMTP-сервер MAIL.RU содержимое поля From: должно совпадать с именем
почтового ящика, в котором была осуществлена SMTP-авторизация: если в настройках почтовой программы
указан почтовый ящик ivanov@mail.ru, то именно это имя почтового ящика должно указываться в
поле From:.

Магазин на Yii2, часть 23. Админка: создание модуля и


аутентификация админа

Хорошо, с публичной частью сайта мы закончили, теперь займемся панелью управления. Для этого
создадим модуль с помощью генератора кода Gii. Модуль можно рассматривать как миниатюрное
приложение, состоящие из моделей, представлений, контроллеров и других вспомогательных
компонентов. Но, в отличие от приложения, модуль нельзя развертывать отдельно, он должен
находиться внутри приложения.

Создаем модуль для админки


Итак, набираем в адресной строке браузера www.server.loc/gii, переходим по ссылке Module
Generator, задаем имя класса модуля и идентификатор модуля:

Module Class: app\modules\admin\Module

Module ID: admin


Добавляем в файл конфигурации config/web.php:
$config = [
'id' => 'basic',
/*...*/
'aliases' => [
/*...*/
],
'modules' => [
'admin' => [
'class' => 'app\modules\admin\Module',
],
],
'components' => [
/*...*/
],
/*...*/
];
Теперь панель управления доступна по адресу /admin/default/index или просто admin:

А в нашем приложении появилась еще одна директория modules:

Создаем layout-шаблон для админки


По аналогии с приложением нам нужен layout-шаблон в директории modules/admin/views/layouts,
пусть это будет main.php. Добавим имя layout-шаблона в конфигурацию модуля:
$config = [
'id' => 'basic',
/*...*/
'aliases' => [
/*...*/
],
'modules' => [
'admin' => [
'class' => 'app\modules\admin\Module',
'layout' => 'main'
],
],
'components' => [
/*...*/
],
/*...*/
];
И создаем файл modules/admin/views/layouts/main.php:
<?php
/* @var $this \yii\web\View */
/* @var $content string */

use yii\helpers\Html;
use yii\bootstrap\Nav;
use yii\bootstrap\NavBar;
use yii\helpers\Url;
use app\assets\AppAsset;

AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
<meta charset="<?= Yii::$app->charset ?>">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php $this->registerCsrfMetaTags() ?>
<title><?= Html::encode($this->title) ?> | Панель управления</title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>

<header>
<?php
NavBar::begin([
'brandLabel' => 'Панель управления',
'brandUrl' => Url::to(['/admin/default/index']),
'options' => [
'class' => 'navbar-inverse',
],
]);
echo Nav::widget([
'options' => ['class' => 'navbar-nav'],
'items' => [
[
'label' => 'Каталог',
'items' => [
['label' => 'Категории', 'url' =>
['/admin/category/index']],
['label' => 'Товары', 'url' => ['/admin/product/index']],
],
],
['label' => 'Заказы', 'url' => ['/admin/order/index']],
['label' => 'Пользователи', 'url' => ['/admin/user/index']],
['label' => 'Страницы', 'url' => ['/admin/page/index']],
],
]);
echo Nav::widget([
'options' => ['class' => 'navbar-nav navbar-right'],
'items' => [
['label' => 'Выйти', 'url' => ['/admin/auth/logout']],
],
]);
NavBar::end();
?>
</header>

<div class="container">
<?= $content; ?>
</div>

<footer class="footer">
<div class="container">
<p>&copy; <?= Yii::$app->params['shopName'] ?></p>
</div>
</footer>

<?php $this->endBody() ?>


</body>
</html>
<?php $this->endPage() ?>

Аутентификация администратора
Здесь все будет предельно просто. Создаем контроллер AuthController и модель LoginForm — чтобы
проверять данные формы. Саму форму разместим в view-шаблоне login.php:
<?php
namespace app\modules\admin\controllers;

use Yii;
use yii\web\Controller;
use app\modules\admin\models\LoginForm;

class AuthController extends Controller {

public function actionLogin() {


$model = new LoginForm();
/*
* Если пришли post-данные, загружаем их в модель...
*/
if ($model->load(Yii::$app->request->post())) {
// ...и проверяем эти данные
if ($model->validate()) {
// данные корректные, пробуем авторизовать
if (Yii::$app->params['adminEmail'] == $model->email
&& Yii::$app->params['adminPassword'] == $model->password) {
LoginForm::login();
return $this->redirect('/admin/default/index');
} else {
return $this->refresh();
}
}
}
return $this->render('login', ['model' => $model]);;
}

public function actionLogout() {


LoginForm::logout();
return $this->redirect('/admin/auth/login');
}
}
<?php
namespace app\modules\admin\models;

use yii\base\Model;

class LoginForm extends Model {

public $email;
public $password;

public function rules() {


return [
// удалим случайные пробелы для двух полей
[['email', 'password'], 'trim'],
// email и пароль обязательны для заполнения
[
['email', 'password'],
'required',
'message' => 'Это поле обязательно для заполнения'
],
// поле email должно быть адресом почты
['email', 'email'],
// пароль не может быть короче 12 символов
[['password'], 'string', 'min' => 12],
];
}

public function attributeLabels() {


return [
'username' => 'E-mail',
'password' => 'Пароль',
];
}

public static function login() {


$session = Yii::$app->session;
$session->open();
$session->set('auth_site_admin', true);
}

public static function logout() {


$session = Yii::$app->session;
$session->open();
if ($session->has('auth_site_admin')) {
$session->remove('auth_site_admin');
}
}
}
<?php
/*
* View-шаблон, файл modules/admin/views/auth/login.php
*/
use yii\helpers\Html;
use yii\widgets\ActiveForm;

$this->title = 'Аутентификация';
?>

<div class="container">
<?php
$form = ActiveForm::begin();
?>
<?= $form->field($model, 'email')->input('email'); ?>
<?= $form->field($model, 'password')->input('password'); ?>
<div class="form-group">
<?= Html::submitButton('Отправить', ['class' => 'btn btn-primary']) ?>
</div>
<?php
ActiveForm::end();
?>
</div>
Пароль администратора будем хранить в настройках приложения, файл config/params.php:
<?php

return [
'adminEmail' => 'admin@example.com',
'adminPassword' => 'qwerty123456',
'senderEmail' => 'noreply@example.com',
'senderName' => 'Example.com mailer',
/*
* Название магазина, например «Lamoda» или «WildBerries»
*/
'shopName' => 'Магазин одежды и обуви',
/*
* Значения по умолчанию для мета-тегов title, keywords и description
*/
'defaultTitle' => 'Интернет-магазин модной одежды и обуви',
'defaultKeywords' => 'одежда, обувь, мужская, женская, детская, зимняя,
летняя',
'defaultDescription' => 'Коллекции женской, мужской, детской одежды и обуви',
/*
* Количество товаров на странице для постраничной навигации
*/
'pageSize' => 2
];
Как видите, мы просто создаем элемент массива $_SESSION, пока он существует — администратор
может находиться в панели управления. Теперь нам надо обеспечить проверку существования этого
элемента массива перед выполнением любого action любого контроллера в модуле admin. Для этого
создадим еще один контроллер AdminController, который будет родительским для всех остальных.
<?php
namespace app\modules\admin\controllers;

use yii\web\Controller;

class AdminController extends Controller {

public function beforeAction($action) {


$session = Yii::$app->session;
$session->open();
if (!$session->has('auth_site_admin')) {
$this->redirect('/admin/auth/login');
return false;
}
return parent::beforeAction($action);
}
}
И внесем изменения в DefaultController, унаследовав его от AdminController:
<?php
namespace app\modules\admin\controllers;

class DefaultController extends AdminController {


public function actionIndex() {
return $this->render('index');
}
}
Магазин на Yii2, часть 24. Админка: модель,
контроллер и представления для заказов
Главная > Блог > Web-разработка > Yii2 и Laravel >

Магазин на Yii2, часть 24. Админка: модель, контроллер и


представления для заказов
28.08.2019
Теги: CRUD • Web-
разработка • Yii2 • Заказ • ИнтернетМагазин • КаталогТоваров • ПанельУправления • Практика • Фреймворк

Чтобы создать модель для работы с заказами в админке — используем генератор кода. Переходим по
ссылке «Model Generator», задаем имя таблицы БД, имя класса модели и пространство имен:

Table Name: order

Model Class Name: Order

Namespace: app\modules\admin\models

Далее жмем кнопку «Preview» и после этого — «Generate». В итоге получаем класс модели:

<?php
namespace app\modules\admin\models;

use Yii;

/**
* This is the model class for table "order".
*
* @property int $id Идентификатор заказа
* @property int $user_id Идентификатор пользователя
* @property string $name Имя и фамилия покупателя
* @property string $email Почта покупателя
* @property string $phone Телефон покупателя
* @property string $address Адрес доставки
* @property string $comment Комментарий к заказу
* @property string $amount Сумма заказа
* @property int $status Статус заказа
* @property string $created Дата и время создания
* @property string $updated Дата и время обновления
*/
class Order extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'order';
}

/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['user_id', 'status'], 'integer'],
[['amount'], 'number'],
[['created', 'updated'], 'safe'],
[['name', 'email', 'phone'], 'string', 'max' => 50],
[['address', 'comment'], 'string', 'max' => 255],
];
}

/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'user_id' => 'User ID',
'name' => 'Name',
'email' => 'Email',
'phone' => 'Phone',
'address' => 'Address',
'comment' => 'Comment',
'amount' => 'Amount',
'status' => 'Status',
'created' => 'Created',
'updated' => 'Updated',
];
}
}

Теперь используем «CRUD Generator», который создаст нам контроллер и view-шаблоны. И мы получим
готовой код для создания, просмотра, редактирования и удаления заказов.

Model Class: app\modules\admin\models\Order

Controller Class: app\modules\admin\controllers\OrderController

View Path: @app/modules/admin/views/order

Base Controller Class: app\modules\admin\controllers\AdminController


Перейдем по адресу /admin/order/index и посмотрим на результат работы GRUD генератора:

Хорошо, теперь займемся приведением в порядок того кода, который сформировал герератор. Начнем с
класса модели:

<?php
namespace app\modules\admin\models;

use yii\db\ActiveRecord;

/**
* Это модель для таблицы БД `order`
*
* @property int $id Идентификатор заказа
* @property int $user_id Идентификатор пользователя
* @property string $name Имя и фамилия покупателя
* @property string $email Почта покупателя
* @property string $phone Телефон покупателя
* @property string $address Адрес доставки
* @property string $comment Комментарий к заказу
* @property string $amount Сумма заказа
* @property int $status Статус заказа
* @property string $created Дата и время создания
* @property string $updated Дата и время обновления
*/
class Order extends ActiveRecord {
/**
* Возвращает имя таблицы базы данных
*/
public static function tableName() {
return 'order';
}

/**
* Правила валидации полей формы при редактировании заказа
*/
public function rules() {
return [
[['user_id', 'status'], 'integer'],
[['amount'], 'number'],
[['created', 'updated'], 'safe'],
[['name', 'email', 'phone'], 'string', 'max' => 50],
[['address', 'comment'], 'string', 'max' => 255],
];
}

/**
* Возвращает имена полей формы для редактирования заказа
*/
public function attributeLabels() {
return [
'id' => 'Номер',
'user_id' => 'User ID',
'name' => 'Имя',
'email' => 'Почта',
'phone' => 'Телефон',
'address' => 'Адрес доставки',
'comment' => 'Комментарий',
'amount' => 'Сумма',
'status' => 'Статус',
'created' => 'Дата создания',
'updated' => 'Дата обновления',
];
}
}

Отредактируем view-шаблон списка заказов — уберем кнопку «Create Order», поменяем заголовок и
изменим состав полей, которые выводятся в таблице:

<?php
/*
* Файл view-шаблона modules/admin/views/order/index.php
*/
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */


/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Заказы';
?>

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

<?=
GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'name',
'email:email',
'phone',
'amount',
[
'attribute' => 'status',
'value' => function ($data) {
switch ($data->status) {
case 0: return '<span class="text-danger">Новый</span>';
case 1: return '<span class="text-warning">Обработан</span>';
case 2: return '<span class="text-warning">Оплачен</span>';
case 3: return '<span class="text-warning">Доставлен</span>';
case 4: return '<span class="text-success">Завершен</span>';
default: return 'Ошибка';
}
},
'format' => 'html'
],
'created',
'updated',
['class' => 'yii\grid\ActionColumn'],
],
]);
?>

И вот что у нас получилось в итоге:

Благодаря тому, что мы используем провайдер данных и виджет данных GridView, автоматически
получаем постраничное разделение данных. Кроме того, заказы можно отсортировать по любому полю, в
том числе по статусу, так что наверху будут новые заказы, а внизу — завершенные.

Но для удобства работы желательно, чтобы заказы изначально были отсортированы по статусу. Для
этого внесем изменения в контроллер:
<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Order;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* Класс OrderController реализует CRUD для заказов
*/
class OrderController extends AdminController {

public function behaviors() {


return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Список всех заказов с постраничной навигацией
* и сортировкой по статусу
*/
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Order::find(),
'pagination' => [
'pageSize' => 5 // пять заказов на страницу
],
'sort' => [
'defaultOrder' => [
// сортировка по статусу, по возрастанию
'status' => SORT_ASC
]
]
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Просмотр данных существующего заказа
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Создание нового заказа, метод не используется
*/
public function actionCreate() {
$model = new Order();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('create', [
'model' => $model,
]);
}

/**
* Обновление существующего заказа
*/
public function actionUpdate($id) {
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}

/**
* Удаление существующего заказа
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Поиск заказа по идентификатору
*/
protected function findModel($id) {
if (($model = Order::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('Заказ не найден');
}
}

Магазин на Yii2, часть 25. Админка: главная страница


и работа с заказами
Главная страница
На главной странице панели управления будем показывать две таблицы — новые заказы и заказы в
работе. Чтобы администратор магазина сразу видел, какие заказы надо обработать, у каких —
отслеживать оплату и доставку, а какие — можно завершать. Итак, вносим изменения в
контроллер DefaultController и view-шаболон default/index.php.
<?php
namespace app\modules\admin\controllers;

use yii\data\ActiveDataProvider;
use app\modules\admin\models\Order;

class DefaultController extends AdminController {

public function actionIndex() {


$queueOrders = new ActiveDataProvider([
'query' => Order::find()
->where(['status' => 0])
->orderBy(['created' => SORT_ASC]),
'sort' => false,
'pagination' => [
// три заказа на страницу
'pageSize' => 3,
// уникальный параметр пагинации
'pageParam' => 'queue',

]
]);
$processOrders = new ActiveDataProvider([
'query' => Order::find()
->where(['IN', 'status', [1,2,3]])
->orderBy(['updated' => SORT_ASC]),
'sort' => false,
'pagination' => [
// три заказа на страницу
'pageSize' => 3,
// уникальный параметр пагинации
'pageParam' => 'process',

]
]);
return $this->render('index', [
'queueOrders' => $queueOrders,
'processOrders' => $processOrders,
]);
}
}
Поскольку на главной странице у нас две таблицы GridView, при переходе на вторую страницу списка новых
заказов или списка заказов в работе, будет происходить переход на вторую страницу сразу для двух списков.
Поэтому надо задать для каждого списка уникальный параметр пагинации pageParam. Тогда ссылки на
вторую страницу для первого и второго списков будут иметь вид:

http://www.server.com/admin/default/index?queue=2&per-page=3
http://www.server.com/admin/default/index?process=2&per-page=3
<?php
/*
* Файл view-шаблона modules/admin/views/default/index.php
*/
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */


/* @var $queueOrders yii\data\ActiveDataProvider */
/* @var $processOrders yii\data\ActiveDataProvider */

$this->title = 'Текущее состояние';


?>

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

<h2>Новые заказы</h2>

<?=
GridView::widget([
'dataProvider' => $queueOrders,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'name',
'email:email',
'phone',
'amount',
[
'attribute' => 'status',
'value' => function ($data) {
switch ($data->status) {
case 0: return '<span class="text-danger">Новый</span>';
case 1: return '<span class="text-warning">Обработан</span>';
case 2: return '<span class="text-warning">Оплачен</span>';
case 3: return '<span class="text-warning">Доставлен</span>';
case 4: return '<span class="text-success">Завершен</span>';
default: return 'Ошибка';
}
},
'format' => 'html'
],
'created',
'updated'
],
]);
?>

<h2>Заказы в работе</h2>

<?=
GridView::widget([
'dataProvider' => $processOrders,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'id',
'name',
'email:email',
'phone',
'amount',
[
'attribute' => 'status',
'value' => function ($data) {
switch ($data->status) {
case 0: return '<span class="text-danger">Новый</span>';
case 1: return '<span class="text-warning">Обработан</span>';
case 2: return '<span class="text-warning">Оплачен</span>';
case 3: return '<span class="text-warning">Доставлен</span>';
case 4: return '<span class="text-success">Завершен</span>';
default: return 'Ошибка';
}
},
'format' => 'html'
],
'created',
'updated'
],
]);
?>
Работа с заказами
Сейчас при просмотре заказа мы видим только данные покупателя, дату создания и обновления заказа.
Но на странице просмотра нет данных о составе заказа. Давайте это исправим: создадим
модель OrderItem и добавим в класс модели Order метод getItems():
<?php
namespace app\modules\admin\models;

use yii\db\ActiveRecord;

class OrderItem extends ActiveRecord {

/**
* Возвращает имя таблицы БД
*/
public static function tableName() {
return 'order_item';
}

/**
* Позволяет получить заказ, в который входит этот элемент
*/
public function getOrder() {
// связь таблицы БД `order_item` с таблицей `order`
return $this->hasOne(Order::class, ['id' => 'order_id']);
}
}
<?php
namespace app\modules\admin\models;

use yii\db\ActiveRecord;

class Order extends ActiveRecord {

/*...*/

/**
* Позволяет получить все товары заказа
*/
public function getItems() {
// связь таблицы БД `order` с таблицей `order_item`
return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
}
}

В представлении получим список товаров заказа и покажем таблицу:

<?php
/*
* Файл view-шаблона modules/admin/views/order/view.php
*/
use yii\helpers\Html;
use yii\widgets\DetailView;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Order */

$this->title = 'Просмотр заказа № ' . $model->id;


?>

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

<p>
<?=
Html::a(
'Изменить',
['update', 'id' => $model->id],
['class' => 'btn btn-primary']
);
?>
<?=
Html::a(
'Удалить',
['delete', 'id' => $model->id],
[
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Вы уверены, что хотите удалить заказ?',
'method' => 'post',
],
]
);
?>
</p>

<?=
DetailView::widget([
'model' => $model,
'attributes' => [
[
'attribute' => 'status',
'value' => function ($data) {
switch ($data->status) {
case 0: return '<span class="text-danger">Новый</span>';
case 1: return '<span class="text-warning">Обработан</span>';
case 2: return '<span class="text-warning">Оплачен</span>';
case 3: return '<span class="text-warning">Доставлен</span>';
case 4: return '<span class="text-success">Завершен</span>';
default: return 'Ошибка';
}
},
'format' => 'html'
],
'name',
'email:email',
'phone',
'address',
'comment',
'created',
'updated'
],
]);
?>

<?php
$products = $model->items;
?>
<table class="table table-bordered table-striped">
<tr>
<th>Наименование</th>
<th>Количество</th>
<th>Цена</th>
<th>Сумма</th>
</tr>
<?php foreach ($products as $product): ?>
<tr>
<td><?= $product->name; ?></td>
<td class="text-right"><?= $product->quantity; ?></td>
<td class="text-right"><?= $product->price; ?></td>
<td class="text-right"><?= $product->cost; ?></td>
</tr>
<?php endforeach; ?>
<tr>
<th colspan="3" class="text-right">Итого</th>
<th class="text-right"><?= $model->amount; ?></th>
</tr>
</table>

Хорошо, с просмотром заказа разобрались, осталось еще подправить view-шаблон для редактирования
заказа. Давайте посмотрим на код шаблона:

<?php
/*
* Файл view-шаблона modules/admin/views/order/update.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Order */

$this->title = 'Редактирование заказа № ' . $model->id;


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

<?=
$this->render('_form', [
'model' => $model,
]);
?>
Вопреки ожиданиям, формы там нет, она расположена в файле _form.php:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Order */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>

<?= $form->field($model, 'user_id')->textInput() ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'email')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'phone')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'address')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'comment')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'amount')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'status')->textInput() ?>
<?= $form->field($model, 'created')->textInput() ?>
<?= $form->field($model, 'updated')->textInput() ?>
<div class="form-group">
<?= Html::submitButton('Save', ['class' => 'btn btn-success']) ?>
</div>

<?php ActiveForm::end(); ?>

Давайте ее немного изменим — запретим редактировать сумму заказа, дату создания и обновления +
заменим текстовое поле статуса заказа на выпадающий список.

<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Order */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>

<?php
$items = [
0 => 'Новый',
1 => 'Обработан',
2 => 'Оплачен',
3 => 'Доставлен',
4 => 'Завершен',
];
echo $form->field($model, 'status')->dropDownList($items);
echo $form->field($model, 'name')->textInput(['maxlength' => true]);
echo $form->field($model, 'email')->textInput(['maxlength' => true]);
echo $form->field($model, 'phone')->textInput(['maxlength' => true]);
echo $form->field($model, 'address')->textarea(['rows' => 2, 'maxlength' =>
true]);
echo $form->field($model, 'comment')->textarea(['rows' => 2, 'maxlength' =>
true]);
echo $form->field($model, 'amount')->textInput(['readonly' => true]);
echo $form->field($model, 'created')->textInput(['readonly' => true]);
echo $form->field($model, 'updated')->textInput(['readonly' => true]);
?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>

<?php ActiveForm::end(); ?>

Нам еще нужно изменять дату и время обновления заказа при редактировании. Чтобы правильно их
сортировать на главной странице панели управления. Для этого добавим метод behaviors() классу
модели Order:
<?php
namespace app\modules\admin\models;

use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\Expression;

class Order extends ActiveRecord {

/*...*/

public function behaviors() {


return [
[
'class' => TimestampBehavior::class,
'attributes' => [
// при обновлении существующей записи присвоить атрибуту
// updated значение метки времени UNIX
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated'],
],
// если вместо метки времени UNIX используется DATETIME
'value' => new Expression('NOW()'),
],
];
}

/*...*/
}
И последнее, что осталось сделать — удалять записи из таблицы БД order_item при удалении заказа:
<?php
namespace app\modules\admin\models;

use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\Expression;

class Order extends ActiveRecord {


/*...*/

/**
* Удаляет товары заказа при удалении заказа
*/
public function afterDelete() {
parent::afterDelete();
OrderItem::deleteAll(['order_id' => $this->id]);
}
/*...*/
}

Магазин на Yii2, часть 26. Админка: модели и


контроллеры для категорий и товаров
Хорошо, с заказами закончили, теперь займемся каталогом. Воспользуемся генератором кода Gii, чтобы
создать модели категорий и товаров каталога. После чего с помощью все того же генератора кода
создадим контроллеры и представления для реализации функционала просмотра, создания,
редактирования и удаления категорий и товаров.

Создаем модели категорий и товаров каталога


Итак, создаем модель категорий каталога:

Table Name: category

Model Class Name: Category


<?php
namespace app\modules\admin\models;

use Yii;

/**
* This is the model class for table "category".
*
* @property int $id Уникальный идентификатор
* @property int $parent_id Родительская категория
* @property string $name Наименование категории
* @property string $content Описание категории
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
* @property string $image Имя файла изображения
*/
class Category extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'category';
}

/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['parent_id'], 'integer'],
[['name'], 'required'],
[['name', 'content', 'keywords', 'description', 'image'], 'string',
'max' => 255],
];
}

/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'parent_id' => 'Parent ID',
'name' => 'Name',
'content' => 'Content',
'keywords' => 'Keywords',
'description' => 'Description',
'image' => 'Image',
];
}
}

Создаем модель товаров каталога:

Table Name: product

Model Class Name: Product

<?php
namespace app\modules\admin\models;

use Yii;

/**
* This is the model class for table "product".
*
* @property int $id Уникальный идентификатор
* @property int $category_id Родительская категория
* @property int $brand_id Идентификатор бренда
* @property string $name Наименование товара
* @property string $content Описание товара
* @property string $price Цена товара
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
* @property string $image Имя файла изображения
* @property int $hit Лидер продаж?
* @property int $new Новый товар?
* @property int $sale Распродажа?
*/
class Product extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'product';
}

/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['category_id', 'brand_id', 'name'], 'required'],
[['category_id', 'brand_id', 'hit', 'new', 'sale'], 'integer'],
[['content'], 'string'],
[['price'], 'number'],
[['name', 'keywords', 'description', 'image'], 'string', 'max' => 255],
];
}

/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'category_id' => 'Category ID',
'brand_id' => 'Brand ID',
'name' => 'Name',
'content' => 'Content',
'price' => 'Price',
'keywords' => 'Keywords',
'description' => 'Description',
'image' => 'Image',
'hit' => 'Hit',
'new' => 'New',
'sale' => 'Sale',
];
}
}

Создадем контроллеры и представления


Контроллеры и представления, реализующие CRUD-операции для категорий каталога:

Model Class: app\modules\admin\models\Category

Controller Class: app\modules\admin\controllers\CategoryController

View Path: @app/modules/admin/views/category

Base Controller Class: app\modules\admin\controllers\AdminController


<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Category;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* CategoryController implements the CRUD actions for Category model.
*/
class CategoryController extends AdminController
{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Lists all Category models.
* @return mixed
*/
public function actionIndex()
{
$dataProvider = new ActiveDataProvider([
'query' => Category::find(),
]);

return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Displays a single Category model.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new Category model.
* If creation is successful, the browser will be redirected to the 'view'
page.
* @return mixed
*/
public function actionCreate()
{
$model = new Category();

if ($model->load(Yii::$app->request->post()) && $model->save()) {


return $this->redirect(['view', 'id' => $model->id]);
}

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

/**
* Updates an existing Category model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);

if ($model->load(Yii::$app->request->post()) && $model->save()) {


return $this->redirect(['view', 'id' => $model->id]);
}

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

/**
* Deletes an existing Category model.
* If deletion is successful, the browser will be redirected to the 'index'
page.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->findModel($id)->delete();

return $this->redirect(['index']);
}

/**
* Finds the Category model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be thrown.
* @param integer $id
* @return Category the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = Category::findOne($id)) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not exist.');


}
}

Контроллеры и представления, реализующие CRUD-операции для товаров каталога:

Model Class: app\modules\admin\models\Product

Controller Class: app\modules\admin\controllers\ProductController

View Path: @app/modules/admin/views/product

Base Controller Class: app\modules\admin\controllers\AdminController

<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Product;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* ProductController implements the CRUD actions for Product model.
*/
class ProductController extends AdminController
{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Lists all Product models.
* @return mixed
*/
public function actionIndex()
{
$dataProvider = new ActiveDataProvider([
'query' => Product::find(),
]);

return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Displays a single Product model.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new Product model.
* If creation is successful, the browser will be redirected to the 'view'
page.
* @return mixed
*/
public function actionCreate()
{
$model = new Product();

if ($model->load(Yii::$app->request->post()) && $model->save()) {


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

/**
* Updates an existing Product model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);

if ($model->load(Yii::$app->request->post()) && $model->save()) {


return $this->redirect(['view', 'id' => $model->id]);
}

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

/**
* Deletes an existing Product model.
* If deletion is successful, the browser will be redirected to the 'index'
page.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->findModel($id)->delete();

return $this->redirect(['index']);
}

/**
* Finds the Product model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be thrown.
* @param integer $id
* @return Product the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = Product::findOne($id)) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not exist.');


}
}
Магазин на Yii2, часть 27. Админка: приводим в
порядок сгенерированный код
Создадим с помощью генератора кода классы модели, контроллера (для CRUD-операций) и файлы view-
шаблонов для брендов. Все по аналогии с категориями и товарами каталога. Подробно на этом
останавливаться не будем, потому что проделывали это уже несколько раз. И после этого займемся
приведением в порядок кода, который сформировал для нас Gii.

Это будет не окончательный вариант, а только первое приближение. Дальше нужно будет еще
организовать загрузку файлов изображений для товаров, категорий и брендов; создать выпадающий
список для выбора родителя для категории и товара и т.д. и т.п.

Работа с брендами
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;

/**
* Это модель для таблицы БД `category`
*
* @property int $id Уникальный идентификатор
* @property string $name Наименование бренда
* @property string $content Описание бренда
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
* @property string $image Имя файла изображения
*/
class Brand extends ActiveRecord {

/**
* Возвращает имя таблицы базы данных
*/
public static function tableName() {
return 'brand';
}

/**
* Правила валидации полей формы при создании и редактировании бренда
*/
public function rules() {
return [
[['name'], 'required'],
[['name', 'content', 'keywords', 'description', 'image'], 'string',
'max' => 255],
];
}

/**
* Возвращает имена полей формы для создания и редактирования бренда
*/
public function attributeLabels() {
return [
'id' => 'ID',
'name' => 'Наименование',
'content' => 'Описание',
'keywords' => 'Мета-тег keywords',
'description' => 'Мета-тег description',
'image' => 'Изображение',
];
}
}
<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Brand;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* Класс BrandController реализует CRUD для брендов
*/
class BrandController extends AdminController {

public function behaviors() {


return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Список всех брендов с постраничной навигацией
*/
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Brand::find(),
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Просмотр данных существующего бренда
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Создание нового бренда
*/
public function actionCreate() {
$model = new Brand();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('create', [
'model' => $model,
]);
}

/**
* Обновление существующего бренда
*/
public function actionUpdate($id) {
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}

/**
* Удаление существующего бренда
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Поиск бренда по идентификатору
*/
protected function findModel($id) {
if (($model = Brand::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('The requested page does not exist.');
}
}
<?php
/*
* Страница списка всех брендов, файл modules/admin/views/brand/index.php
*/
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */


/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Бренды';
?>

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


<p>
<?= Html::a('Добавить бренд', ['create'], ['class' => 'btn btn-success']); ?>
</p>

<?=
GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'name',
[
'attribute' => 'keywords',
'value' => function($data) {
return empty($data->keywords) ? 'Не задано' : $data->keywords;
}
],
[
'attribute' => 'description',
'value' => function($data) {
return empty($data->description) ? 'Не задано' : $data-
>description;
}
],
['class' => 'yii\grid\ActionColumn'],
],
]);
?>
<?php
/*
* Страница добавления нового бренда, файл modules/admin/views/brand/create.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Brand */

$this->title = 'Новый бренд';


?>

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


<?=
$this->render(
'_form',
['model' => $model]
);
?>
<?php
/*
* Страница редактирования существующего бренда, файл
modules/admin/views/brand/update.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Brand */

$this->title = 'Редактирование бренда: ' . $model->name;


?>

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


<?=
$this->render(
'_form', [
'model' => $model,
]);
?>
<?php
/*
* Страница просмотра данных бренда, файл modules/admin/views/brand/view.php
*/
use yii\helpers\Html;
use yii\widgets\DetailView;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Brand */

$this->title = 'Просмотр бренда: ' . $model->name;


?>

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


<p>
<?= Html::a('Изменить', ['update', 'id' => $model->id], ['class' => 'btn btn-
primary']); ?>
<?=
Html::a(
'Удалить',
['delete', 'id' => $model->id],
[
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Вы уверены, что хотите удалить бренд?',
'method' => 'post',
],
]
);
?>
</p>

<?=
DetailView::widget([
'model' => $model,
'attributes' => [
'name',
'content',
'keywords',
'description',
'image',
],
]);
?>
<?php
/*
* Форма для добавления и редактирования бренда, файл
modules/admin/views/brand/_form.php
*/
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Brand */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'content')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'image')->textInput(['maxlength' => true]) ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

Работа с категориями
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;

/**
* Это модель для таблицы БД `category`
*
* @property int $id Уникальный идентификатор
* @property int $parent_id Родительская категория
* @property string $name Наименование категории
* @property string $content Описание категории
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
* @property string $image Имя файла изображения
*/
class Category extends ActiveRecord {

/**
* Возвращает имя таблицы базы данных
*/
public static function tableName() {
return 'category';
}

/**
* Возвращает данные о родительской категории
*/
public function getParent() {
return $this->hasOne(Category::class, ['id' => 'parent_id']);
}

/**
* Возвращает наименование родительской категории
*/
public function getParentName() {
$parent = $this->parent;
return $parent ? $parent->name : '';
}

/**
* Правила валидации полей формы при создании и редактировании категории
*/
public function rules() {
return [
[['parent_id'], 'integer'],
[['name'], 'required'],
[['name', 'content', 'keywords', 'description', 'image'], 'string',
'max' => 255],
];
}

/**
* Возвращает имена полей формы для создания и редактирования категории
*/
public function attributeLabels() {
return [
'id' => 'ID',
'parent_id' => 'Родитель',
'name' => 'Наименование',
'content' => 'Описание',
'keywords' => 'Мета-тег keywords',
'description' => 'Мета-тег description',
'image' => 'Изображение'
];
}
}
<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Category;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* Класс CategoryController реализует CRUD для категорий
*/
class CategoryController extends AdminController {

public function behaviors() {


return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Список всех категорий с постраничной навигацией
*/
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Category::find(),
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Просмотр данных существующей категории
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Создание новой категории
*/
public function actionCreate() {
$model = new Category();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('create', [
'model' => $model,
]);
}

/**
* Обновление существующей категории
*/
public function actionUpdate($id) {
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}

/**
* Удаление существующей категории
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Поиск категории по идентификатору
*/
protected function findModel($id) {
if (($model = Category::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('The requested page does not exist.');
}
}
<?php
/*
* Страница списка всех категорий, файл modules/admin/views/category/index.php
*/
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */


/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Категории каталога';


?>

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


<p>
<?= Html::a('Добавить категорию', ['create'], ['class' => 'btn btn-success']);
?>
</p>
<?=
GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'name',
[
'attribute' => 'parent_id',
'value' => function($data) {
return $data->getParentName();
}
],
[
'attribute' => 'keywords',
'value' => function($data) {
return empty($data->keywords) ? 'Не задано' : $data->keywords;
}
],
[
'attribute' => 'description',
'value' => function($data) {
return empty($data->description) ? 'Не задано' : $data-
>description;
}
],
['class' => 'yii\grid\ActionColumn'],
],
]);
?>
<?php
/*
* Страница добавления новой категории, файл
modules/admin/views/category/create.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Category */

$this->title = 'Новая категория';


?>

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


<?=
$this->render(
'_form',
['model' => $model]
);
?>
<?php
/*
* Страница редактирования категории, файл modules/admin/views/category/update.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Category */

$this->title = 'Редактирование категории: ' . $model->name;


?>

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


<?=
$this->render(
'_form', [
'model' => $model,
]);
?>
<?php
/*
* Страница просмотра категории, файл modules/admin/views/category/view.php
*/
use yii\helpers\Html;
use yii\widgets\DetailView;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Category */

$this->title = 'Просмотр категории: ' . $model->name;


?>

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


<p>
<?= Html::a('Изменить', ['update', 'id' => $model->id], ['class' => 'btn btn-
primary']); ?>
<?=
Html::a(
'Удалить',
['delete', 'id' => $model->id],
[
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Вы уверены, что хотите удалить категорию?',
'method' => 'post',
],
]
);
?>
</p>

<?=
DetailView::widget([
'model' => $model,
'attributes' => [
'name',
[
'attribute' => 'parent_id',
'value' => $model->getParentName()
],
'content',
'keywords',
'description',
'image',
],
]);
?>
<?php
/*
* Форма для добавления и редактирования категории, файл
modules/admin/views/category/_form.php
*/
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Category */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'parent_id')->textInput() ?>
<?= $form->field($model, 'name')->textInput(['maxlength' => true]) ?>
<?= $form->field($model, 'content')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'image')->textInput(['maxlength' => true]) ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

Работа с товарами
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;
/**
* Это модель для таблицы БД `product`
*
* @property int $id Уникальный идентификатор
* @property int $category_id Родительская категория
* @property int $brand_id Идентификатор бренда
* @property string $name Наименование товара
* @property string $content Описание товара
* @property string $price Цена товара
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
* @property string $image Имя файла изображения
* @property int $hit Лидер продаж?
* @property int $new Новый товар?
* @property int $sale Распродажа?
*/
class Product extends ActiveRecord {

/**
* Возвращает имя таблицы базы данных
*/
public static function tableName() {
return 'product';
}

/**
* Возвращает данные о родительской категории
*/
public function getCategory() {
return $this->hasOne(Category::class, ['id' => 'category_id']);
}

/**
* Возвращает наименование родительской категории
*/
public function getCategoryName() {
$parent = $this->category;
return $parent ? $parent->name : '';
}

/**
* Возвращает данные о бренде товара
*/
public function getBrand() {
return $this->hasOne(Brand::class, ['id' => 'brand_id']);
}

/**
* Возвращает наименование бренда товара
*/
public function getBrandName() {
$brand = $this->brand;
return $brand ? $brand->name : '';
}

/**
* Правила валидации полей формы при создании и редактировании товара
*/
public function rules() {
return [
[['category_id', 'brand_id', 'name'], 'required'],
[['category_id', 'brand_id', 'hit', 'new', 'sale'], 'integer'],
[['content'], 'string'],
[['price'], 'number'],
[['name', 'keywords', 'description', 'image'], 'string', 'max' => 255],
];
}

/**
* Возвращает имена полей формы для создания и редактирования товара
*/
public function attributeLabels() {
return [
'id' => 'ID',
'category_id' => 'Категория',
'brand_id' => 'Бренд',
'name' => 'Наименование',
'content' => 'Описание',
'price' => 'Цена',
'keywords' => 'Мета-тег keywords',
'description' => 'Мета-тег description',
'image' => 'Изображение',
'hit' => 'Лидер продаж',
'new' => 'Новинка',
'sale' => 'Распродажа',
];
}
}
<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Product;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* Класс ProductController реализует CRUD для товаров
*/
class ProductController extends AdminController {

public function behaviors() {


return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Список всех товаров с постраничной навигацией
*/
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Product::find(),
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Просмотр данных существующего товара
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Создание нового товара
*/
public function actionCreate() {
$model = new Product();

if ($model->load(Yii::$app->request->post()) && $model->save()) {


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

/**
* Обновление существующего товара
*/
public function actionUpdate($id) {
$model = $this->findModel($id);

if ($model->load(Yii::$app->request->post()) && $model->save()) {


return $this->redirect(['view', 'id' => $model->id]);
}

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

/**
* Удаление существующего товара
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Поиск товара по идентификатору
*/
protected function findModel($id) {
if (($model = Product::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('The requested page does not exist.');
}
}
<?php
/*
* Страница списка всех товаров, файл modules/admin/views/product/index.php
*/
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */


/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Товары каталога';


?>

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


<p>
<?= Html::a('Добавить товар', ['create'], ['class' => 'btn btn-success']); ?>
</p>

<?=
GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'name',
[
'attribute' => 'category_id',
'value' => function($data){
return $data->getCategoryName();
}
],
[
'attribute' => 'brand_id',
'value' => function($data){
return $data->getBrandName();
}
],
'price',
[
'attribute' => 'hit',
'value' => function($data) {
return $data->hit ? 'Да' : 'Нет';
}
],
[
'attribute' => 'new',
'value' => function($data) {
return $data->new ? 'Да' : 'Нет';
}
],
[
'attribute' => 'sale',
'value' => function($data) {
return $data->sale ? 'Да' : 'Нет';
}
],
['class' => 'yii\grid\ActionColumn'],
],
]);
?>
<?php
/*
* Страница добавления нового товара, файл modules/admin/views/product/create.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Product */

$this->title = 'Новый товар';


?>

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


<?=
$this->render(
'_form',
['model' => $model]
);
?>
<?php
/*
* Страница редактирования товара, файл modules/admin/views/product/update.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Product */

$this->title = 'Редактирование товара: ' . $model->name;


?>

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


<?=
$this->render(
'_form',
['model' => $model]
);
?>
<?php
/*
* Страница просмотра товара, файл modules/admin/views/product/view.php
*/
use yii\helpers\Html;
use yii\widgets\DetailView;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Product */

$this->title = 'Просмотр товара: ' . $model->name;


?>

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


<p>
<?= Html::a('Изменить', ['update', 'id' => $model->id], ['class' => 'btn btn-
primary']); ?>
<?=
Html::a(
'Удалить',
['delete', 'id' => $model->id],
[
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Вы уверены, что хотите удалить товар?',
'method' => 'post',
],
]
);
?>
</p>

<?=
DetailView::widget([
'model' => $model,
'attributes' => [
'name',
[
'attribute' => 'category_id',
'value' => $model->getCategoryName()
],
[
'attribute' => 'brand_id',
'value' => $model->getBrandName()
],
'price',
'content:html',
'keywords',
'description',
'image',
[
'attribute' => 'hit',
'value' => $model->hit ? 'Да' : 'Нет'
],
[
'attribute' => 'new',
'value' => $model->new ? 'Да' : 'Нет'
],
[
'attribute' => 'sale',
'value' => $model->sale ? 'Да' : 'Нет'
],
],
]);
?>
<?php
/*
* Форма для добавления и редактирования товара, файл
modules/admin/views/product/_form.php
*/
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Product */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'category_id')->textInput(); ?>
<?= $form->field($model, 'brand_id')->textInput(); ?>
<?= $form->field($model, 'price')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'image')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'content')->textarea(['rows' => 6]); ?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'hit')->textInput(); ?>
<?= $form->field($model, 'new')->textInput(); ?>
<?= $form->field($model, 'sale')->textInput(); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

Магазин на Yii2, часть 28. Админка: выбор родителя и


список всех категорий
Продолжим работу по приведению в порядок кода, который для нас сформировал Gii. В первую очередь
займемся созданием выпадающего списка для выбора родителя при создании и редактировании
категории или товара. Для этого добавим в класс модели Category два метода, которые будут
возвращать список всех категорий в упорядоченном виде:
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;

class Category extends ActiveRecord {

/*...*/

/**
* Возвращает массив всех категорий каталога в виде дерева
*/
public static function getAllCategories($parent = 0, $level = 0, $exclude = 0)
{
$children = self::find()
->where(['parent_id' => $parent])
->asArray()
->all();
$result = [];
foreach ($children as $category) {
// при выборе родителя категории нельзя допустить
// чтобы она размещалась внутри самой себя
if ($category['id'] == $exclude) {
continue;
}
if ($level) {
$category['name'] = str_repeat('— ', $level) . $category['name'];
}
$result[] = $category;
$result = array_merge(
$result,
self::getAllCategories($category['id'], $level+1, $exclude)
);
}
return $result;
}

/**
* Возвращает массив всех категорий каталога для возможности
* выбора родителя при добавлении или редактировании товара
* или категории
*/
public static function getTree($exclude = 0, $root = false) {
$data = self::getAllCategories(0, 0, $exclude);
$tree = [];
// при выборе родителя категории можно выбрать значение «Без родителя»,
// т.е. создать категорию верхнего уровня, у которой не будет родителя
if ($root) {
$tree[0] = 'Без родителя';
}
foreach ($data as $item) {
$tree[$item['id']] = $item['name'];
}
return $tree;
}
}

Выбор родителя для категории и товара


Теперь изменим форму добавления или редактирования категории — заменим input на select:
<?php
/*
* Форма для добавления и редактирования категории, файл
modules/admin/views/category/_form.php
*/
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Category */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?php
// при редактировании существующей категории нельзя допустить, чтобы
// в качестве родителя была выбрана эта же категория или ее потомок
$exclude = 0;
if (!empty($model->id)) {
$exclude = $model->id;
}
$parents = $model::getTree($exclude, true);
echo $form->field($model, 'parent_id')->dropDownList($parents);
?>
<?= $form->field($model, 'content')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'image')->textInput(['maxlength' => true]) ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

И вот что у нас получилось в итоге:


Аналогично, изменим форму для добавления и редактирования товара:

<?php
/*
* Форма для добавления и редактирования товара, файл
modules/admin/views/product/_form.php
*/

use app\modules\admin\models\Brand;
use app\modules\admin\models\Category;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Product */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'category_id')->dropDownList(Category::getTree()); ?>
<?= $form->field($model, 'brand_id')-
>dropDownList(ArrayHelper::map(Brand::find()->all(), 'id', 'name')); ?>
<?= $form->field($model, 'price')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'image')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'content')->textarea(['rows' => 6]); ?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'hit')->textInput(); ?>
<?= $form->field($model, 'new')->textInput(); ?>
<?= $form->field($model, 'sale')->textInput(); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>
Список всех категорий
Сейчас список всех категорий показывается с использованием провайдера
данных ActiveDataProvider и виджета GridView. Нам это не подходит, потому что категории должны
показываться с учетом вложенности. Так что изменим метод контроллера actionIndex() и view-
шаблон index.php:
<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Category;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* Класс OrderController реализует CRUD для категорий
*/
class CategoryController extends AdminController {

/*...*/

/**
* Список всех категорий каталога товаров
*/
public function actionIndex() {
return $this->render(
'index',
['categories' => Category::getAllCategories()]
);
}

/*...*/
}
<?php
/*
* Страница списка всех категорий, файл modules/admin/views/category/index.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */

$this->title = 'Категории каталога';


?>

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


<p>
<?= Html::a('Добавить категорию', ['create'], ['class' => 'btn btn-success']);
?>
</p>

<table class="table table-striped table-bordered">


<thead>
<tr>
<th>Наименование</th>
<th>Мета-тег keywords</th>
<th>Мета-тег description</th>
<th><span class="glyphicon glyphicon-eye-open"></span></th>
<th><span class="glyphicon glyphicon-pencil"></span></th>
<th><span class="glyphicon glyphicon-trash"></span></th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $category): ?>
<tr>
<td><?= $category['name']; ?></td>
<td><?= $category['keywords']; ?></td>
<td><?= $category['description']; ?></td>
<td>
<?=
Html::a(
'<span class="glyphicon glyphicon-eye-open"></span>',
['/admin/category/view', 'id' => $category['id']]
);
?>
</td>
<td>
<?=
Html::a(
'<span class="glyphicon glyphicon-pencil"></span>',
['/admin/category/update', 'id' => $category['id']]
);
?>
</td>
<td>
<?=
Html::a(
'<span class="glyphicon glyphicon-trash"></span>',
['/admin/category/delete', 'id' => $category['id']],
[
'data-confirm' => 'Вы уверены, что хотите удалить эту
категорию?',
'data-method' => 'post'
]
);
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
Магазин на Yii2, часть 29. Админка: добавляем список
товаров категории
Сейчас для действия index контроллера ProductController показывается список всех товаров
каталога. Найти в этом длинном списке нужный товар, чтобы его отредактировать, довольно
проблематично. Давайте на страницу списка всех категорий добавим еще одну ссылку, которая позволит
просмотреть список товаров каждой категории. Для этого добавим метод actionProducts() для
контроллера CategoryController:
<?php
namespace app\modules\admin\controllers;

use app\modules\admin\models\Product;
use Yii;
use app\modules\admin\models\Category;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* Класс CategoryController реализует CRUD для категорий
*/
class CategoryController extends AdminController {

/*...*/

/**
* Список всех товаров категории
*/
public function actionProducts($id) {
// получаем массив идентификаторов всех потомков категории,
// чтобы запросом выбрать товары и в дочерних категориях
$ids = Category::getAllChildIds($id);
$ids[] = $id;
$products = new ActiveDataProvider([
'query' => Product::find()->where(['in', 'category_id', $ids])
]);
return $this->render(
'products',
[
'category' => $this->findModel($id),
'products' => $products,
]
);
}

/*...*/
}

Теперь добавим ссылку для просмотра товаров категории в view-шаблон:

<?php
/*
* Страница списка всех категорий, файл modules/admin/views/category/index.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */

$this->title = 'Категории каталога';


?>
<h1><?= Html::encode($this->title); ?></h1>
<p>
<?= Html::a('Добавить категорию', ['create'], ['class' => 'btn btn-success']);
?>
</p>

<table class="table table-striped table-bordered">


<thead>
<tr>
<th>Наименование</th>
<th>Мета-тег keywords</th>
<th>Мета-тег description</th>
<th><span class="glyphicon glyphicon-list"></span></th>
<th><span class="glyphicon glyphicon-eye-open"></span></th>
<th><span class="glyphicon glyphicon-pencil"></span></th>
<th><span class="glyphicon glyphicon-trash"></span></th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $category): ?>
<tr>
<td><?= $category['name']; ?></td>
<td><?= $category['keywords']; ?></td>
<td><?= $category['description']; ?></td>
<td>
<?php
echo Html::a(
'<span class="glyphicon glyphicon-list"></span>',
['/admin/category/products', 'id' => $category['id']]
);
?>
</td>
<td>
..........
</td>
<td>
..........
</td>
<td>
..........
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
Создадим новый view-шаблон для списка товаров категории:

<?php
/*
* Страница списка товаров категории, файл
modules/admin/views/category/products.php
*/
use yii\helpers\Url;
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */


/* @var $category app\modules\admin\models\Category */
/* @var $products yii\data\ActiveDataProvider */

$this->title = 'Товары категории: ' . $category->name;


?>

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


<p>
<?=
Html::a(
'Добавить товар',
['/admin/product/create', 'category' => $category->id],
['class' => 'btn btn-success']
);
?>
</p>

<?=
GridView::widget([
'dataProvider' => $products,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'name',
[
'attribute' => 'category_id',
'value' => function($data){
return $data->getCategoryName();
}
],
[
'attribute' => 'brand_id',
'value' => function($data){
return $data->getBrandName();
}
],
'price',
[
'attribute' => 'hit',
'value' => function($data) {
return $data->hit ? 'Да' : 'Нет';
}
],
[
'attribute' => 'new',
'value' => function($data) {
return $data->new ? 'Да' : 'Нет';
}
],
[
'attribute' => 'sale',
'value' => function($data) {
return $data->sale ? 'Да' : 'Нет';
}
],
[
'class' => 'yii\grid\ActionColumn',
'urlCreator' => function ($action, $model, $key, $index) {
return Url::to(['/admin/product/'.$action, 'id' => $model->id]);
}
],
],
]);
?>

И нам еще потребуется метод в модели Category, который позволит получить идентификаторы всех
потомков категории:
<?php
namespace app\modules\admin\models;
use yii\db\ActiveRecord;

/**
* Это модель для таблицы БД `category`
*/
class Category extends ActiveRecord {

/*...*/

/**
* Возвращает массив идентификаторов всех потомков категории $id,
* т.е. дочерние, дочерние дочерних и так далее
*/
public static function getAllChildIds($id) {
$children = [];
$ids = self::getChildIds($id);
foreach ($ids as $item) {
$children[] = $item;
$c = self::getAllChildIds($item);
foreach ($c as $v) {
$children[] = $v;
}
}
return $children;
}

/**
* Возвращает массив идентификаторов дочерних категорий (прямых
* потомков) категории с уникальным идентификатором $id
*/
protected static function getChildIds($id) {
$children = self::find()->where(['parent_id' => $id])->asArray()->all();
$ids = [];
foreach ($children as $child) {
$ids[] = $child['id'];
}
return $ids;
}

/*...*/
}
Обратите внимание, что ссылка на добавление товара в шаблоне products.php имеет вид:
<p>
<?=
Html::a(
'Добавить товар',
['/admin/product/create', 'category' => $category->id],
['class' => 'btn btn-success']
);
?>
</p>

Это для того, чтобы в выпадающем списке выбора родителя сразу установить нужное значение. Вот как
это работает:

<?php
/*
* Форма для добавления и редактирования товара, файл
modules/admin/views/product/_form.php
*/

use app\modules\admin\models\Brand;
use app\modules\admin\models\Category;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Product */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?php
$category = Yii::$app->request->get('category') ?: 0;
$param = ['options' => [$category => ['selected' => true]]];
echo $form->field($model, 'category_id')->dropDownList(Category::getTree(),
$param);
?>
<?=
$form->field($model, 'brand_id')->dropDownList(
ArrayHelper::map(Brand::find()->all(), 'id', 'name')
);
?>
<?= $form->field($model, 'price')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'image')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'content')->textarea(['rows' => 6]); ?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'hit')->textInput(); ?>
<?= $form->field($model, 'new')->textInput(); ?>
<?= $form->field($model, 'sale')->textInput(); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

Магазин на Yii2, часть 30. Админка: WYSIWYG-


редактор и изображение для товара
Теперь займемся формой для добавления и редактирования товара. Установим расширение CKEditor,
чтобы добавить WYSIWYG-редактор для удобной работы с описанием товара. И организуем загрузку
картинки товара с использованием класса yii\web\UploadedFile.

WYSIWYG-редактор для товаров


Начнем с установки расширения CKEditor:

> composer require --prefer-dist mihaildev/yii2-ckeditor "*"


./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Installing mihaildev/yii2-ckeditor (1.0.1): Downloading (100%)
Package phpunit/phpunit-mock-objects is abandoned, you should avoid using it. No
replacement was suggested.
Writing lock file
Generating autoload files

Пример использования расширения:

use mihaildev\ckeditor\CKEditor;

CKEditor::widget([
'editorOptions' => [
// разработанны стандартные настройки basic, standard, full
'preset' => 'basic',
'inline' => false, // по умолчанию false
]
]);

// или c ActiveForm
echo $form->field($post, 'content')->widget(CKEditor::className(),[
'editorOptions' => [
// разработанны стандартные настройки basic, standard, full
'preset' => 'basic',
'inline' => false, // по умолчанию false
],
]);

Открываем на редактирование файл view-шаблона с формой добавления-редактироваия товара:

<?php
/*
* Форма для добавления и редактирования товара, файл
modules/admin/views/product/_form.php
*/

use mihaildev\ckeditor\CKEditor;
use app\modules\admin\models\Brand;
use app\modules\admin\models\Category;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Product */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?php
$category = Yii::$app->request->get('category') ?: 0;
$param = ['options' => [$category => ['selected' => true]]];
echo $form->field($model, 'category_id')->dropDownList(Category::getTree(),
$param);
?>
<?=
$form->field($model, 'brand_id')->dropDownList(
ArrayHelper::map(Brand::find()->all(), 'id', 'name')
);
?>
<?= $form->field($model, 'price')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'image')->textInput(['maxlength' => true]); ?>
<?=
$form->field($model, 'content')->widget(
CKEditor::class,
[
'editorOptions' => [
// разработанны стандартные настройки basic, standard, full
'preset' => 'basic',
'inline' => false, // по умолчанию false
],
]
);
?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'hit')->checkbox(); ?>
<?= $form->field($model, 'new')->checkbox(); ?>
<?= $form->field($model, 'sale')->checkbox(); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

Загрузка и резайз изображений


Добавляем два вспомогательных свойства для класса модели Product, изменяем правила валидации.
Кроме того, нам потребуются несколько новых методов — для загрузки изображения при добавлении или
обновлении товара и для удаления старого изображения.
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;
use yii\imagine\Image;

/**
* Это модель для таблицы БД `product`
*
* @property int $id Уникальный идентификатор
* @property int $category_id Родительская категория
* @property int $brand_id Идентификатор бренда
* @property string $name Наименование товара
* @property string $content Описание товара
* @property string $price Цена товара
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
* @property string $image Имя файла изображения
* @property int $hit Лидер продаж?
* @property int $new Новый товар?
* @property int $sale Распродажа?
*/
class Product extends ActiveRecord {

/**
* Вспомогательный атрибут для загрузки изображения товара
*/
public $upload;

/**
* Вспомогательный атрибут для удаления изображения товара
*/
public $remove;

/**
* Возвращает имя таблицы базы данных
*/
public static function tableName() {
return 'product';
}

/**
* Возвращает данные о родительской категории
*/
public function getCategory() {
return $this->hasOne(Category::class, ['id' => 'category_id']);
}

/**
* Возвращает наименование родительской категории
*/
public function getCategoryName() {
$parent = $this->category;
return $parent ? $parent->name : '';
}

/**
* Возвращает данные о бренде товара
*/
public function getBrand() {
return $this->hasOne(Brand::class, ['id' => 'brand_id']);
}

/**
* Возвращает наименование бренда товара
*/
public function getBrandName() {
$brand = $this->brand;
return $brand ? $brand->name : '';
}

/**
* Правила валидации полей формы при создании и редактировании товара
*/
public function rules() {
return [
[['category_id', 'brand_id', 'name', 'price'], 'required'],
[['category_id', 'brand_id', 'hit', 'new', 'sale'], 'integer'],
['content', 'string'],
['price', 'number', 'min' => 1],
[['name', 'keywords', 'description'], 'string', 'max' => 255],
// атрибут image проверяем с помощью валидатора image
['image', 'image', 'extensions' => 'png, jpg, gif'],
// вспомогательный атрибут remove помечаем как безопасный
['remove', 'safe']
];
}

/**
* Возвращает имена полей формы для создания и редактирования товара
*/
public function attributeLabels() {
return [
'id' => 'ID',
'category_id' => 'Категория',
'brand_id' => 'Бренд',
'name' => 'Наименование',
'content' => 'Описание',
'price' => 'Цена',
'keywords' => 'Мета-тег keywords',
'description' => 'Мета-тег description',
'image' => 'Изображение',
'hit' => 'Лидер продаж',
'new' => 'Новинка',
'sale' => 'Распродажа',
'remove' => 'Удалить изображение'
];
}

/**
* Загружает файл изображения товара
*/
public function uploadImage() {
if ($this->upload) { // только если был выбран файл для загрузки
$name = md5(uniqid(rand(), true)) . '.' . $this->upload->extension;
// сохраняем исходное изображение в директории source
$source = Yii::getAlias('@webroot/images/products/source/' . $name);
if ($this->upload->saveAs($source)) {
// выполняем resize, чтобы получить еще три размера
$large = Yii::getAlias('@webroot/images/products/large/' . $name);
Image::thumbnail($source, 1000, 1000)->save($large, ['quality' =>
100]);
$medium = Yii::getAlias('@webroot/images/products/medium/' .
$name);
Image::thumbnail($source, 500, 500)->save($medium, ['quality' =>
95]);
$small = Yii::getAlias('@webroot/images/products/small/' . $name);
Image::thumbnail($source, 250, 250)->save($small, ['quality' =>
90]);
return $name;
}
}
return false;
}

/**
* Удаляет старое изображение при загрузке нового
*/
public static function removeImage($name) {
if (!empty($name)) {
$source = Yii::getAlias('@webroot/images/products/source/' . $name);
if (is_file($source)) {
unlink($source);
}
$large = Yii::getAlias('@webroot/images/products/large/' . $name);
if (is_file($large)) {
unlink($large);
}
$medium = Yii::getAlias('@webroot/images/products/medium/' . $name);
if (is_file($medium)) {
unlink($medium);
}
$small = Yii::getAlias('@webroot/images/products/small/' . $name);
if (is_file($small)) {
unlink($small);
}
}
}

/**
* Удаляет изображение при удалении товара
*/
public function afterDelete() {
parent::afterDelete();
self::removeImage($this->image);
}
}
Для ресайза изображений используем расширение yii2-imagine, которое установим через Composer:
> composer require --prefer-dist yiisoft/yii2-imagine
Using version ^2.2 for yiisoft/yii2-imagine
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing imagine/imagine (1.2.2): Downloading (100%)
- Installing yiisoft/yii2-imagine (2.2.0): Downloading (100%)
imagine/imagine suggests installing ext-imagick (to use the Imagick implementation)
imagine/imagine suggests installing ext-gmagick (to use the Gmagick implementation)
Package phpunit/phpunit-mock-objects is abandoned, you should avoid using it. No
replacement was suggested.
Writing lock file
Generating autoload files
Вносим изменения в методы контроллера actionCreate() и actionUpdate():
<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Product;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\web\UploadedFile;

/**
* Класс ProductController реализует CRUD для товаров
*/
class ProductController extends AdminController {

public function behaviors() {


return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Список всех товаров с постраничной навигацией
*/
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Product::find(),
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Просмотр данных существующего товара
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Создание нового товара
*/
public function actionCreate() {
$model = new Product();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
// загружаем изображение и выполняем resize исходного изображения
$model->upload = UploadedFile::getInstance($model, 'image');
if ($name = $model->uploadImage()) { // если изображение было загружено
// сохраняем в БД имя файла изображения
$model->image = $name;
}
$model->save();
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render(
'create',
['model' => $model]
);
}

/**
* Обновление существующего товара
*/
public function actionUpdate($id) {
$model = $this->findModel($id);
// старое изображение, которое надо удалить, если загружено новое
$old = $model->image;
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
// если отмечен checkbox «Удалить изображение»
if ($model->remove) {
// удаляем старое изображение
if (!empty($old)) {
$model::removeImage($old);
}
// сохраняем в БД пустое имя
$model->image = '';
// чтобы повторно не удалять
$old = '';
} else { // оставляем старое изображение
$model->image = $old;
}
// загружаем изображение и выполняем resize исходного изображения
$model->upload = UploadedFile::getInstance($model, 'image');
if ($new = $model->uploadImage()) { // если изображение было загружено
// удаляем старое изображение
if (!empty($old)) {
$model::removeImage($old);
}
// сохраняем в БД новое имя
$model->image = $new;
}
$model->save();
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}

/**
* Удаление существующего товара
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Поиск товара по идентификатору
*/
protected function findModel($id) {
if (($model = Product::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('The requested page does not exist.');
}
}

Изменяем форму добавления и редактирования товара:

<?php
/*
* Форма для добавления и редактирования товара, файл
modules/admin/views/product/_form.php
*/

use mihaildev\ckeditor\CKEditor;
use app\modules\admin\models\Brand;
use app\modules\admin\models\Category;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Product */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?php
$category = Yii::$app->request->get('category') ?: 0;
$param = ['options' => [$category => ['selected' => true]]];
echo $form->field($model, 'category_id')->dropDownList(Category::getTree(),
$param);
?>
<?=
$form->field($model, 'brand_id')->dropDownList(
ArrayHelper::map(Brand::find()->all(), 'id', 'name')
);
?>
<?= $form->field($model, 'price')->textInput(['maxlength' => true]); ?>
<fieldset>
<legend>Загрузить изображение</legend>
<?= $form->field($model, 'image')->fileInput(); ?>
<?php
if (!empty($model->image)) {
$img = Yii::getAlias('@webroot') . '/images/products/source/' .
$model->image;
if (is_file($img)) {
$url = Yii::getAlias('@web') . '/images/products/source/' .
$model->image;
echo 'Уже загружено ', Html::a('изображение', $url, ['target' =>
'_blank']);
echo $form->field($model,'remove')->checkbox();
}
}
?>
</fieldset>
<?=
$form->field($model, 'content')->widget(
CKEditor::class,
[
'editorOptions' => [
// разработанны стандартные настройки basic, standard, full
'preset' => 'basic',
'inline' => false, // по умолчанию false
],
]
);
?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'hit')->checkbox(); ?>
<?= $form->field($model, 'new')->checkbox(); ?>
<?= $form->field($model, 'sale')->checkbox(); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>
До версии 2.0.8 для формы, которая загружает файлы, обязательно нужно было добавлять
атрибут enctype со значением multipart/form-data:
<?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]);
?>

Магазин на Yii2, часть 31. Админка: загрузка


изображений для категорий и брендов
Добавим загрузку и ресайз изображений для категорий и брендов. Здесь все по аналогии с загрузкой и
ресайзом изображений для товаров. Посколько мы это уже делали, нет смысла все подробно описывать.
Так что только исходные коды контроллеров, моделей и view-шаблонов с формой.

Загрузка изображения для категории


<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Category;
use app\modules\admin\models\Product;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\web\UploadedFile;

/**
* Класс CategoryController реализует CRUD для категорий
*/
class CategoryController extends AdminController {

public function behaviors() {


return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Список всех категорий каталога товаров
*/
public function actionIndex() {
return $this->render(
'index',
['categories' => Category::getAllCategories()]
);
}

/**
* Просмотр данных существующей категории
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Список всех товаров категории
*/
public function actionProducts($id) {
// получаем массив идентификаторов всех потомков категории,
// чтобы запросом выбрать товары и в дочерних категориях
$ids = Category::getAllChildIds($id);
$ids[] = $id;
$products = new ActiveDataProvider([
'query' => Product::find()->where(['in', 'category_id', $ids])
]);
return $this->render(
'products',
[
'category' => $this->findModel($id),
'products' => $products,
]
);
}

/**
* Создание новой категории
*/
public function actionCreate() {
$model = new Category();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
// загружаем изображение и выполняем resize исходного изображения
$model->upload = UploadedFile::getInstance($model, 'image');
if ($name = $model->uploadImage()) { // если изображение было загружено
// сохраняем в БД имя файла изображения
$model->image = $name;
}
$model->save();
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('create', [
'model' => $model,
]);
}
/**
* Обновление существующей категории
*/
public function actionUpdate($id) {
$model = $this->findModel($id);
// старое изображение, которое надо удалить, если загружено новое
$old = $model->image;
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
// загружаем изображение и выполняем resize исходного изображения
$model->upload = UploadedFile::getInstance($model, 'image');
if ($new = $model->uploadImage()) { // если изображение было загружено
// удаляем старое изображение
if (!empty($old)) {
$model::removeImage($old);
}
// сохраняем в БД новое имя
$model->image = $new;
} else { // оставляем старое изображение
$model->image = $old;
}
$model->save();
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}

/**
* Удаление существующей категории
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Поиск категории по идентификатору
*/
protected function findModel($id) {
if (($model = Category::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('The requested page does not exist.');
}
}
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;
use yii\imagine\Image;

/**
* Это модель для таблицы БД `category`
*
* @property int $id Уникальный идентификатор
* @property int $parent_id Родительская категория
* @property string $name Наименование категории
* @property string $content Описание категории
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
* @property string $image Имя файла изображения
*/
class Category extends ActiveRecord {

/**
* Вспомогательный атрибут для загрузки изображения
*/
public $upload;

/**
* Возвращает имя таблицы базы данных
*/
public static function tableName() {
return 'category';
}

/**
* Возвращает данные о родительской категории
*/
public function getParent() {
return $this->hasOne(Category::class, ['id' => 'parent_id']);
}

/**
* Возвращает наименование родительской категории
*/
public function getParentName() {
$parent = $this->parent;
return $parent ? $parent->name : '';
}

/**
* Правила валидации полей формы при создании и редактировании категории
*/
public function rules() {
return [
[['parent_id'], 'integer'],
[['name'], 'required'],
[['name', 'content', 'keywords', 'description'], 'string', 'max' =>
255],
// атрибут image проверяем с помощью валидатора image
['image', 'image', 'extensions' => 'png, jpg, gif'],
];
}

/**
* Возвращает имена полей формы для создания и редактирования категории
*/
public function attributeLabels() {
return [
'id' => 'ID',
'parent_id' => 'Родитель',
'name' => 'Наименование',
'content' => 'Описание',
'keywords' => 'Мета-тег keywords',
'description' => 'Мета-тег description',
'image' => 'Изображение'
];
}

/**
* Возвращает массив всех категорий каталога в виде дерева
*/
public static function getAllCategories($parent = 0, $level = 0, $exclude = 0)
{
$children = self::find()
->where(['parent_id' => $parent])
->asArray()
->all();
$result = [];
foreach ($children as $category) {
// при выборе родителя категории нельзя допустить
// чтобы она размещалась внутри самой себя
if ($category['id'] == $exclude) {
continue;
}
if ($level) {
$category['name'] = str_repeat('— ', $level) . $category['name'];
}
$result[] = $category;
$result = array_merge(
$result,
self::getAllCategories($category['id'], $level+1, $exclude)
);
}
return $result;
}

/**
* Возвращает массив всех категорий каталога для возможности
* выбора родителя при добавлении или редактировании товара
* или категории
*/
public static function getTree($exclude = 0, $root = false) {
$data = self::getAllCategories(0, 0, $exclude);
$tree = [];
// при выборе родителя категории можно выбрать значение
// «Без родителя» — это будет категория верхнего уровня
if ($root) {
$tree[0] = 'Без родителя';
}
foreach ($data as $item) {
$tree[$item['id']] = $item['name'];
}
return $tree;
}

/**
* Возвращает массив идентификаторов всех потомков категории $id,
* т.е. дочерние, дочерние дочерних и так далее
*/
public static function getAllChildIds($id) {
$children = [];
$ids = self::getChildIds($id);
foreach ($ids as $item) {
$children[] = $item;
$c = self::getAllChildIds($item);
foreach ($c as $v) {
$children[] = $v;
}
}
return $children;
}

/**
* Возвращает массив идентификаторов дочерних категорий (прямых
* потомков) категории с уникальным идентификатором $id
*/
protected static function getChildIds($id) {
$children = self::find()->where(['parent_id' => $id])->asArray()->all();
$ids = [];
foreach ($children as $child) {
$ids[] = $child['id'];
}
return $ids;
}

/**
* Загружает файл изображения категории
*/
public function uploadImage() {
if ($this->upload) { // только если был выбран файл для загрузки
$name = md5(uniqid(rand(), true)) . '.' . $this->upload->extension;
// сохраняем исходное изображение в директории source
$source = Yii::getAlias('@webroot/images/categories/source/' . $name);
if ($this->upload->saveAs($source)) {
// выполняем resize, чтобы получить маленькое изображение
$thumb = Yii::getAlias('@webroot/images/categories/thumb/' .
$name);
Image::thumbnail($source, 250, 250)->save($thumb, ['quality' =>
90]);
return $name;
}
}
return false;
}

/**
* Удаляет старое изображение при загрузке нового
*/
public static function removeImage($name) {
if (!empty($name)) {
$source = Yii::getAlias('@webroot/images/categories/source/' . $name);
if (is_file($source)) {
unlink($source);
}
$thumb = Yii::getAlias('@webroot/images/categories/thumb/' . $name);
if (is_file($thumb)) {
unlink($thumb);
}
}
}

/**
* Удаляет изображение при удалении категории
*/
public function afterDelete() {
parent::afterDelete();
self::removeImage($this->image);
}
}
<?php
/*
* Форма для добавления и редактирования категории, файл
modules/admin/views/category/_form.php
*/
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Category */
/* @var $form yii\widgets\ActiveForm */
?>
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?php
// при редактировании существующей категории нельзя допустить, чтобы
// в качестве родителя была выбрана эта же категория или ее потомок
$exclude = 0;
if (!empty($model->id)) {
$exclude = $model->id;
}
$parents = $model::getTree($exclude, true);
echo $form->field($model, 'parent_id')->dropDownList($parents);
?>
<fieldset>
<legend>Загрузить изображение</legend>
<?= $form->field($model, 'image')->fileInput(); ?>
<?php
if (!empty($model->image)) {
$img = Yii::getAlias('@webroot') . '/images/categories/source/' .
$model->image;
if (is_file($img)) {
$url = Yii::getAlias('@web') . '/images/categories/source/' .
$model->image;
echo 'Уже загружено ', Html::a('изображение', $url, ['target' =>
'_blank']);
}
}
?>
</fieldset>
<?= $form->field($model, 'content')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>
Загрузка изображения для бренда
<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Brand;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\web\UploadedFile;

/**
* Класс OrderController реализует CRUD для брендов
*/
class BrandController extends AdminController {

public function behaviors() {


return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Список всех брендов с постраничной навигацией
*/
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Brand::find(),
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Просмотр данных существующего бренда
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Создание нового бренда
*/
public function actionCreate() {
$model = new Brand();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
// загружаем изображение и выполняем resize исходного изображения
$model->upload = UploadedFile::getInstance($model, 'image');
if ($name = $model->uploadImage()) { // если изображение было загружено
// сохраняем в БД имя файла изображения
$model->image = $name;
}
$model->save();
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('create', [
'model' => $model,
]);
}

/**
* Обновление существующего бренда
*/
public function actionUpdate($id) {
$model = $this->findModel($id);
// старое изображение, которое надо удалить, если загружено новое
$old = $model->image;
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
// загружаем изображение и выполняем resize исходного изображения
$model->upload = UploadedFile::getInstance($model, 'image');
if ($new = $model->uploadImage()) { // если изображение было загружено
// удаляем старое изображение
if (!empty($old)) {
$model::removeImage($old);
}
// сохраняем в БД новое имя
$model->image = $new;
} else { // оставляем старое изображение
$model->image = $old;
}
$model->save();
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}

/**
* Удаление существующего бренда
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Поиск бренда по идентификатору
*/
protected function findModel($id) {
if (($model = Brand::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('The requested page does not exist.');
}
}
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;
use yii\imagine\Image;

/**
* Это модель для таблицы БД `brand`
*
* @property int $id Уникальный идентификатор
* @property string $name Наименование бренда
* @property string $content Описание бренда
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
* @property string $image Имя файла изображения
*/
class Brand extends ActiveRecord {

/**
* Вспомогательный атрибут для загрузки изображения
*/
public $upload;

/**
* Возвращает имя таблицы базы данных
*/
public static function tableName() {
return 'brand';
}

/**
* Правила валидации полей формы при создании и редактировании бренда
*/
public function rules() {
return [
[['name'], 'required'],
[['name', 'content', 'keywords', 'description'], 'string', 'max' =>
255],
// атрибут image проверяем с помощью валидатора image
['image', 'image', 'extensions' => 'png, jpg, gif'],
];
}

/**
* Возвращает имена полей формы для создания и редактирования бренда
*/
public function attributeLabels() {
return [
'id' => 'ID',
'name' => 'Наименование',
'content' => 'Описание',
'keywords' => 'Мета-тег keywords',
'description' => 'Мета-тег description',
'image' => 'Изображение',
];
}

/**
* Загружает файл изображения бренда
*/
public function uploadImage() {
if ($this->upload) { // только если был выбран файл для загрузки
$name = md5(uniqid(rand(), true)) . '.' . $this->upload->extension;
// сохраняем исходное изображение в директории source
$source = Yii::getAlias('@webroot/images/brands/source/' . $name);
if ($this->upload->saveAs($source)) {
// выполняем resize, чтобы получить маленькое изображение
$thumb = Yii::getAlias('@webroot/images/brands/thumb/' . $name);
Image::thumbnail($source, 250, 250)->save($thumb, ['quality' =>
90]);
return $name;
}
}
return false;
}
/**
* Удаляет старое изображение при загрузке нового
*/
public static function removeImage($name) {
if (!empty($name)) {
$source = Yii::getAlias('@webroot/images/brands/source/' . $name);
if (is_file($source)) {
unlink($source);
}
$thumb = Yii::getAlias('@webroot/images/brands/thumb/' . $name);
if (is_file($thumb)) {
unlink($thumb);
}
}
}

/**
* Удаляет изображение при удалении бренда
*/
public function afterDelete() {
parent::afterDelete();
self::removeImage($this->image);
}
}
<?php
/*
* Форма для добавления и редактирования бренда, файл
modules/admin/views/brand/_form.php
*/
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Brand */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<fieldset>
<legend>Загрузить изображение</legend>
<?= $form->field($model, 'image')->fileInput(); ?>
<?php
if (!empty($model->image)) {
$img = Yii::getAlias('@webroot') . '/images/brands/source/' . $model-
>image;
if (is_file($img)) {
$url = Yii::getAlias('@web') . '/images/brands/source/' . $model-
>image;
echo 'Уже загружено ', Html::a('изображение', $url, ['target' =>
'_blank']);
}
}
?>
</fieldset>
<?= $form->field($model, 'content')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

Магазин на Yii2, часть 32. Админка: удаление


категорий и CRUD для страниц
Перед удалением категории нужно выполнить две проверки. Первая — что категория не содержит
товары. Вторая — что категория не имеет дочерних категорий. Если хотя бы одно условие ложно,
категорию удалять нельзя. Добавим метод beforeDelete() в класс модели Category:
class Category extends ActiveRecord {
/*...*/

/**
* Проверка перед удалением категории
*/
public function beforeDelete() {
$children = self::find()->where(['parent_id' => $this->id])->all();
$products = Product::find()->where(['category_id' => $this->id])->all();
if (!empty($children) || !empty($products)) {
Yii::$app->session->setFlash(
'warning',
'Нельзя удалить категорию, которая имеет товары или дочерние
категории'
);
return false;
}
return parent::beforeDelete();
}
/*...*/
}

Мы записываем в сессию сообщение об ошибке, которое покажем в layout-шаблоне:

<?php
/*
* Layout-шаблон, файл modules/views/layouts/main.php
*/

/* @var $this \yii\web\View */


/* @var $content string */

use yii\helpers\Html;
use yii\bootstrap\Nav;
use yii\bootstrap\NavBar;
use yii\helpers\Url;
use app\assets\AppAsset;

AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
<meta charset="<?= Yii::$app->charset ?>">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php $this->registerCsrfMetaTags() ?>
<title><?= Html::encode($this->title) ?> | Панель управления</title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>

<header>
<!-- .......... -->
</header>

<div class="container">
<?php if (Yii::$app->session->hasFlash('warning')): ?>
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close"
data-dismiss="alert" aria-label="Закрыть">
<span aria-hidden="true">&times;</span>
</button>
<p><?= Yii::$app->session->getFlash('warning'); ?></p>
</div>
<?php endif; ?>
<?= $content; ?>
</div>

<footer class="footer">
<!-- .......... -->
</footer>

<?php $this->endBody() ?>


</body>
</html>
<?php $this->endPage() ?>
По аналогии с категориями, добавим метод beforeDelete() в класс модели Brand:
class Brand extends ActiveRecord {
/*...*/

/**
* Проверка перед удалением бренда
*/
public function beforeDelete() {
$products = Product::find()->where(['brand_id' => $this->id])->all();
if (!empty($products)) {
Yii::$app->session->setFlash(
'warning',
'Нельзя удалить бренд, у которого есть товары'
);
return false;
}
return parent::beforeDelete();
}
/*...*/
}

Создаем модель и CRUD-контроллер для страниц


У нас по дизайну предусмотрено несколько страниц — «Доставка», «Оплата», «Контакты». Но пока нет
возможности добавить эти страницы и динамически показывать список этих страниц в публичной части.
Давайте это исправим — создадим таблицу базы данных page, создадим для нее класс модели и
возможность выполнения CRUD-операций над страницами.
CREATE TABLE `page` (
`id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Уникальный
идентификатор',
`parent_id` int(10) UNSIGNED NOT NULL COMMENT 'Родительская страница',
`name` varchar(100) NOT NULL COMMENT 'Заголовок страницы',
`slug` varchar(100) NOT NULL UNIQUE KEY COMMENT 'Для создания ссылки',
`content` text COMMENT 'Содержимое страницы',
`keywords` varchar(255) DEFAULT NULL COMMENT 'Мета-тег keywords',
`description` varchar(255) DEFAULT NULL COMMENT 'Мета-тег description'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Как обычно, воспользуемся генератором кода Gii. Переходим по ссылке «Model Generator», задаем имя
таблицы БД, имя класса модели и пространство имен:

Table Name: page

Model Class Name: Page

Namespace: app\modules\admin\models
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;

/**
* This is the model class for table "page".
*
* @property int $id Уникальный идентификатор
* @property int $parent_id Родительская страница
* @property string $name Заголовок страницы
* @property string $slug Для создания ссылки
* @property string $content Содержимое страницы
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
*/
class Page extends ActiveRecord {
/**
* {@inheritdoc}
*/
public static function tableName() {
return 'page';
}

/**
* {@inheritdoc}
*/
public function rules() {
return [
[['parent_id', 'name', 'slug'], 'required'],
[['parent_id'], 'integer'],
[['content'], 'string'],
[['name', 'slug'], 'string', 'max' => 100],
[['keywords', 'description'], 'string', 'max' => 255],
[['slug'], 'unique'],
];
}

/**
* {@inheritdoc}
*/
public function attributeLabels() {
return [
'id' => 'ID',
'parent_id' => 'Parent',
'name' => 'Name',
'slug' => 'Slug',
'content' => 'Content',
'keywords' => 'Keywords',
'description' => 'Description',
];
}
}

Теперь используем «CRUD Generator», который создаст нам контроллер и view-шаблоны. И мы получим
готовой код для создания, просмотра, редактирования и удаления страниц.

Model Class: app\modules\admin\models\Page

Controller Class: app\modules\admin\controllers\PageController

View Path: @app/modules/admin/views/page

Base Controller Class: app\modules\admin\controllers\AdminController


<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Page;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* PageController implements the CRUD actions for Page model.
*/
class PageController extends AdminController {
/**
* {@inheritdoc}
*/
public function behaviors() {
return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}
/**
* Lists all Page models.
* @return mixed
*/
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Page::find(),
]);

return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Displays a single Page model.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Creates a new Page model.
* If creation is successful, the browser will be redirected to the 'view'
page.
* @return mixed
*/
public function actionCreate() {
$model = new Page();

if ($model->load(Yii::$app->request->post()) && $model->save()) {


return $this->redirect(['view', 'id' => $model->id]);
}

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

/**
* Updates an existing Page model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id) {
$model = $this->findModel($id);

if ($model->load(Yii::$app->request->post()) && $model->save()) {


return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}

/**
* Deletes an existing Page model.
* If deletion is successful, the browser will be redirected to the 'index'
page.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Finds the Page model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be thrown.
* @param integer $id
* @return Page the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id) {
if (($model = Page::findOne($id)) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not exist.');


}
}

Магазин на Yii2, часть 33. Админка: приводим в


порядок CRUD-код для страниц
Код для работы со страницами уже работает, но нуждается в некоторой доработке. Нужно предоставить
возможность выбора родителя для страницы, прикрутить WYSIWYG-редактор, добавить валидатор для
slug, изменить надписи на кнопках и так далее. Все это мы уже делали для товаров, категорий и брендов,
так что без подробностей.

Модель и контроллер
<?php
namespace app\modules\admin\models;

use Yii;
use yii\db\ActiveRecord;

/**
* Это модель для таблицы БД `page`
*
* @property int $id Уникальный идентификатор
* @property int $parent_id Родительская страница
* @property string $name Заголовок страницы
* @property string $slug Для создания ссылки
* @property string $content Содержимое страницы
* @property string $keywords Мета-тег keywords
* @property string $description Мета-тег description
*/
class Page extends ActiveRecord {

/**
* Возвращает имя таблицы базы данных
*/
public static function tableName() {
return 'page';
}

/**
* Возвращает данные о родительской странице
*/
public function getParent() {
return $this->hasOne(Page::class, ['id' => 'parent_id']);
}

/**
* Возвращает наименование родительской страницы
*/
public function getParentName() {
$parent = $this->parent;
return $parent ? $parent->name : '';
}

/**
* Возвращает массив страниц верхнего уровня для
* возможности выбора родителя
*/
public static function getRootPages($exclude = 0) {
$parents = [0 => 'Без родителя'];
$root = Page::find()->where(['parent_id' => 0])->all();
foreach ($root as $item) {
if ($exclude == $item['id']) {
continue;
}
$parents[$item['id']] = $item['name'];
}
return $parents;
}

/**
* Правила валидации полей формы при создании и редактировании страницы
*/
public function rules() {
return [
[['parent_id', 'name', 'slug'], 'required'],
['parent_id', 'integer'],
// не должно быть двух страниц с одинаковым slug
['slug', 'unique'],
// slug может содержать только латиницу, цифры, дефис и подчеркивание
['slug', 'match', 'pattern' => '/^[a-z][-_a-z0-9]*$/i'],
['content', 'string'],
[['name', 'slug'], 'string', 'max' => 100],
[['keywords', 'description'], 'string', 'max' => 255],
];
}

/**
* Возвращает имена полей формы для создания и редактирования страницы
*/
public function attributeLabels() {
return [
'id' => 'ID',
'parent_id' => 'Родитель',
'name' => 'Заголовок',
'slug' => 'Для создания ссылки',
'content' => 'Содержимое',
'keywords' => 'Мета-тег keywords',
'description' => 'Мета-тег description',
];
}
}
<?php
namespace app\modules\admin\controllers;

use Yii;
use app\modules\admin\models\Page;
use yii\data\ActiveDataProvider;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;

/**
* PageController implements the CRUD actions for Page model.
*/
class PageController extends AdminController {

public function behaviors() {


return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}

/**
* Список всех страниц сайта
*/
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Page::find(),
]);

return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

/**
* Просмотр данных существующей страницы
*/
public function actionView($id) {
return $this->render('view', [
'model' => $this->findModel($id),
]);
}

/**
* Создание новой страницы сайта
*/
public function actionCreate() {
$model = new Page();

if ($model->load(Yii::$app->request->post()) && $model->save()) {


return $this->redirect(['view', 'id' => $model->id]);
}

return $this->render('create', [
'model' => $model,
]);
}
/**
* Обновление существующей страницы
*/
public function actionUpdate($id) {
$model = $this->findModel($id);

if ($model->load(Yii::$app->request->post()) && $model->save()) {


return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}

/**
* Удаление существующей страницы
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}

/**
* Поиск страницы по идентификатору
*/
protected function findModel($id) {
if (($model = Page::findOne($id)) !== null) {
return $model;
}

throw new NotFoundHttpException('The requested page does not exist.');


}
}

View-шаблоны для страниц


<?php
/*
* Страница списка всех страниц, файл modules/admin/views/page/index.php
*/
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */


/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Все страницы';


?>

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


<p>
<?= Html::a('Добавить страницу', ['create'], ['class' => 'btn btn-success']);
?>
</p>

<?=
GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'name',
'slug',
[
'attribute' => 'parent_id',
'value' => function($data) {
$parent = $data->getParentName();
return empty($parent) ? 'Без родителя' : $parent;
}
],
[
'attribute' => 'keywords',
'value' => function($data) {
return empty($data->keywords) ? 'Не задано' : $data->keywords;
}
],
[
'attribute' => 'description',
'value' => function($data) {
return empty($data->description) ? 'Не задано' : $data-
>description;
}
],
['class' => 'yii\grid\ActionColumn'],
],
]);
?>

<?php
/*
* Страница просмотра страницы, файл modules/admin/views/page/view.php
*/
use yii\helpers\Html;
use yii\widgets\DetailView;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Page */

$this->title = 'Просмотр страницы: ' . $model->name;


?>

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

<p>
<?= Html::a('Изменить', ['update', 'id' => $model->id], ['class' => 'btn btn-
primary']) ?>
<?= Html::a('Удалить', ['delete', 'id' => $model->id], [
'class' => 'btn btn-danger',
'data' => [
'confirm' => 'Вы уверены, что хотите удалить эту страницу?',
'method' => 'post',
],
]) ?>
</p>

<?=
DetailView::widget([
'model' => $model,
'attributes' => [
'name',
'slug',
[
'attribute' => 'parent_id',
'value' => $model->getParentName()
],
[
'attribute' => 'content',
'value' => empty($model->content) ? 'Не задано' : $model->content,
'format' => 'html'
],
[
'attribute' => 'keywords',
'value' => empty($model->keywords) ? 'Не задано' : $model->keywords
],
[
'attribute' => 'description',
'value' => empty($model->description) ? 'Не задано' : $model-
>description
],
],
]);
?>

<?php
/*
* Страница добавления новой страницы, файл modules/admin/views/page/create.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Page */

$this->title = 'Добавить страницу';


?>

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


<?=
$this->render(
'_form',
['model' => $model]
);
?>

<?php
/*
* Страница редактирования страницы, файл modules/admin/views/page/update.php
*/
use yii\helpers\Html;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Page */

$this->title = 'Редактирование страницы: ' . $model->name;


?>

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


<?=
$this->render(
'_form',
['model' => $model]
);
?>
<?php
/*
* Форма для добавления и редактирования страницы, файл
modules/admin/views/page/_form.php
*/
use app\modules\admin\models\Page;
use mihaildev\ckeditor\CKEditor;
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Page */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'slug')->textInput(['maxlength' => true]); ?>
<?php
// при редактировании существующей страницы нельзя допустить,
// чтобы в качестве родителя была выбрана эта же страница
$exclude = 0;
if (!empty($model->id)) {
$exclude = $model->id;
}
echo $form->field($model, 'parent_id')-
>dropDownList(Page::getRootPages($exclude));
?>
<?=
$form->field($model, 'content')->widget(
CKEditor::class,
[
'editorOptions' => [
// разработанны стандартные настройки basic, standard, full
'preset' => 'basic',
'inline' => false, // по умолчанию false
],
]
);
?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

Магазин на Yii2, часть 34. Показываем меню страниц в


публичной части
Список всех страниц сайта
Сейчас для показа всех страниц в панели управления используется класс ActiveDataProvider и
виджет GridView. Нам это не подходит, потому что страницы надо показывать с учетом иерархии. По
аналогии с категориями каталога изменим метод контроллера actionIndex() и view-шаблон index.php.
class PageController extends AdminController {
/*...*/
public function actionIndex() {
return $this->render(
'index',
['pages' => Page::getTree()]
);
}
/*...*/
}
class Page extends ActiveRecord {
/*...*/
public static function getTree($parent = 0) {
$children = self::find()
->where(['parent_id' => $parent])
->asArray()
->all();
$result = [];
foreach ($children as $page) {
if ($parent) {
$page['name'] = '— ' . $page['name'];
}
$result[] = $page;
$result = array_merge(
$result,
self::getTree($page['id'])
);
}
return $result;
}
/*...*/
}
<?php
/*
* Страница списка всех страниц, файл modules/admin/views/page/index.php
*/
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */


/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Все страницы';


?>

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


<p>
<?= Html::a('Добавить страницу', ['create'], ['class' => 'btn btn-success']);
?>
</p>

<table class="table table-striped table-bordered">


<thead>
<tr>
<th>Наименование</th>
<th>Мета-тег keywords</th>
<th>Мета-тег description</th>
<th><span class="glyphicon glyphicon-eye-open"></span></th>
<th><span class="glyphicon glyphicon-pencil"></span></th>
<th><span class="glyphicon glyphicon-trash"></span></th>
</tr>
</thead>
<tbody>
<?php foreach ($pages as $page): ?>
<tr>
<td><?= $page['name']; ?></td>
<td><?= $page['keywords']; ?></td>
<td><?= $page['description']; ?></td>
<td>
<?=
Html::a(
'<span class="glyphicon glyphicon-eye-open"></span>',
['/admin/page/view', 'id' => $page['id']]
);
?>
</td>
<td>
<?=
Html::a(
'<span class="glyphicon glyphicon-pencil"></span>',
['/admin/page/update', 'id' => $page['id']]
);
?>
</td>
<td>
<?=
Html::a(
'<span class="glyphicon glyphicon-trash"></span>',
['/admin/page/delete', 'id' => $page['id']],
[
'data-confirm'=> 'Вы уверены, что хотите удалить эту
страницу?',
'data-method'=> 'post'
]
);
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
Проверка перед удалением страницы
Перед удалением страницы необходимо проверить, что у нее нет дочерних страниц. Поэтому добавим в
модель метод beforeDelete():
class Page extends ActiveRecord {
/*...*/
public function beforeDelete() {
$children = self::find()->where(['parent_id' => $this->id])->all();
if (!empty($children)) {
Yii::$app->session->setFlash(
'warning',
'Нельзя удалить страницу, которая имеет дочерние стрницы'
);
return false;
}
return parent::beforeDelete();
}
/*...*/
}

Показываем меню в публичной части


Для этого надо получить список всех страниц и передать эти данные в layout-шаблон. Добавим в
класс AppController переменную $pageMenu и метод beforeAction() для решения этой задачи:
class AppController extends Controller {
/*...*/
public $pageMenu;
/*...*/
public function beforeAction($action) {
$this->pageMenu = Page::getTree();
return parent::beforeAction($action);
}
/*...*/
}

Еще нам потребуется модель для страниц:


<?php
namespace app\models;

use Yii;
use yii\db\ActiveRecord;

class Page extends ActiveRecord {

/**
* Метод возвращает имя таблицы БД
*/
public static function tableName() {
return 'page';
}

/**
* Метод возвращает все страницы в виде дерева
*/
public static function getTree() {
// пробуем извлечь данные из кеша
$data = Yii::$app->cache->get('page-menu');
if ($data === false) {
// данных нет в кеше, получаем их заново
$pages = Page::find()
->select(['id', 'name', 'slug', 'parent_id'])
->indexBy('id')
->asArray()
->all();
$data = self::makeTree($pages);
// сохраняем полученные данные в кеше
Yii::$app->cache->set('page-menu', $data, 60);
}
return $data;
}

/**
* Принимает на вход линейный массив элеменов, связанных отношениями
* parent-child, и возвращает массив в виде дерева
*/
protected static function makeTree($data = []) {
if (count($data) == 0) {
return [];
}
$tree = [];
foreach ($data as $id => &$node) {
if ($node['parent_id'] == 0) {
$tree[$id] = &$node;
} else {
$data[$node['parent_id']]['childs'][$id] = &$node;
}
}
return $tree;
}
}

И изменяем layout-шаблон:

<?php

/* @var $this \yii\web\View */


/* @var $content string */

use yii\helpers\Html;
use yii\helpers\Url;
use app\assets\AppAsset;
use yii\bootstrap\Modal;

AppAsset::register($this);
?>
<?php $this->beginPage(); ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language; ?>">
<head>
<meta charset="<?= Yii::$app->charset; ?>">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<?php $this->registerCsrfMetaTags(); ?>
<title><?= Html::encode($this->title); ?></title>
<?php $this->head(); ?>
</head>

<body>
<?php $this->beginBody(); ?>
<header>

<div class="header-top">
<div class="container">
<div class="row">
<div class="col-sm-6">
<ul class="nav nav-pills">
<li><a href="#"><i class="fa fa-phone"></i> +2 95 01 88
821</a></li>
<li><a href="#"><i class="fa fa-envelope"></i>
info@domain.com</a></li>
</ul>
</div>
<div class="col-sm-6">
<ul class="nav nav-pills pull-right">
<li><a href="#"><i class="fa fa-facebook"></i></a></li>
<li><a href="#"><i class="fa fa-twitter"></i></a></li>
<li><a href="#"><i class="fa fa-linkedin"></i></a></li>
<li><a href="#"><i class="fa fa-dribbble"></i></a></li>
<li><a href="#"><i class="fa fa-google-plus"></i></a></li>
</ul>
</div>
</div>
</div>
</div>

<div class="header-middle">
<div class="container">
<div class="row">
<div class="col-sm-4">
<div class="pull-left">
<a href="<?= Url::home(); ?>">
<?=
Html::img(
'@web/images/home/logo.png',
['alt' => Yii::$app->params['shopName']]
);
?>
</a>
</div>
</div>
<div class="col-sm-8">
<ul class="pull-right">
<li><i class="fa fa-user"></i> <a href="#">Аккаунт</a></li>
<li><i class="fa fa-star"></i> <a
href="#">Избранное</a></li>
<li><i class="fa fa-crosshairs"></i> <a
href="#">Оформить</a></li>
<li>
<i class="fa fa-shopping-cart"></i>
<a href="<?= Url::to(['basket/index']); ?>">Корзина</a>
</li>
<li><i class="fa fa-lock"></i> <a href="#">Войти</a></li>
</ul>
</div>
</div>
</div>
</div>

<div class="header-bottom">
<div class="container">
<div class="row">
<div class="col-sm-8">
<div id="menu">
<ul>
<li>
<a href="<?= Url::to(['catalog/index']); ?>">
Каталог
</a>
</li>
<?php foreach ($this->context->pageMenu as $page): ?>
<li>
<a href="<?= Url::to(['page/view', 'slug' =>
$page['slug']]); ?>">
<?= $page['name']; ?>
</a>
<?php if (isset($page['childs'])): ?>
<ul>
<?php foreach ($page['childs'] as $child):
?>
<li>
<a href="<?= Url::to(['page/view',
'slug' => $child['slug']]); ?>">
<?= $child['name']; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<div class="col-sm-4">
<form method="post" action="<?= Url::to(['catalog/search']);
?>" class="pull-right">
<?=
Html::hiddenInput(
Yii::$app->request->csrfParam,
Yii::$app->request->csrfToken
);
?>
<div class="input-group">
<input type="text" name="query" class="form-control"
placeholder="Поиск по каталогу">
<div class="input-group-btn">
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-
search"></span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

</header>

<?= $content ?>

<footer>
<div class="container">
Copyright © 2018 E-SHOPPER Inc. All rights reserved.
</div>
</footer>

<?php
$checkout = Url::to(['order/checkout']);
$footer =
<<<FOOTER
<button type="button" class="btn btn-default" data-dismiss="modal">
Продолжить покупки
</button>
<a href="$checkout" class="btn btn-warning">
Оформить заказ
</a>
FOOTER;
Modal::begin([
'header' => '<h2>Корзина</h2>',
'id' => 'basket-modal',
'size'=>'modal-lg',
'footer' => $footer
]);
Modal::end();
unset($checkout, $footer);
?>

<?php $this->endBody(); ?>


</body>
</html>
<?php $this->endPage(); ?>
Показываем страницу сайта
Для начала изменим настройки компонента UrlManager:
/*...*/
$config = [
/*...*/
'components' => [
/*...*/
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
// раздел каталога: 2, 3, 4 страница списка товаров
'catalog/category/<id:\d+>/page/<page:\d+>' => 'catalog/category',
// раздел каталога: первая страница списка товаров
'catalog/category/<id:\d+>' => 'catalog/category',
// бренд каталога: 2, 3, 4 страница списка товаров
'catalog/brand/<id:\d+>/page/<page:\d+>' => 'catalog/brand',
// бренд каталога: первая страница списка товаров
'catalog/brand/<id:\d+>' => 'catalog/brand',
// страница отдельного товара каталога
'catalog/product/<id:\d+>' => 'catalog/product',
// правило для 2, 3, 4 страницы результатов поиска
'catalog/search/query/<query:.*?>/page/<page:\d+>' =>
'catalog/search',
// правило для первой страницы результатов поиска
'catalog/search/query/<query:.*?>' => 'catalog/search',
// правило для первой страницы с пустым запросом
'catalog/search' => 'catalog/search',
// страница сайта
'/page/<slug:[-_0-9a-zA-Z]+>/' => 'page/view'
],
],
/*...*/
],
/*...*/
];
/*...*/
Добавим метод actionView() в контроллер — он будет отвечать за показ страницы:
class PageController extends AppController {

/*
* Главная страница сайта
*/
public function actionIndex() {
/*...*/
}

/*
* Произвольная страница сайта
*/
public function actionView($slug) {
if ($page = Page::find()->where(['slug' => $slug])->one()) {
$this->setMetaTags(
$page->name,
$page->keywords,
$page->description
);
return $this->render(
'view',
['page' => $page]
);
}
throw new NotFoundHttpException('Запрошенная страница не найдена');
}
}

И последнее — создадим view-шаблон для показа отдельной страницы сайта:

<?php
/*
* Произвольная страница сайта, файл views/page/view.php
*/

use app\components\TreeWidget;
use app\components\BrandsWidget;
?>

<section>
<div class="container">
<div class="row">
<div class="col-sm-3">
<h2>Каталог</h2>
<div class="category-products">
<?= TreeWidget::widget(); ?>
</div>

<h2>Бренды</h2>
<div class="brand-products">
<?= BrandsWidget::widget(); ?>
</div>
</div>

<div class="col-sm-9">
<h1><?= $page['name']; ?></h1>
<?= $page['content']; ?>
</div>
</div>
</div>
</section>
Магазин на Yii2, часть 35. Админка: загрузка картинок
для страниц и страница 404
При редактировании с помощью WYSIWYG-редактора страницы сайта может возникнуть необходимость
загрузки изображений, так что установим файловый менеджер ELFinder. Кроме того, создадим
отдельную страницу 404 Not Found для панели управления, потому что сейчас используется страница
404 общедоступной части сайта, что не очень удобно.

> composer require --prefer-dist mihaildev/yii2-elfinder "*"


./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
- Installing studio-42/elfinder (2.1.50): Downloading (100%)
- Installing bower-asset/jquery-ui (1.12.1): Loading from cache
- Installing yiisoft/yii2-jui (2.0.7): Loading from cache
- Installing mihaildev/yii2-elfinder (1.3.0): Downloading (100%)
studio-42/elfinder suggests installing kunalvarma05/dropbox-php-sdk (VolumeDriver
`Dropbox`2 require `kunalvarma05/dropbox-php-sdk.)
studio-42/elfinder suggests installing google/apiclient (VolumeDriver GoogleDrive
require `google/apiclient:^2.0.)
studio-42/elfinder suggests installing barryvdh/elfinder-flysystem-driver
(VolumeDriver for elFinder to use Flysystem as a root.)
studio-42/elfinder suggests installing nao-pon/flysystem-google-drive (require in
GoogleDrive network volume mounting with Flysystem.)
Package phpunit/phpunit-mock-objects is abandoned, you should avoid using it. No
replacement was suggested.
Writing lock file
Generating autoload files
Расширение установлено. Теперь подключим и настроим его согласно инструкции. Для начала откроем
файл config/web.php и добавим в него следующий код:
/*...*/
$config = [
/*...*/
'components' => [
/*...*/
],
'controllerMap' => [
'elfinder' => [
'class' => 'mihaildev\elfinder\PathController',
'access' => ['?'], // доступ для всех
'root' => [
'path' => 'images/pages', // директория внутри web
'name' => 'Изображения'
],
]
],
'params' => $params,
];
/*...*/
Создадим директорию web/images/pages, куда будут загружаться изображения и изменим код вызова
WYSIWYG-редактора:
<?php
/*
* Форма для добавления и редактирования страницы, файл
modules/admin/views/page/_form.php
*/
use app\modules\admin\models\Page;
use mihaildev\ckeditor\CKEditor;
use mihaildev\elfinder\ElFinder;
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */


/* @var $model app\modules\admin\models\Page */
/* @var $form yii\widgets\ActiveForm */
?>

<?php $form = ActiveForm::begin(); ?>


<?= $form->field($model, 'name')->textInput(['maxlength' => true]); ?>
<?= $form->field($model, 'slug')->textInput(['maxlength' => true]); ?>
<?php
// при редактировании существующей страницы нельзя допустить,
// чтобы в качестве родителя была выбрана эта же страница
$exclude = 0;
if (!empty($model->id)) {
$exclude = $model->id;
}
echo $form->field($model, 'parent_id')-
>dropDownList(Page::getRootPages($exclude));
?>
<?=
/*
$form->field($model, 'content')->widget(
CKEditor::class,
[
'editorOptions' => [
// разработанны стандартные настройки basic, standard, full
'preset' => 'basic',
'inline' => false, // по умолчанию false
],
]
);
*/
$form->field($model, 'content')->widget(
CKEditor::class,
[
'editorOptions' => ElFinder::ckeditorOptions(
'elfinder',
[
// разработанны стандартные настройки basic, standard, full
'preset' => 'basic',
'inline' => false, // по умолчанию false
]
),
]
);
?>
<?= $form->field($model, 'keywords')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<?= $form->field($model, 'description')->textarea(['rows' => 2, 'maxlength' =>
true]); ?>
<div class="form-group">
<?= Html::submitButton('Сохранить', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>
Теперь создаем страницу 404 Not Found для панели управления. Yii2 не поддерживает отдельную от
приложения обработку ошибок в модулях, обработчик ошибок является глобальным. Поэтому нужно
перезаписать компонент errorHandler в классе модуля, установить его для приложения и
зарегистрировать как обработчик ошибок.
<?php
namespace app\modules\admin;

use Yii;
use yii\web\ErrorHandler;

class Module extends \yii\base\Module {

public $controllerNamespace = 'app\modules\admin\controllers';

public function init() {


parent::init();
Yii::configure($this, [
'components' => [
'errorHandler' => [
'class' => ErrorHandler::class,
'errorAction' => 'admin/admin/error'
]
],
]);
$handler = $this->get('errorHandler');
Yii::$app->set('errorHandler', $handler);
$handler->register();
}
}
Добавляем метод actions() в класс контроллера AdminController:
<?php
namespace app\modules\admin\controllers;

use Yii;
use yii\web\Controller;

class AdminController extends Controller {


public function beforeAction($action) {
$session = Yii::$app->session;
$session->open();
if (!$session->has('auth_site_admin')) {
$this->redirect('/admin/auth/login');
return false;
}
return parent::beforeAction($action);
}

public function actions() {


return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
];
}
}

И создаем view-шаблон:

<?php
/*
* Файл modules/admin/views/admin/error.php
*/

/* @var $this yii\web\View */


/* @var $name string */
/* @var $message string */
/* @var $exception Exception */

use yii\helpers\Html;

$this->title = $name;
?>

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


<div class="alert alert-danger">
<?= nl2br(Html::encode($message)) ?>
</div>

Вам также может понравиться