Магазин на Yii2, часть 34. Показываем меню страниц в публичной части

22.09.2019

Теги: Web-разработкаYii2ИерархияИнтернетМагазинКаталогТоваровПрактикаСтраницаСайтаУдалитьФреймворк

Список всех страниц сайта

Сейчас для показа всех страниц в панели управления используется класс ActiveDataProvider и виджет GridView. Нам это не подходит, потому что страницы надо показывать с учетом иерархии. По аналогии с категориями каталога изменим метод контроллера actionIndex() и view-шаблон index.php.

class PageController extends AdminController {
    /*...*/
    public function actionIndex() {
        return $this->render(
            'index',
            ['pages' => Page::getTree()]
        );
    }
    /*...*/
}
class Page extends ActiveRecord {
    /*...*/
    public static function getTree($parent = 0) {
        $children = self::find()
            ->where(['parent_id' => $parent])
            ->asArray()
            ->all();
        $result = [];
        foreach ($children as $page) {
            if ($parent) {
                $page['name'] = '— ' . $page['name'];
            }
            $result[] = $page;
            $result = array_merge(
                $result,
                self::getTree($page['id'])
            );
        }
        return $result;
    }
    /*...*/
}
<?php
/*
 * Страница списка всех страниц, файл modules/admin/views/page/index.php
 */
use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */
/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = 'Все страницы';
?>

<h1><?= Html::encode($this->title); ?></h1>
<p>
    <?= Html::a('Добавить страницу', ['create'], ['class' => 'btn btn-success']); ?>
</p>

<table class="table table-striped table-bordered">
    <thead>
    <tr>
        <th>Наименование</th>
        <th>Мета-тег keywords</th>
        <th>Мета-тег description</th>
        <th><span class="glyphicon glyphicon-eye-open"></span></th>
        <th><span class="glyphicon glyphicon-pencil"></span></th>
        <th><span class="glyphicon glyphicon-trash"></span></th>
    </tr>
    </thead>
    <tbody>
    <?php foreach ($pages as $page): ?>
        <tr>
            <td><?= $page['name']; ?></td>
            <td><?= $page['keywords']; ?></td>
            <td><?= $page['description']; ?></td>
            <td>
                <?=
                Html::a(
                    '<span class="glyphicon glyphicon-eye-open"></span>',
                    ['/admin/page/view', 'id' => $page['id']]
                );
                ?>
            </td>
            <td>
                <?=
                Html::a(
                    '<span class="glyphicon glyphicon-pencil"></span>',
                    ['/admin/page/update', 'id' => $page['id']]
                );
                ?>
            </td>
            <td>
                <?=
                Html::a(
                    '<span class="glyphicon glyphicon-trash"></span>',
                    ['/admin/page/delete', 'id' => $page['id']],
                    [
                        'data-confirm'=> 'Вы уверены, что хотите удалить эту страницу?',
                        'data-method'=> 'post'
                    ]
                );
                ?>
            </td>
        </tr>
    <?php endforeach; ?>
    </tbody>
</table>

Проверка перед удалением страницы

Перед удалением страницы необходимо проверить, что у нее нет дочерних страниц. Поэтому добавим в модель метод beforeDelete():

class Page extends ActiveRecord {
    /*...*/
    public function beforeDelete() {
        $children = self::find()->where(['parent_id' => $this->id])->all();
        if (!empty($children)) {
            Yii::$app->session->setFlash(
                'warning',
                'Нельзя удалить страницу, которая имеет дочерние стрницы'
            );
            return false;
        }
        return parent::beforeDelete();
    }
    /*...*/
}

Показываем меню в публичной части

Для этого надо получить список всех страниц и передать эти данные в layout-шаблон. Добавим в класс AppController переменную $pageMenu и метод beforeAction() для решения этой задачи:

