Shopkeeper 4.0 — Интернет-магазин на Symfony + Angular + MongoDB


    История


    Немного предыстории. Первая версия Shopkeeper вышла в 2009 году. Тогда это был модуль для CMS MODX, а точнее для первой его ветки, которая сейчас называется Evolution. В то время у MODX был всего один подобный модуль, но его качество меня не устраивало, поэтому я решил написать свой. Плюс нужен был какой-то проект для наращивания опыта в программировании на PHP. Т.к. конкуренции почти не было, мой компонент стал самым популярным для той версии MODX.

    Страшная картинка


    Сначала Shopkeeper это был только компонент для создания корзины товаров и управления заказами. Позже добавился функционал для управления каталогом (создание и редактирование товаров и т.п.).

    Позднее вышел MODX Revolution. Для него я тоже разработал новую версию своего компонента.

    Так выглядел интерфейс управления заказами Shopkeeper 2.0


    Затем я немного отошел от разработки на MODX и углубился во Front-end разработку. В основном в то время я работал на AngularJS 1.x. Когда пришло время обновить свой компонент для создания интернет-магазина, я принял решение разрабатывать его на AngularJS.

    Shopkeeper 3.0


    3.2.7-pl3 — Последняя на данный момент версия Shopkeeper из ветки 3.x.

    На основной работе я часто использую Symfony, Angular 2.0+ и MongoDB, поэтому новую версию SHK я разрабатываю с использованием этих инструментов.

    Обзор возможностей


    Shopkeeper 4.0 — это не просто компонент для CMS, это самостоятельный движок. Данное приложение я стараюсь сделать максимально простым, но в то же время гибким. Планируется возможность интеграции с другими CMS. В первую очередь будет сделана интеграция для MODX Revolution. Я считаю, что нет смысла создавать узкоспециализированный компонент для одной CMS. Ведь всё равно приходится реализовывать не только корзину товаров, но ещё и управление каталогом и т.д. Нужно стараться создавать приложения максимально универсальными.

    На данный момент Shopkeeper 4.0 находится в разработке, недавно вышла бета-версия. Уже реализованы почти все основные возможности, которые были запланированы, но есть мелкие недоработки, а так же планирую добавить больше Ajax.




    Возможности:

    — Можно создавать разные типы товаров с разными наборами параметров (полями). Для каждого поля можно выбрать тип ввода и вывода (как в MODX).
    — Товары можно сохранять в разные коллекции (таблицы) базы данных MongoDB.
    — Управление пользователями для администратора.
    — Управление заказами для администратора.
    — Интерфейс для управления валютами, способами доставки, способами оплаты товаров и т.д.
    — Для покупателя есть личный кабинет, где он может отслеживать статус заказа и редактировать свои контактные данные.
    — Вывод товаров с возможностью фильтрации по параметрам.
    — Параметры товаров, которые могут влиять на цену.
    — Шаблон с отзывчивой версткой с использованием Bootstrap 4.
    … другое.







    Да, я знаю, что на Symfony уже есть как минимум один движок для создания интернет-магазина — Sylius. Но его реализация мне лично не очень понравилась. Я думаю, что вы сразу увидите разницу в подходе. Например, там нет Angular, TypeScript и MongoDB, а для меня это важно.

    Техническая информация


    В свойствах типа контента можно добавить поле и выбрать тип ввода и тип вывода для этого поля. Есть интерфейс для создания своих типов ввода и вывода.




    Данные полей товаров в шаблоне сайта можно выводить несколькими способами.

    Вывод сырых данных:

    <span class="text-secondary">
    {{ currentPage.price }}
    </span>
    

    Вывод данных с использованием шаблона типа вывода:

    {{ renderOutputTypeField(currentPage, fields, 'price') | raw }}
    

    Аргументы:

    1. Массив со всеми данными страницы.
    2. Массив с данными полей типа контента.
    3. Системное имя поля.

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

    Вывод данных поля по названию чанка:

    {{ renderOutputTypeChunk(currentPage, fields, 'price') | raw }}
    

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

    Вывод всех полей, в свойствах которых включен флажок «Показывать в списке».

    {{ renderOutputTypeArray(currentPage, fields, 'prefix_') | raw }}
    

    Для удобства сделано много Twig-функций. Например, так выводится меню категорий каталога:

    {{ categoriesTree(0, 'menu_dropdown', null, true) }}
    

    Здесь 4-й аргумент отвечает за управление кэшированием (по умолчанию выключено).

    Если включено кэширование, то весь HTML код меню будет сохранен в файле. Но при этом есть возможность отмечать текущий пункт меню. Более подробно в документации.

    Так в шаблоне выводится корзина товаров:

    {{ shopCart('shop_cart_edit', 'shop_cart_edit_empty') }}
    

    Здесь первый параметр — название шаблона, второй — название шаблона пустой корзины (указывать не обязательно).

    Так выглядит код шаблона простой корзины, где выводится только число выбранных товаров и общая цена:

    <div class="shop-cart-bottom">
        <div class="shop-cart-bottom-b">
            <div class="container">
                <div class="float-md-left">
                    {{ 'Selected' | trans }}:
                    <span class="badge badge-pill badge-light big mx-2">
                        {{ countTotal }}
                    </span>
                    {{ 'product with a total cost|products with a total cost' | transchoice(countTotal) }}
                    <span class="badge badge-light big mx-2">
                        {{ priceTotal | price }}
                    </span>
                    {{ currency }}
                </div>
                    <div class="float-md-right mt-3 mt-md-0">
                        <a class="btn btn-outline-light" href="{{ path('shop_cart_edit') }}">
                            {{ 'Proceed to checkout' | trans }}
                        </a>
                        <a class="btn btn-outline-light ml-1" href="{{ path('shop_cart_clear') }}" data-toggle="tooltip" data-placement="top" title="{{ 'Empty cart' | trans }}">
                            <i class="icon-cross"></i>
                        </a>
                    </div>
                <div class="clearfix"></div>
            </div>
        </div>
    </div>
    

    Для автоматического изменения размеров изображений используется LiipImagineBundle. Пример вывода изображения товара:

    <img src="{{ imageUrl(data) | imagine_filter('thumb_small') }}" alt="">
    

    Здесь «thumb_small» — это название одного из наборов фильтров, которые созданы в файле конфигурации. По умолчанию созданы три таких набора: thumb_small, thumb_medium, thumb_big. Подробнее в документации.

    Пример настроек набора фильтров:

    thumb_small:
        quality: 85
        filters:
            relative_resize:
                widen: 200
            thumbnail: { size: [200, 200], mode: inset }
            background: { size: [200, 200], position: center, color: '#ffffff' }
    

    Ссылку на демо-сайт давать не буду, т.к. боюсь «хабра-эффекта». Но при желании её легко найти.
    Буду благодарен за ваши комментарии, критику и пулл-реквесты.

    Последняя версия на данный момент: beta3.
    Проект на GitHub
    Документация
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 37
      +3
      Интересно было посмотреть что внутри. После второй мадженты все такое простое…
      Открываю контроллер Cart. В методе addAction написано 100 строк кода и вот эти mongo->cache. Не знаю зачем в сотый раз нужно это писать. Сейчас, например, прошла мода на создание так называемых витрин для магазинов. Сама витрина работает на vue, а магазин на чем угодно. Вот за этим будущее, имхо.
      Не увидел у вас api. А как расширять систему, модулей как-то не видно. Монга выбрана потому что так надо? Я не работал с ней в проектах, разве там можно обеспечить целостность?

      Хочу, нет, мечтаю увидеть магазин на ddd с по-настоящему универсальными модулями. У которых будут отдельно интерфейсы и реализация. Чтобы в будущем 1 раз написали глобальный модуль товаров и весь мир его использовал в любых системах с любыми реализациями. Ведь нельзя же изобретать Log в десятитысячный раз.
        0
        У меня тоже мечта, чтобы был какой-то нормальный микросервисный движок, типа Shopfy, но с возможностью хостить на своем сервере. И чтобы я мог прикручивать любые фронты: телефоны, компы, мобильные приложения, React-фронты и пр.
          0
          > Открываю контроллер Cart. В методе addAction написано 100 строк кода и вот эти mongo->cache.

          Спасибо за замечание. Согласен, что выглядит немного пугающе. Позже вынесу некоторые куски кода в отдельные методы. Там происходит довольно много действий. Нужно определить в каком поле находится цена товара, т.к. вы его можете назвать как угодно по своему желанию. Но нужно поставить метку (об этом писал в статье). Товар уже может быть в корзине, поэтому нужно его найти, сравнить параметры и изменить количество, если параметры сходятся… и т.д. Содержимое корзины сохраняется в БД, чтобы можно было оформить покупку хоть через несколько дней (проверка актуальности цены будет добавлена позже). Для этого хорошо подошел стандартный для Symfony механизм кэширования в БД.

          Ну и я пока не хочу отказываться от поддержки PHP 5.x, поэтому код получается более многословным. В PHP 7, например, появился оператор "??", который я использовать не могу.

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

          Не понял к чему это.

          > А как расширять систему, модулей как-то не видно.

          Для модулей админки есть абстрактный класс (StorageControllerAbstract), в котором реализованы большинство методов, которые могут понадобиться в других модулях. Для стандартного модуля достаточно контроллер наследовать от этого класса и добавить методы валидации данных и создания/редактирования сущности. А для добавления функционала нужно использовать систему бандлов Symfony. По-моему этот механизм очень удобен.

          > Монга выбрана потому что так надо? Я не работал с ней в проектах, разве там можно обеспечить целостность?

          Забыл об этом написать в статье. Спасибо за вопрос. MongoDB выбрана для удобства. Например, там очень легко менять структуру данных. Так же по идеологии считается нормальным сохранять в ДБ вложенные коллекции. К примеру, в реляционных БД нужно создавать кучу отдельных таблиц и потом вытаскивать данные через JOIN. Но, если задуматься, эти данные отдельно друг от друга нужны в очень редких случаях. В данном случае у меня все данные типов контента хранятся в одной коллекции.
          Как это выглядит



            0
            Для модулей админки есть абстрактный класс

            так вот к чему про 2009-ый год в статье.


            По-моему этот механизм очень удобен.

            нет.


            Например, там очень легко менять структуру данных

            сегодня даже sqlite умеет в json.


            эти данные отдельно друг от друга нужны в очень редких случаях.

            типичная ошибка начинающего архитектора — проектировать модель данных под UI.


            Просто шутки ради рекомендую посмотреть: Microservices and Rules Engines – a blast from the past — Udi Dahan. Там больше про связанность и про мелочи декомпозиции которые могут быть неочевидны поначалу но заставляют чуть по другому смотреть на проектирование.

              0
              так вот к чему про 2009-ый год в статье.

              Тут не хватает ещё одного предложения.

              нет.

              Там написано «по-моему». Вы лучше знаете что мне удобно?

              сегодня даже sqlite умеет в json.

              И что? Я уже отвечал про доступность MongoDB ниже.

              Просто шутки ради...

              Если бы без этого, то я бы сказал «спасибо» за ссылку. Но тут я чувствую, что цель была не помочь, а выпендриться. Осторожно с короной, смотрите не уроните.
                0
                Тут не хватает ещё одного предложения.

                Это тонкая отсылка к Yii, который был популярен как раз в те времена, и который прославлял наследование как единственно верный путь расширения функционала и реюза кода.


                Чем это плохо — не мне рассказывать.


                Вы лучше знаете что мне удобно?

                А вы продукт свой единолично будете использовать? если так — то никаких вопросов. Но тогда зачем вы статью опубликовали?


                Если вы делаете продукт, без разницы коммерческий или опенсурс, он должен быть удобен не вам а пользователям продукта. Увы.


                И тут вопрос не "мне удобно" а в том что расширяемость через бандлы — так себе идея. Это не плагины в чистом виде, ваша система должна предоставлять какие-то свои точки расширения функционала. И это не просто.


                Но тут я чувствую, что цель была не помочь, а выпендриться.

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

                  0
                  Чем это плохо — не мне рассказывать.
                  Тогда мне сложно вас понять, даже не знаю как написать в гугле.

                  А вы продукт свой единолично будете использовать?
                  Что бы я не сделал всегда найдутся люди, которым это будет не удобно, которые знают «самый крутой способ сделать лучше». Я поделился с такими же как я. Думаю, я не один такой на свете.

                  И тут вопрос не «мне удобно» а в том что расширяемость через бандлы — так себе идея. Это не плагины в чистом виде...
                  Не согласен. По-моему это именно плагины в чистом виде. Вы не написали никакой конкретики, чтобы меня переубедить. Да, нужен интерфейс для их удобной установки. Я думаю это немного другой вопрос. Там был вопрос плагина с точки зрения разработчика.
                    0
                    Что бы я не сделал всегда найдутся люди, которым это будет не удобно...
                    Тут хочу пояснить, что я готов рассматривать любые альтернативы. Но нужна конкретика. Я не могу знать их все.
                      0

                      я дал вам ссылку, ознакомьтесь с одной из многих альтернатив в плане реализации более-менее расширяемых систем.

                      0
                      Тогда мне сложно вас понять, даже не знаю как написать в гугле.


                      Composition vs inheritance
                        0
                        В своем приложении я не создавал какую-то свою архитектуру. Фреймворк Symfony имеет свою архитектуру, в которой реализованы паттерны.
                          0
                          Фреймворк Symfony (начиная с версии 2) особой архитектуры не имеет. Сейчас Symfony это скорее конструктор для пострения архитектур.
                      0
                      Не согласен.

                      Тогда приведите конкретику, как именно бандлы позволят вам расширять вашу систему. Скажем что нужно будет сделать что бы при просмотре карзины можно было лицезреть продукты, которые вы выложили из корзины перед чекаутом в предыдущие заходы?


                      их удобной установки

                      Проблема в том что точки расширений волшебным образом у вашего приложения за счет бандлов не появляются. Они вообще для реюза инфраструктуры, у вас чуть другая ситуация.

                        –1
                        Тогда приведите конкретику, как именно бандлы позволят вам расширять вашу систему. Скажем что нужно будет сделать что бы при просмотре карзины можно было лицезреть продукты, которые вы выложили из корзины перед чекаутом в предыдущие заходы?
                        Например, в бандле Вы может создать свою Twig-функцию, которая может выводить что угодно, в данном случае товары, которые вы выложили из корзины. Можно добавить события на удаление элементов
                        github.com/andchir/shopkeeper4/tree/master/src/AppBundle/Event
                        Пока таких событий нет, но архитектура позволяет из легко добавить.

                        Проблема в том что точки расширений волшебным образом у вашего приложения за счет бандлов не появляются. Они вообще для реюза инфраструктуры, у вас чуть другая ситуация.
                        Взять, к примеру, LiipImagineBundle Чем это не плагин? Нужно только добавить конфигурацию в файл.
                          0
                          Попробуйте так модуль скидок кастомный прикрутить. Или модуль трекинга доставки. Что бы было дешево интегрировать и в целом небыло конфликтов по функционалу с другими элементами системы.

                          Это в целом беда всех CMS которые пытаются таким образом расширять функциональность. за счет обобщения и объединения.

                          Словом, не вы первый и не вы последний. Несколько раз в год кто-то делает свою новую CMS и как бы… ничего толком качественно не меняется.
                0
                Ваша система неплоха в вашем случае. Вот так. В этом нет ничего плохого, просто не обманывайтесь что пишите что-то универсальное и масштабное.

                Посмотрите на ту же мадженту 2. Там сидит огромный штат не самых глупых людей которые пытаются создать «ту самую систему» и это у них с трудом получается. Загляните как-нибудь в код и гляньте как там сделаны модули и di (и плагины), думаю что откроете что-то интересное.
                0
                когда витрина на vue, как решается вопрос с индексацией поисковыми роботами таких страниц?
                  0
                    0
                    Для vuejs есть расширение Nuxt с серверным рендерингом. Это как вариант.
                  +2
                  Спасибо за то что вы делаете. Считаю нужно поощрять разработку продуктов под опенсоурс. Уважаемый iproger и вам спасибо за то что даёте своё виденье на то как улучшить продукт. А если решите его реализовать в предлагаемом варианте, то сообщество будет вам благодарно!
                    0
                    Это очень и очень хорошо. Но для популярности необходимо учитывать популярность развёртывания. Если продукт нельзя развернуть на обычном 150 рублевом хостинге, если используются бд типа монго (как вышел mysql 5.7 с поддержкой json полей в монго вообще по-моему смысла все меньше и меньше, а в расчёте на популярность точно), то стоит ожидать обхода со стороны потенциальных пользователей, которые обходят битрикс потому что у заказчиков нет 35тр на движек.
                      0
                      35 т.р. — это меньше чем пол месяца работы программиста. Стоимость движка — это очень малая часть ИМ (работа, дизайн, обслуживание).
                      Другое дело, что выбирая новое решение, владельцу ИМ необходимо будет брать дополнительные риски связанные с поиском программиста\команды с нужной компетенцией.
                        +1
                        Стоимость движка — это очень малая часть ИМ (работа, дизайн, обслуживание).

                        Ну кто спорит. Только вы мыслите как настоящий маркетолог. Многие программисты своим знакомым, девушкам, друзьям, родителям бесплатно поднимают ИМ при условии что не надо вкладывать никакие деньги — просто попробовать, дать людям возможность, и 35тр ради попробовать ой не надо. Автором именно и движет любовь к ближнему, а не к бизнесу.

                        В некоторой степени для общества тут проблема, малый бизнес (если он остался) обдирает IT своей монополией.

                        То есть суть то вобщем про любовь. Бирикс это про бабки. В итоге у нас остаются либо облака, либо адские динозавры типа опенкарта или престашопа, которые тоже обросли платными модулями, делающих их них что-то похожее на магазин.
                        По факту сейчас нет современного бесплатного движка ИМ (про Юпи не знаю особо ничего) который я мог бы посоветовать поставить — советую облака.
                          0
                          Не устаю советовать OXID eShop
                        0
                        У MongoDB есть свой облачный сервис. Бесплатный сервер там, конечно, очень медленный, но цены не очень кусаются. Для магазина их цены вполне приемлемы. Я думаю когда появится спрос, тогда появится и предложение.
                        Сейчас хостинг с VDS стоит очень не дорого и развернуть там MongoDB так же просто как и MySQL.
                          0
                          VDS не отдашь клиенту с потрохами. Нужна поддержка. Тут штука такая, чтобы быть популярным т.е понятным нужно упасть уровнем до дурака ленивого. Но все равно у вас замечательный продукт.
                            +1
                            Даже обычный шаред-хостинг не каждому клиенту отдашь с потрохами. Даже не каждый SaaS. Всё зависит от компетентности клиента или желания её повышать до уровня, минимизирующего внешнюю поддержку.
                        +1
                        > Copyright © 2004-2017 Fabien Potencier

                        Поправьте файл LICENSE :)
                          0
                          «планирую добавить больше Ajax» — че, ангуляр юзать передумали?
                            0
                            Имеется ввиду во внешней части сайта. Ангуляр используется только в админке.
                              0
                              ага, понял, спасибо.
                            +1

                            От autoescape можно избавиться


                            {% autoescape false %}
                                {{ renderOutputTypeChunk(currentPage, fields, 'header', 'page_') }}
                            {% endautoescape %}

                            https://twig.symfony.com/doc/2.x/advanced.html#automatic-escaping


                            new TwigFunction('renderOutputTypeChunk', [$this, 'renderOutputTypeChunkFunction'], [
                                'is_safe' => ['html'],
                            ]),

                            Чтобы получать инстанс twig вместо передачи контейнера лучше


                            new TwigFunction('renderOutputTypeChunk', [$this, 'renderOutputTypeChunkFunction'], [
                                'needs_environment' => true,
                            ]),

                            Тогда он будет передаваться первым параметром:


                            public function renderOutputTypeChunkFunction(\Twig_Environment $environment, $itemData, $fieldsData, $chunkName, $chunkNamePrefix = '')
                            {

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое