Академический Документы
Профессиональный Документы
Культура Документы
Установка фрейморка и
внедрение верстки
Установка фреймворка
Устанавливать Yii2 будем через Composer, переходим в корневую директорию проекта и выполняем
команду:
Изменяем 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;
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',
];
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
]
);
}
}
AppAsset::register($this);
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>.....</head>
<body>
<?php $this->beginBody() ?>
<header>.....</header>
<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>
--
-- Структура таблицы `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;
/**
* Возвращает имя таблицы БД
*/
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;
/**
* Возвращает имя таблицы БД
*/
public static function tableName() {
return 'product';
}
/**
* Возвращает родительскую категорию
*/
public function getCategory() {
// связь таблицы БД `product` с таблицей `category`
return $this->hasOne(Category::class, ['id' => 'category_id']);
}
}
use yii\base\Widget;
use app\models\Category;
use Yii;
/**
* Виджет для вывода дерева разделов каталога товаров
*/
class TreeWidget extends Widget {
/**
* Выборка категорий каталога из базы данных
*/
protected $data;
/**
* Массив категорий каталога в виде дерева
*/
protected $tree;
/**
* Функция принимает на вход линейный массив элеменов, связанных
* отношениями 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().
<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',
];
Давайте изменим ее на
http://www.server.com/catalog/category/123
Для этого изменяем правила роутинга в файле конфигурации config/web.php:
$config = [
/*...*/
'components' => [
/*...*/
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'catalog/category/<id:\d+>' => 'catalog/category'
],
],
/*...*/
],
/*...*/
];
use yii\db\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;
/**
* Категория каталога товаров
*/
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')
);
}
}
Добавим в модель еще один метод, который возвращает массив популярных брендов:
<?php
namespace app\models;
use yii\db\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 {
}
<?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>
use app\models\Product;
return $this->render(
'index',
compact('hitProducts', 'newProducts', 'saleProducts')
);
}
}
Контроллер наследует класс AppController, где у нас будут методы, общие для всех контроллеров:
<?php
namespace app\controllers;
use yii\web\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>
Но, обо всем по порядку. Мы добавляем метод, позволяющий получить данные о категории. И у нас
описана связь таблицы 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);
}
}
<?php
namespace app\models;
use yii\db\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;
/* ... */
/*
* Категория каталога товаров
*/
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>
use yii\db\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;
/*...*/
/**
* Список всех брендов каталога товаров
*/
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>
В базе данных для таблиц 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;
/**
* Метод устанавливает мета-теги для страницы сайта
* @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;
return $this->render(
'index',
compact('hitProducts', 'newProducts', 'saleProducts')
);
}
}
<?php
namespace app\controllers;
use app\models\Category;
use app\models\Product;
use Yii;
<?php
namespace app\models;
use yii\data\Pagination;
use yii\db\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;
/*...*/
/**
* Категория каталога товаров
*/
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',
],
],
],
/*...*/
];
use yii\data\Pagination;
use yii\db\ActiveRecord;
use Yii;
/*...*/
/**
* Возвращает массив всех товаров бренда с идентификатором $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;
/*...*/
/**
* Список товаров бренда с идентификатором $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')
);
}
}
<?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. Промежуточные итоги и
рефакторинг кода
Некий промежуточный итог — здесь все, что было сделано на текущий момент. Исправлены ошибки,
допущенные ранее. Добавлены новые поля в таблицы базы данных. Переписаны некоторые фрагменты
кода, которые оказались неудачными. Добавлено кеширование тяжелых фрагментов кода, связанных с
выборкой данных из БД.
<?php
namespace app\controllers;
use yii\web\Controller;
use Yii;
/**
* Метод устанавливает мета-теги для страницы сайта
* @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;
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;
/**
* Категория каталога товаров
*/
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;
/**
* Метод возвращает имя таблицы БД
*/
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;
/**
* Метод возвращает имя таблицы БД
*/
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;
}
}
<?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>
<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; ?>
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>
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>
use app\models\Category;
use app\models\Brand;
use app\models\Product;
use Yii;
/*...*/
/**
* Страница товара с идентификатором $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;
/**
* Возвращает имя таблицы БД
*/
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;
}
И создаем файл 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>
Мы договорились ранее использовать класс 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;
/**
* Категория каталога товаров
*/
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')
);
}
}
Простой вариант
Начнем с 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;
/* ... */
/**
* Результаты поиска по каталогу товаров
*/
public function actionSearch($query = '', $page = 1) {
$page = (int)$page;
return $this->render(
'search',
compact('products', 'pages')
);
}
}
В класс модели Product добавим метод getSearchResult():
<?php
namespace app\models;
use Yii;
use yii\data\Pagination;
use yii\db\ActiveRecord;
/* ... */
/**
* Результаты поиска по каталогу товаров
*/
public function getSearchResult($search, $page) {
$search = $this->cleanSearchString($search);
if (empty($search)) {
return [null, null];
}
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>
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',
],
],
],
/* ... */
];
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=мужская+летняя+одежда
..........
Чтобы запрос что-то вернул, все три слова должны быть в названии товара. И именно в том порядке, в
каком они встречаются в поисковом запросе. Если название товара «Одежда летняя мужская» — то
такой товар не попадет в результаты поиска. Давайте это исправим.
<?php
namespace app\models;
use Yii;
use yii\data\Pagination;
use yii\db\ActiveRecord;
/* ... */
/**
* Результаты поиска по каталогу товаров
*/
public function getSearchResult($search, $page) {
$search = $this->cleanSearchString($search);
if (empty($search)) {
return [null, null];
}
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;
/* ... */
/**
* Результаты поиска по каталогу товаров
*/
public function getSearchResult($search, $page) {
$search = $this->cleanSearchString($search);
if (empty($search)) {
return [null, null];
}
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;
/* ... */
/**
* Результаты поиска по каталогу товаров
*/
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;
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',
],
],
],
/* ... */
];
Для решения этой проблемы используем стеммер Портера, который отсекает окончания и суффиксы
слова, оставляя только корень. Мне удалось найти на packagist.org готовый класс стеммера для русского
языка. Его и будем использовать.
Стеммер Портера — алгоритм стемминга, опубликованный Мартином Портером в 1980 году. Оригинальная
версия стеммера была предназначена для английского языка. Впоследствии Мартин создал проект «Snowball»
и, используя основную идею алгоритма, написал стеммеры для распространённых индоевропейских языков, в
том числе для русского.
Алгоритм не использует морфологический словарь, а только применяя последовательно ряд правил, отсекает
окончания и суффиксы, основываясь на особенностях языка, в связи с чем работает быстро, но не всегда
безошибочно.
Итак, устанавливаем пакет с использованием composer:
> composer require ladamalina/lingua-stem-ru
[InvalidArgumentException]
use Yii;
use yii\data\Pagination;
use yii\db\ActiveRecord;
use yii\db\Query;
use Stem\LinguaStemRu;
// постраничная навигация
$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;
}
/*.....*/
}
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
use app\models\Basket;
use Yii;
/*
* Данные должны приходить методом 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;
}
return $this->redirect(['basket/index']);
}
}
Создаем класс модели Basket:
<?php
namespace app\models;
use yii\base\Model;
use Yii;
/**
* Метод удаляет товар из корзины
*/
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', []);
}
<?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>
Обратите внимание на скрытое поле со значением CSRF токена. Это нужно, чтобы не получить ошибку:
<?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;
/*
* Данные должны приходить методом 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;
}
<?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);
?>
</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-код корзины
и показать модальное окно.
/*
* Добавление товара в корзину с использованием 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;
/* ... */
/* ... */
/*
* Добавление товара в корзину с использованием 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;
?>
use app\models\Basket;
use Yii;
/* ... */
/* ... */
<?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>
<?php
/*
* Корзина покупателя в модальном окне, файл views/basket/modal.php
*/
use yii\helpers\Html;
use yii\helpers\Url;
?>
use app\models\Basket;
use Yii;
Теперь удаление уже работает, но не слишком красиво. При удалении товара из корзины в модальном
окне присходит переход на страницу корзины. Давайте это исправим, будем перехватывать событие
клика по ссылке и отправлять 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;
По аналогии с удалением товара из корзины, будем отлавливать событие клика по ссылке «Очистить
корзину» и отправлять 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;
Array
[123] => 1
[456] => 2
)
Мы принимаем данные в методе actionUpdate() контроллера и вызываем
метод updateBasket() модели:
<?php
namespace app\controllers;
use app\models\Basket;
use Yii;
/*
* Данные должны приходить методом 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;
/**
* Метод удаляет товар из корзины
*/
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;
/*
* Данные должны приходить методом 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']);
}
}
<?php
namespace app\models;
use Yii;
use yii\db\ActiveRecord;
use yii\behaviors\TimestampBehavior;
/**
* Метод возвращает имя таблицы БД
*/
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],
];
}
use Yii;
use yii\db\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;
И файл 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:
use Yii;
use yii\db\ActiveRecord;
use yii\behaviors\TimestampBehavior;
use yii\db\Expression;
/*...*/
/**
* Метод расширяет возможности класса 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;
В 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
$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;
/*...*/
/**
* Добавляет записи в таблицу БД `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();
}
}
}
<?php
namespace app\controllers;
use Yii;
use app\models\Basket;
use app\models\Order;
--_=_swift_1566026927_e97faf701339ec540abb7bfef2e0f908_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
=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
<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>
<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_=_--
На этом с оформлением заказа мы закончили.
use Yii;
use app\models\Feedback;
use yii\web\Controller;
class SiteController extends Controller {
/*...*/
Yii::$app->mailer->compose()
->setFrom(Yii::$app->params['senderEmail'])
->setTo(Yii::$app->params['adminEmail'])
->setSubject('Заполнена форма обратной связи')
->setTextBody($textBody)
->setHtmlBody($htmlBody)
->send();
/*...*/
}
<?php
namespace app\models;
use yii\base\Model;
public $name;
public $email;
public $body;
/*
* Если данные формы не прошли валидацию, получаем из сессии сохраненные
* данные, чтобы заполнить ими поля формы, не заставляя пользователя
* заполнять форму повторно
*/
$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">×</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">×</span>
</button>
<p>Ваше сообщение успешно отправлено</p>
</div>
<?php endif; ?>
</div>
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_=_--
$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;
$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
--_=_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,
],
/*...*/
],
/*...*/
];
В этой конфигурации:
Хорошо, с публичной частью сайта мы закончили, теперь займемся панелью управления. Для этого
создадим модуль с помощью генератора кода Gii. Модуль можно рассматривать как миниатюрное
приложение, состоящие из моделей, представлений, контроллеров и других вспомогательных
компонентов. Но, в отличие от приложения, модуль нельзя развертывать отдельно, он должен
находиться внутри приложения.
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>© <?= Yii::$app->params['shopName'] ?></p>
</div>
</footer>
Аутентификация администратора
Здесь все будет предельно просто. Создаем контроллер AuthController и модель LoginForm — чтобы
проверять данные формы. Саму форму разместим в view-шаблоне login.php:
<?php
namespace app\modules\admin\controllers;
use Yii;
use yii\web\Controller;
use app\modules\admin\models\LoginForm;
use yii\base\Model;
public $email;
public $password;
$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;
Чтобы создать модель для работы с заказами в админке — используем генератор кода. Переходим по
ссылке «Model Generator», задаем имя таблицы БД, имя класса модели и пространство имен:
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-шаблоны. И мы получим
готовой код для создания, просмотра, редактирования и удаления заказов.
Хорошо, теперь займемся приведением в порядок того кода, который сформировал герератор. Начнем с
класса модели:
<?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;
$this->title = 'Заказы';
?>
<?=
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 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('Заказ не найден');
}
}
use yii\data\ActiveDataProvider;
use app\modules\admin\models\Order;
]
]);
$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;
<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;
/**
* Возвращает имя таблицы БД
*/
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;
/*...*/
/**
* Позволяет получить все товары заказа
*/
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;
<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;
<?=
$this->render('_form', [
'model' => $model,
]);
?>
Вопреки ожиданиям, формы там нет, она расположена в файле _form.php:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
Давайте ее немного изменим — запретим редактировать сумму заказа, дату создания и обновления +
заменим текстовое поле статуса заказа на выпадающий список.
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
<?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>
Нам еще нужно изменять дату и время обновления заказа при редактировании. Чтобы правильно их
сортировать на главной странице панели управления. Для этого добавим метод behaviors() классу
модели Order:
<?php
namespace app\modules\admin\models;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\Expression;
/*...*/
/*...*/
}
И последнее, что осталось сделать — удалять записи из таблицы БД order_item при удалении заказа:
<?php
namespace app\modules\admin\models;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\Expression;
/**
* Удаляет товары заказа при удалении заказа
*/
public function afterDelete() {
parent::afterDelete();
OrderItem::deleteAll(['order_id' => $this->id]);
}
/*...*/
}
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',
];
}
}
<?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',
];
}
}
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();
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);
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;
}
<?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();
/**
* 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);
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;
}
Это будет не окончательный вариант, а только первое приближение. Дальше нужно будет еще
организовать загрузку файлов изображений для товаров, категорий и брендов; создать выпадающий
список для выбора родителя для категории и товара и т.д. и т.п.
Работа с брендами
<?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 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;
$this->title = 'Бренды';
?>
<?=
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;
<?=
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;
Работа с категориями
<?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 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;
<?=
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;
Работа с товарами
<?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 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();
/**
* Обновление существующего товара
*/
public function actionUpdate($id) {
$model = $this->findModel($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;
<?=
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;
<?=
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;
use Yii;
use yii\db\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;
}
}
<?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;
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;
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,
]
);
}
/*...*/
}
<?php
/*
* Страница списка всех категорий, файл modules/admin/views/category/index.php
*/
use yii\helpers\Html;
<?php
/*
* Страница списка товаров категории, файл
modules/admin/views/category/products.php
*/
use yii\helpers\Url;
use yii\helpers\Html;
use yii\grid\GridView;
<?=
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;
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
],
]);
<?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;
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 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;
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 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;
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 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;
/**
* Проверка перед удалением категории
*/
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();
}
/*...*/
}
<?php
/*
* Layout-шаблон, файл modules/views/layouts/main.php
*/
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">×</span>
</button>
<p><?= Yii::$app->session->getFlash('warning'); ?></p>
</div>
<?php endif; ?>
<?= $content; ?>
</div>
<footer class="footer">
<!-- .......... -->
</footer>
/**
* Проверка перед удалением бренда
*/
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();
}
/*...*/
}
Как обычно, воспользуемся генератором кода Gii. Переходим по ссылке «Model Generator», задаем имя
таблицы БД, имя класса модели и пространство имен:
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-шаблоны. И мы получим
готовой код для создания, просмотра, редактирования и удаления страниц.
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();
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);
/**
* 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;
}
Модель и контроллер
<?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 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();
return $this->render('create', [
'model' => $model,
]);
}
/**
* Обновление существующей страницы
*/
public function actionUpdate($id) {
$model = $this->findModel($id);
/**
* Удаление существующей страницы
*/
public function actionDelete($id) {
$this->findModel($id)->delete();
return $this->redirect(['index']);
}
/**
* Поиск страницы по идентификатору
*/
protected function findModel($id) {
if (($model = Page::findOne($id)) !== null) {
return $model;
}
<?=
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;
<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;
<?php
/*
* Страница редактирования страницы, файл modules/admin/views/page/update.php
*/
use yii\helpers\Html;
use Yii;
use yii\db\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
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>
<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);
?>
/*
* Главная страница сайта
*/
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('Запрошенная страница не найдена');
}
}
<?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 общедоступной части сайта, что не очень удобно.
use Yii;
use yii\web\ErrorHandler;
use Yii;
use yii\web\Controller;
И создаем view-шаблон:
<?php
/*
* Файл modules/admin/views/admin/error.php
*/
use yii\helpers\Html;
$this->title = $name;
?>