Магазин на Laravel 7, часть 7. Меню каталога товаров и популярные бренды в левой колонке

07.10.2020

Теги: ComposerLaravelMySQLPHPWeb-разработкаБазаДанныхИнтернетМагазинКаталогТоваровПеременнаяПрактикаФреймворкШаблонСайта

Два меню с сайдбаре

На всех страницах сайта в левой колонке показывается меню каталога и список популярных брендов. Это значит, что эти данные мы должны получать всегда, и отправлять их в layout-шаблон. Именно для таких случаев в Laravel предусмотрено готовое решение — View Composers. Но лучше мы создадим два маленьких шаблона (roots.blade.php и brands.blade.php) в директории layout/part, чтобы не прегружать layout-шаблон — и с помощью композера будет отправлять в них данные.

<h4>Разделы каталога</h4>
<ul>
@foreach($items as $item)
    <li>
        <a href="{{ route('catalog.category', ['slug' => $item->slug]) }}">{{ $item->name }}</a>
    </li>
@endforeach
</ul>
<h4>Популярные бренды</h4>
<ul>
@foreach($items as $item)
    <li>
        <a href="{{ route('catalog.brand', ['slug' => $item->slug]) }}">{{ $item->name }}</a>
        <span class="badge badge-dark float-right">{{ $item->products_count }}</span>
    </li>
@endforeach
</ul>

И будем подключать эти два шаблона внутри layout-шаблона:

<div class="row">
    <div class="col-md-3">
        @include('layout.part.roots')
        @include('layout.part.brands')
        <!--
        <h4>Разделы каталога</h4>
        <p>Здесь будут корневые разделы</p>
        <h4>Популярные бренды</h4>
        <p>Здесь будут популярные бренды</p>
        -->
    </div>
    <div class="col-md-9">
        @yield('content')
    </div>
</div>

Теперь создадим поставщика услуг ComposerServiceProvider:

> php artisan make:provider ComposerServiceProvider

И сразу добавим его в массив providers файла конфигурации config/app.php:

return [
    /* ... */
    'providers' => [
        /* ... */
        App\Providers\ComposerServiceProvider::class,
        /* ... */
    ],
    /* ... */
];

Далее редактируем созданный app/Providers/ComposerServiceProvider.php:

namespace App\Providers;

use App\Brand;
use App\Category;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

class ComposerServiceProvider extends ServiceProvider {
    /**
     * Register services.
     *
     * @return void
     */
    public function register() {
        // .....
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot() {
        View::composer('layout.part.roots', function($view) {
            $view->with(['items' => Category::roots()]);
        });
        View::composer('layout.part.brands', function($view) {
            $view->with(['items' => Brand::popular()]);
        });
    }
}

В модель Category добавляем метод roots(), а в модель Brand — метод popular():

namespace App;

use Illuminate\Database\Eloquent\Model;

class Category extends Model {
    /* ... */

    /**
     * Возвращает список корневых категорий каталога товаров
     */
    public static function roots() {
        return self::where('parent_id', 0)->get();
    }
}
namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

class Brand extends Model {
    /* ... */

    /**
     * Возвращает список популярных брендов каталога товаров.
     * Следовало бы отобрать бренды, товары которых продаются
     * чаще всего. Но поскольку таких данных у нас еще нет,
     * просто получаем 5 брендов с наибольшим кол-вом товаров
     */
    public static function popular() {
        return self::withCount('products')->orderByDesc('products_count')->limit(5)->get();
    }
}

Все, можно проверять, что получилось в итоге:

При желании, мы можем показывать в меню каталога не только корневые категории, но и дочерние. Для этого добавим в модель Category новую связь таблицы categories с таблицей categories.

namespace App;

use Illuminate\Database\Eloquent\Model;

class Category extends Model {
    /* ... */
    
    /**
     * Связь «один ко многим» таблицы `categories` с таблицей `categories`
     */
    public function children() {
        return $this->hasMany(Category::class, 'parent_id');
    }

    /**
     * Возвращает список корневых категорий каталога товаров
     */
    public static function roots() {
        return self::where('parent_id', 0)->get();
    }
}

Теперь в шаблоне roots.blade.php мы можем обратиться к виртуальному свойству children, чтобы получить список дочерних категорий:

<h4>Разделы каталога</h4>
<div id="catalog-sidebar">
    <ul>
    @foreach($items as $item)
        <li>
            <a href="{{ route('catalog.category', ['slug' => $item->slug]) }}">{{ $item->name }}</a>
            @if ($item->children->count())
                <ul>
                @foreach($item->children as $child)
                    <li>
                        <a href="{{ route('catalog.category', ['slug' => $child->slug]) }}">
                            {{ $child->name }}
                        </a>
                    </li>
                @endforeach
                </ul>
            @endif
        </li>
    @endforeach
    </ul>
</div>

Добавляем javascript

Давайте добавим возможность сворачивать и разворачивать меню каталога в сайдбаре. При загрузке страницы видны будут только корневые разделы каталога. При клике по иконке с плюсом будут показаны дочерние категории. При повторном клике по иконке (но уже с минусом) дочерние категории будут скрыты.

<h4>Разделы каталога</h4>
<div id="catalog-sidebar">
    <ul>
    @foreach($items as $item)
        <li>
            <a href="{{ route('catalog.category', ['slug' => $item->slug]) }}">{{ $item->name }}</a>
            @isset($item->children)
                <span class="badge badge-dark">
                    <i class="fa fa-plus"></i> <!-- бейдж с плюсом или минусом -->
                </span>
                <ul>
                @foreach($item->children as $child)
                    <li>
                        <a href="{{ route('catalog.category', ['slug' => $child->slug]) }}">
                            {{ $child->name }}
                        </a>
                    </li>
                @endforeach
                </ul>
            @endisset
        </li>
    @endforeach
    </ul>
</div>

Создадим файл site.js в директории public/js и добавим в него следующий код:

jQuery(document).ready(function($) {
    $('#catalog-sidebar > ul ul').hide();
    $('#catalog-sidebar .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');
            });
        }
    });
});

Подключим этот js-файл в layout-шаблоне и посмотрим результат:

Жадная загрузка

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

SELECT * FROM `categories` WHERE `parent_id` = 0
SELECT * FROM `categories` WHERE `categories`.`parent_id` = 1 AND `categories`.`parent_id` IS NOT NULL
SELECT * FROM `categories` WHERE `categories`.`parent_id` = 2 AND `categories`.`parent_id` IS NOT NULL
SELECT * FROM `categories` WHERE `categories`.`parent_id` = 3 AND `categories`.`parent_id` IS NOT NULL
SELECT * FROM `categories` WHERE `categories`.`parent_id` = 4 AND `categories`.`parent_id` IS NOT NULL

Происходит это из-за так называемой ленивой загрузки данных (отложенная загрузка). Именно она используется при обращении к виртуальному свойству children, доступному после реализации связи таблицы categories с таблицей categories. Использовать ленивую загрузку очень удобно, поскольку она работает только тогда, когда мы ее вызываем.

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

class Category extends Model {
    /* ... */

    /**
     * Возвращает список корневых категорий каталога товаров
     */
    public static function roots() {
        return self::where('parent_id', 0)->with('children')->get();
    }
}

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

SELECT * FROM `categories` WHERE `parent_id` = 0
SELECT * FROM `categories` WHERE `categories`.`parent_id` IN (1, 2, 3, 4)

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

Каталог оборудования
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.