Магазин на Yii2, часть 12. Промежуточные итоги и рефакторинг кода

03.06.2019

Теги: Web-разработкаYii2ИнтернетМагазинКаталогТоваровКешированиеПрактикаФреймворк

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

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

<?php
namespace app\controllers;

use yii\web\Controller;
use Yii;

class AppController extends Controller {

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

use app\models\Product;
use Yii;

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

        // устанавливаем мета-теги для страницы
        $this->setMetaTags();

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

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

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

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

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

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

Модели Brand и Category

<?php
namespace app\models;

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

class Brand extends ActiveRecord {

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

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

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

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

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

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

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

class Category extends ActiveRecord {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Поиск: Web-разработка • Yii2 • Интернет магазин • Каталог товаров • Кеширование • Практика • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.