Об Evolution CMS CE
Evolution CMS CE система из начала десятых, которая переживала как свои подъемы так и падения. В данный момент поддерживается фактически 1-м человеком и в среднем выпускает обновление раз в год. В нее запихнули некоторые компоненты Laravel 8 и оставили административную панель. Если искать аналоги из других сфер мне кажется достаточно близок Django из Python.
Для меня эта система стала во многом проводником в мир веб-разработки за счет того, что она поддерживает плавные переходы от легкого к сложному. В начале я пользовался только административной панелью и не прибегал к написанию скриптов или внедрению сложной логики. По мере получения новых навыков переводил шаблоны на blade и добавлял к ним серверную логику.
Система из коробки предоставляет базовые поля такие как pagetitle, description и т.д. в большинстве случаев это позволяет нам не работать с БД за нас все делает CMS, но система не забирает у вас такой возможности, если вы не доверяете внутренним оптимизациям SQL-запросов.
Еще интересный факт из-за нишевости Evolution CMS CE и закрытия актуальных ресурсов системы для ботов. Нейросети плохо работают с Evolution CMS, относить это в плюс или в минус решайте сами. Конечно нейросети могут что-то сделать, но это будет далеко не самый продвинутый уровень разработки в Evolution CMS CE.
Требования к магазину
Перед разработкой магазина на Evolution CMS CE я составил себе список того, что хочу видеть в итоговом результате:
Личный кабинет пользователя
Корзина
Опции товаров
Фильтрация товаров
Поиск по сайту
Импорт и экспорт товаров
Конечно требований по сути не так много, но этого достаточно чтобы был работающий и приносящий прибыль магазин. Для меня по сути это была проверка моих навыков, что я могу выжать из всех доступных мне опций. По этому давайте поэтапно разбирать реализацию.
Реализация Личного кабинета
Задача по своей сути достаточно типовая по этому умельцы сделали готовый компонент под эту задачу. Имя ему FormLister изначально он был разработан под работу с формами, но по мере развития стал стандартом еще и для реализации работы с пользователями и формами на сайте. Дополнительно был использован компонент FormSender для отправки форм без перезагрузки. FormSender это по сути дополнение для FormLister добавляющий AJAX.
Было сделано 3 формы Login, Register и Profile для входа, регистрации и редактирования профиля.
Вот листинг для Login:
<?php
return [
'api' => 2,
'formid' => 'login',
'controller'=> 'Login',
'model' => 'Pathologic\EvolutionCMS\MODxAPI\modUsers',
'loginField'=> 'email',
'rules' => [
'email' => [
'required' => 'Обязательно введите email',
'email' => 'Введите email правильно'
],
'password' => [
'required' => 'Обязательно введите пароль',
'minLength' => [
'params' => 6,
'message' => 'В пароле должно быть больше 6 символов'
]
]
],
'redirectTo'=> 18
];
Чтобы этот код отработал нужно особым образом разметить форму на сайте подробнее можете про это почитать в документации (их несколько старая, новая и еще есть записки на сайте сообщества).
Мой вариант формы для входа представлен в листинге ниже:
<div class="modal-overlay" id="loginModal">
<div class="modal-container auth-modal">
<div class="modal-header">
<h2>Вход в аккаунт</h2>
<button class="modal-close" onclick="closeLoginModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="form-wrapper">
<form class="modal-form">
<input type="hidden" name="formid" value="login">
@csrf
<div class="form-group" data-field="email">
<label for="email">Email <span class="required">*</span></label>
<input type="email" name="email" id="email" class="form-control" placeholder="ivan@gmail.com">
</div>
<div class="form-group" data-field="password">
<label for="password">Пароль <span class="required">*</span></label>
<input type="password" name="password" id="password" class="form-control" placeholder="••••••••">
</div>
<div class="form-actions">
<input type="submit" value="Войти" class="modal-submit-btn">
</div>
<div class="auth-links">
<a href="#" onclick="openRegisterModal(); return false;">Нет аккаунта? Зарегистрироваться</a>
</div>
</form>
</div>
</div>
</div>
Все работает на js так что особое внимание уделите верным названиям классов и data атрибутов. У меня достаточно простая форма с 2-я параметрами почта и пароль. Подобные формы были сделаны для регистрации и редактирования профиля только с большим количеством полей дублировать сюда я их не буду.
Реализация элементов магазина
Поскольку магазин с товарами, опциями, корзиной является типовой задачей, в своей основе был использован готовый компонент Commerce реализующий за нас логику работы, а нас он только просит правильно все настроить.
Корзина есть в нем из коробки, так что нам нужно будет только получить ее в нужном нам виде и месте. Для этого был составлен следйющий запрос представленный в листинге ниже:
<?php
namespace EvolutionCMS\Shop\Traits;
use EvolutionCMS\Shop\Facades\Snippet;
use Illuminate\Support\Facades\Config;
trait CartTraits
{
public function getCart($type = 'mini')
{
$globalParams = Config::get('Cart.global');
$params = [];
if ($type == 'mini') {
$params = Config::get('Cart.mini');
}
if ($type == 'cart') {
$params = Config::get('Cart.cart');
}
return Snippet::Cart(array_merge($params, $globalParams));
}
}
На сайте будет фактически 2 корзины 1 это её мини версия доступная в шапке сайте я туда вынес только количество товаров, но по желанию там можно развернуть полноценную корзину появляющуюся в выпадающем окне. Вторая корзина это отдельная страница, где мы видим все приобретенные товары и опции к ним.
Разметка корзины выглядит вот так:
@if ($data['count'])
<div class="cart-layout" data-commerce-cart="{{ $data['hash'] }}">
<div class="cart-items">
{!! $data['dl.wrap'] !!}
</div>
<div class="cart-sidebar">
<div class="cart-summary">
<h3>Ваш заказ</h3>
<div class="summary-row">
<span>Товары ({{ $data['count'] }} шт.)</span>
<span>@price($data['items_price'])</span>
</div>
<div class="summary-row total">
<span>Итого</span>
<span>@price($data['total'])</span>
</div>
<button class="btn-checkout" onclick="openOrderModal()">
<i class="fas fa-lock"></i> Оформить заказ
</button>
</div>
</div>
</div>
@else
<div class="empty-cart">
<i class="fas fa-shopping-cart"></i>
<h2>Корзина пуста</h2>
<p>Но это никогда не поздно исправить :)</p>
<a href=@makeUrl(2) class="btn-primary"><i class="fas fa-arrow-right"></i> Перейти в каталог</a>
</div>
@endif
По сути это обертка корзины, а для самих товаров в ней и их опций сделан отдельный шаблон сюда его выносить я не буду там по сути схожее получение данных через масив
$data.
Чтобы корзина начала работать нам нужно добавлять туда товары, а откуда их взять нужно как-то разметить форму для этого снова прописываем data-атребуты и передаем в скрытое поле id ресурса для проверки товара на существование и присвоение ему реальной цены существующей в системе.
<form class="product-info" action="#" data-commerce-action="add">
<input type="hidden" name="id" value="{{ $id }}" />
<div class="product-info-header">
<h1>{{ $pagetitle }}</h1>
</div>
<!-- Цена -->
<div class="product-price-block">
<div class="current-price">
<span class="price">@price($price)</span>
@if ($old_price)
<span class="old-price">@price($old_price)</span>
<span class="discount-badge">-10%</span>
@endif
</div>
</div>
<!-- Опции товара -->
@if (!empty($options) and $options != '[]')
<div class="product-options">
<h3>Выберите опции</h3>
@php
$options = json_decode($options, true)['fieldValue'] ?? [];
@endphp
<!-- Группа опций -->
<div class="options-group">
<div class="options-list">
@forelse ($options as $item)
<div class="option-item">
<input type="checkbox" name="options[options_{{ $loop->iteration }}]"
id="options_{{ $loop->iteration }}" value="{{ $item['name'] }}">
<label for="options_{{ $loop->iteration }}">
{{ $item['name'] }}
<span class="option-price">(+{{ $item['value'] }} ₽)</span>
</label>
</div>
@empty
@endforelse
</div>
</div>
</div>
@endif
<!-- Количество -->
<div class="quantity-selector">
<span class="quantity-label">Количество:</span>
<div class="quantity-controls">
<button type="button" class="quantity-btn" onclick="decrementQuantity()"><i
class="fas fa-minus"></i></button>
<input type="number" name="count" class="quantity-input" value="1" min="1"
max="99" step="1">
<button type="button" class="quantity-btn" onclick="incrementQuantity()"><i
class="fas fa-plus"></i></button>
</div>
</div>
<!-- Кнопки покупки -->
<div class="purchase-buttons">
<button type="submit" name="action" value="cart" class="btn-cart">
<i class="fas fa-shopping-cart"></i>
В корзину
</button>
</div>
<!-- Краткие характеристики -->
@php
$parameters = json_decode($parameters, true)['fieldValue'] ?? [];
@endphp
<div class="product-specs-block">
<h3>Характеристики</h3>
<div class="specs-grid">
@forelse ($parameters as $item)
<div class="spec-item">
<span class="spec-label">{{ $item['name'] }}</span>
<span class="spec-value">{{ $item['value'] }}</span>
</div>
@break($loop->index == 3)
@empty
<div>В данный момент характеристики не доступны</div>
@endforelse
</div>
<a href="#" class="full-specs-link">Все характеристики <i class="fas fa-arrow-right"></i></a>
</div>
</form>
В форме вы можете увидеть как переменные $pagetitle которые корректно передаются из системы так и фрагмент с php для опций это уже мой выбор реализации. Так как ядро не все форматы данных может коректно передавать я обработал один из таких форматов прямо в view это плохой способ реализации лучще всего такие даные обрабатывать на сервере и передавать в готовом виде в View.
На этом первую часть думаю можно закончить, если есть вопросы или предложения по улучщению жду ващих комментариев.