class AppController extends Controller {
    /*...*/
    public $pageMenu;
    /*...*/
    public function beforeAction($action) {
        $this->pageMenu = Page::getTree();
        return parent::beforeAction($action);
    }
    /*...*/
}

Еще нам потребуется модель для страниц:

<?php
namespace app\models;

use Yii;
use yii\db\ActiveRecord;

class Page extends ActiveRecord {

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

    /**
     * Метод возвращает все страницы в виде дерева
     */
    public static function getTree() {
        // пробуем извлечь данные из кеша
        $data = Yii::$app->cache->get('page-menu');
        if ($data === false) {
            // данных нет в кеше, получаем их заново
            $pages = Page::find()
                ->select(['id', 'name', 'slug', 'parent_id'])
                ->indexBy('id')
                ->asArray()
                ->all();
            $data = self::makeTree($pages);
            // сохраняем полученные данные в кеше
            Yii::$app->cache->set('page-menu', $data, 60);
        }
        return $data;
    }

    /**
     * Принимает на вход линейный массив элеменов, связанных отношениями
     * parent-child, и возвращает массив в виде дерева
     */
    protected static function makeTree($data = []) {
        if (count($data) == 0) {
            return [];
        }
        $tree = [];
        foreach ($data as $id => &$node) {
            if ($node['parent_id'] == 0) {
                $tree[$id] = &$node;
            } else {
                $data[$node['parent_id']]['childs'][$id] = &$node;
            }
        }
        return $tree;
    }
}

И изменяем layout-шаблон:

<?php

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

use yii\helpers\Html;
use yii\helpers\Url;
use app\assets\AppAsset;
use yii\bootstrap\Modal;

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

<body>
<?php $this->beginBody(); ?>
<header>

    <div class="header-top">
        <div class="container">
            <div class="row">
                <div class="col-sm-6">
                    <ul class="nav nav-pills">
                        <li><a href="#"><i class="fa fa-phone"></i> +2 95 01 88 821</a></li>
                        <li><a href="#"><i class="fa fa-envelope"></i> info@domain.com</a></li>
                    </ul>
                </div>
                <div class="col-sm-6">
                    <ul class="nav nav-pills pull-right">
                        <li><a href="#"><i class="fa fa-facebook"></i></a></li>
                        <li><a href="#"><i class="fa fa-twitter"></i></a></li>
                        <li><a href="#"><i class="fa fa-linkedin"></i></a></li>
                        <li><a href="#"><i class="fa fa-dribbble"></i></a></li>
                        <li><a href="#"><i class="fa fa-google-plus"></i></a></li>
                    </ul>
                </div>
            </div>
        </div>
    </div>

    <div class="header-middle">
        <div class="container">
            <div class="row">
                <div class="col-sm-4">
                    <div class="pull-left">
                        <a href="<?= Url::home(); ?>">
                            <?=
                            Html::img(
                                '@web/images/home/logo.png',
                                ['alt' => Yii::$app->params['shopName']]
                            );
                            ?>
                        </a>
                    </div>
                </div>
                <div class="col-sm-8">
                    <ul class="pull-right">
                        <li><i class="fa fa-user"></i> <a href="#">Аккаунт</a></li>
                        <li><i class="fa fa-star"></i> <a href="#">Избранное</a></li>
                        <li><i class="fa fa-crosshairs"></i> <a href="#">Оформить</a></li>
                        <li>
                            <i class="fa fa-shopping-cart"></i>
                            <a href="<?= Url::to(['basket/index']); ?>">Корзина</a>
                        </li>
                        <li><i class="fa fa-lock"></i> <a href="#">Войти</a></li>
                    </ul>
                </div>
            </div>
        </div>
    </div>

    <div class="header-bottom">
        <div class="container">
            <div class="row">
                <div class="col-sm-8">
                    <div id="menu">
                        <ul>
                            <li>
                                <a href="<?= Url::to(['catalog/index']); ?>">
                                    Каталог
                                </a>
                            </li>
                            <?php foreach ($this->context->pageMenu as $page): ?>
                                <li>
                                    <a href="<?= Url::to(['page/view', 'slug' => $page['slug']]); ?>">
                                        <?= $page['name']; ?>
                                    </a>
                                    <?php if (isset($page['childs'])): ?>
                                        <ul>
                                        <?php foreach ($page['childs'] as $child): ?>
                                            <li>
                                                <a href="<?= Url::to(['page/view', 'slug' => $child['slug']]); ?>">
                                                    <?= $child['name']; ?>
                                                </a>
                                            </li>
                                        <?php endforeach; ?>
                                        </ul>
                                    <?php endif; ?>
                                </li>
                            <?php endforeach; ?>
                        </ul>
                    </div>
                </div>
                <div class="col-sm-4">
                    <form method="post" action="<?= Url::to(['catalog/search']); ?>" class="pull-right">
                        <?=
                        Html::hiddenInput(
                            Yii::$app->request->csrfParam,
                            Yii::$app->request->csrfToken
                        );
                        ?>
                        <div class="input-group">
                            <input type="text" name="query" class="form-control" placeholder="Поиск по каталогу">
                            <div class="input-group-btn">
                                <button class="btn btn-default" type="submit">
                                    <span class="glyphicon glyphicon-search"></span>
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>

</header>

<?= $content ?>

<footer>
    <div class="container">
        Copyright © 2018 E-SHOPPER Inc. All rights reserved.
    </div>
</footer>

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

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

Показываем страницу сайта

Для начала изменим настройки компонента UrlManager:

/*...*/
$config = [
    /*...*/
    'components' => [
        /*...*/
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
                // раздел каталога: 2, 3, 4 страница списка товаров
                'catalog/category/<id:\d+>/page/<page:\d+>' => 'catalog/category',
                // раздел каталога: первая страница списка товаров
                'catalog/category/<id:\d+>' => 'catalog/category',
                // бренд каталога: 2, 3, 4 страница списка товаров
                'catalog/brand/<id:\d+>/page/<page:\d+>' => 'catalog/brand',
                // бренд каталога: первая страница списка товаров
                'catalog/brand/<id:\d+>' => 'catalog/brand',
                // страница отдельного товара каталога
                'catalog/product/<id:\d+>' => 'catalog/product',
                // правило для 2, 3, 4 страницы результатов поиска
                'catalog/search/query/<query:.*?>/page/<page:\d+>' => 'catalog/search',
                // правило для первой страницы результатов поиска
                'catalog/search/query/<query:.*?>' => 'catalog/search',
                // правило для первой страницы с пустым запросом
                'catalog/search' => 'catalog/search',
                // страница сайта
                '/page/<slug:[-_0-9a-zA-Z]+>/' => 'page/view'
            ],
        ],
        /*...*/
    ],
    /*...*/
];
/*...*/

Добавим метод actionView() в контроллер — он будет отвечать за показ страницы:

class PageController extends AppController {

    /*
     * Главная страница сайта
     */
    public function actionIndex() {
        /*...*/
    }

    /*
     * Произвольная страница сайта
     */
    public function actionView($slug) {
        if ($page = Page::find()->where(['slug' => $slug])->one()) {
            $this->setMetaTags(
                $page->name,
                $page->keywords,
                $page->description
            );
            return $this->render(
                'view',
                ['page' => $page]
            );
        }
        throw new NotFoundHttpException('Запрошенная страница не найдена');
    }
}

И последнее — создадим view-шаблон для показа отдельной страницы сайта:

<?php
/*
 * Произвольная страница сайта, файл views/page/view.php
 */

use app\components\TreeWidget;
use app\components\BrandsWidget;
?>

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

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

            <div class="col-sm-9">
                <h1><?= $page['name']; ?></h1>
                <?= $page['content']; ?>
            </div>
        </div>
    </div>
</section>

Поиск: 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.