Архитектура модуля CleverStyle CMS

    В последнее время на хабре было несколько статей о CMS написанных их разработчиками, вот и я решил написать.
    О CleverStyle CMS я уже писал дважды (последний раз год назад), и дважды получал огромный спектр критики и большую пачку замечаний разного плана — спасибо за всё, я потратил время на то, чтобы учесть ошибки и исправить их.

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

    Немного истории


    Когда читаю о том, что движок был написан одним человеком за год в свободное время — возникает удивление и уважение к проделанной работе. Разрабатывая систему три с хвостиком года понимаешь какой это огромный пласт работы, но ещё больше, скорее всего, впереди. В большинстве мне попадалась разработка сайтов с уникальной функциональностью, и движок в первую очередь писался как ядро в помощь разработчику, несмотря на CMS в названии, что сказывается на почти всём что в нём есть.

    Философия модуля


    Модуль — такой компонент, который отвечает за рендеринг содержимого страницы или обработку запросов к API. Модуль содержит всё в себе и не распространяет свои файлы по углам системы (поэтому пути в статье всегда будут указываться относительно корня папки модуля), а если нужно что-то таки положить — после удаления модуль убирает за собой всё таким образом, чтобы не оставить ничего. Это позволяет сохранять производительность системы на должном уровне с течением времени.

    Модуль может быть предельно простым (папка с одним файлом index.html или index.php), или сколько нужно сложным: с поддержкой нескольких БД, хранилищ файлов, зависимостями, внешним API, многоязычностью и кучей разнообразных ресурсов. При всём этом вы используете только то, что вам нужно, и не более того (минимум шаблонного кода).

    Иерархия страниц


    Может быть три основных «части» модуля (в любой комбинации): администрирование, API, страницы для конечного пользователя. Все три части могут состоять из одного index.php файла (или дополнительно index.html в случае страницы для пользователя), или иерархии страниц, которая описана в admin/index.json, api/index.json и index.json. Поддерживаются страницы первого и второго уровня, остальные при необходимости реализовываются разработчиком. Выглядит вот так (пример из модуля системы, администрирование):

    {
    	"components"	: [
    		"modules",
    		"plugins",
    		"blocks",
    		"databases",
    		"storages"
    	],
    	"general"		: [
    		"site_info",
    		"system",
    		"optimization",
    		"appearance",
    		"languages",
    		"about_server"
    	],
    	"users"			: [
    		"general",
    		"users",
    		"groups",
    		"permissions",
    		"security",
    		"mail"
    	]
    }
    

    Соответственно страницы будут выглядеть так:

    • admin/System (будет выбран первый элемент на каждом уровне вложенности из index.json, аналогично admin/System/components/modules)
    • admin/System/general/about_server
    • admin (будет выбран системный модуль)

    Для обработки путей используются одноименные файлы, при чём выполняются в таком логичном порядке:

    • admin/index.php (выполняется при наличии всегда)
    • admin/components.php (при наличии)
    • admin/components/modules.php

    Для API удобно использовать REST, и в связи с этим обработка запросов к API логичным и удобным образом отличается:

    • api/index.php
    • api/index.{method}.php
    • api/posts.php
    • api/posts.{method}.php

    Таким образом можно направлять запросы с помощью разных HTTP методов в разные файлы. Если подходящего файла с методом нет, но есть файл с другим методом — в ответ прилетит логичный 405 Method Not Allowed, а в заголовке Allow будет список доступных методов.

    Так же стоит заметить, что при обработке запросов числовые части пути игнорируются при разборе структуры страниц (это логично, так как числа это обычно id каких-то элементов, либо номер страницы).

    Для ручных манипуляций можно получить путь страницы без префикса admin или api таким нехитрым способом:

    $Config = \cs\Config::instance();
    $route = $Config->route; // ['components', 'modules'] для первого примера, или ['posts', 10] для примера с API
    

    При желании можно оставить только index.php и разбираться с маршрутом самому.

    Ресурсы (скрипты, стили, изображения, шрифты, веб-компоненты)


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

    • includes/css
    • includes/js
    • includes/html (в случае с Apache2 нужно ещё положить сюда или уровнем выше .htaccess, который отроет доступ к *.html файлам)

    Все файлы внутри папок с соответствующим расширением будут автоматически подхвачены системой в алфавитном порядке, файлы могут быть вложены в подпапки — это не проблема. При желании исключить обработку папки ядром достаточно положить в желаемое место пустой файл с названием !include (читается как не подключать).

    Всё просто, а главное эффективно. Дело в том, что при подключении ресурсов ядро умеет разруливать зависимости компонентов, то есть всё необходимое данному компоненту будет автоматически подхвачено. Более того, при выставлении опции в админке, скрипты, стили и веб-компоненты будут минифицированы, объединены (при желании для веб-компонентов можно дополнительно включить так называемую вулканизацию), и запакованы с помощью gzip, и при этом все операции делаются атомарно, то есть каждый самостоятельный набор ресурсов лежит в отдельном файле, и при подключении минифицированных сжатых версий всё ещё сохраняется учёт зависимостей, а файлы получают уникальные префиксы, чтобы при обновлении определённых файлов ресурсов их сжатые версии переименовывались и не висели в кэше браузера вызывая проблемы. Также стоит заметить, что все относительные ссылки на изображения, шрифты, вложенные импорты стилей встраиваются в результирующий css файл минимизируя количество HTTP запросов (пока не придет повсеместный HTTP2 или что они там решат в итоге), это так же справедливо для стилей, которые используются в веб-компонентах.

    Для того, чтобы подключать ресурсы на определённых страницах используется файл includes/map.json, в котором указывается на каких страницах какие ресурсы нужны. Если ресурс там не указан — он будет подключен на всех страницах сайта (именно так, не только в текущем модуле). Вот один из примеров:

    {
    	"admin/Blogs"	: [
    		"admin.css"
    	],
    	"Blogs"			: [
    		"general.css",
    		"general.js"
    	]
    }
    

    includes/css и аналогичные префиксы указывать не нужно, из расширения файла понятно где он лежит.

    Веб-компоненты

    На самом деле их стоит упомянуть немного отдельно. С ядром системы в комплекте идет Polymer Platform (полифилы веб-компонентов) и сам Polymer. Заставить их работать с jQuery и некоторыми другими библиотеками было совсем не тривиально, но разработчики оперативно приняли патчи с исправлениями, так что Polymer и jQuery которые идут в комплекте — это git версии после принятого патча с исправлением, так как релиза пока небыло. Также ядро движка патчит jQuery.ready() так, чтобы выполнять его после инициализации всех веб-компонентов.

    Если быть кратким — не знаю на текущий момент где ещё есть такая комбинация патчей, которая позволяет прямо сейчас использовать веб-компоненты в продакшене (немного громко звучит, но если вы пробовали использовать веб-компоненты в реальных проектах с jQuery — вы понимаете о чём я).

    Если вы не собираетесь использовать CleverStyle CMS

    Хотя код достаточно монолитен, весьма просто можно взять определённую функциональность, и использовать в своем проекте независимо (MIT лицензия это позволяет), как пример — объединение и минифицирование стилей и веб-компонентов (с поддержкой вулканизации).

    Зависимости


    Модули могут зависеть друг от друга, и от плагинов (другой вид компонентов, не имеет собственной страницы для отображения).

    Каждый модуль может иметь не только название, но и предоставлять определённую функциональность, и зависимости чаще всего указывают именно на функциональность, что позволяет выбрать один из нескольких модулей, предоставляющих нужную функциональность (и движок не позволит установить второй модуль, обеспечивающий ту же функциональность). Так же в случае конкретного модуля (например, System, который связан с ядром движка) можно ограничить нужную версию.

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

    Пример мета-файла meta.json с описанием деталей модуля и его зависимостей:

    {
    	"package"		: "Blogs",
    	"category"		: "modules",
    	"version"		: "0.097.0+build-107",
    	"description"	: "Adds blogging functionality. Comments module is required for comments functionality, Plupload or similar module is required for files uploading functionality.",
    	"author"		: "Nazar Mokrynskyi",
    	"website"		: "cleverstyle.org/cms",
    	"license"		: "MIT License",
    	"db_support"	: [
    		"MySQLi"
    	],
    	"provide"		: "blogs",
    	"require"		: "System=>0.574",
    	"optional"		: [
    		"Comments",
    		"Plupload",
    		"TinyMCE",
    		"file_upload",
    		"editor",
    		"simple_editor",
    		"inline_editor"
    	],
    	"multilingual"	: [
    		"interface",
    		"content"
    	],
    	"languages"		: [
    		"English",
    		"Русский",
    		"Українська"
    	]
    }
    

    А как же код?


    Все классы, трейты, и подобные вещи лежат в пространстве имен cs или вложенных.

    Автозагрузчик классов устроен так, что класс cs\modules\Blog\Post будет искаться в файле Post.php модуля Blog — просто и логично.

    Для того чтобы подписаться на события в системе (например, очистка данных модуля при его удалении) используются триггеры, а их объявление чаще всего лежит в файле trigger.php, который вызывается на каждой странице не зависимо от отображаемого модуля, и используется специально для этих целей.

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

    Резюме


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

    Планы


    Последнее время движок был существенно отрефакторен, и близится первый за три года релиз, версия 1.0.
    В связи с этим хотелось бы получить побольше обратной связи.
    На данный момент больше всего меня беспокоит отсутствие тестов (несколько существующих не считаются). В последних изменениях сделаны шаги по направлению к улучшению тестируемости кода, но что-то с тестами всё равно не складывается, буду очень рад помощи в этой области (только не нужно DI предлагать).

    На этом всё, надеюсь сумел вызвать интерес посмотреть на всё это в живую. Код покрыт PhpDoc комментариями практически везде, что очень хорошо подхватывается IDE, так же как пример можно смотреть готовые модули в репозитории движка.
    Страница проекта на GitHub, там же есть wiki с документацией (преимущественно по Backend).

    Демка


    В комментариях попросили демку, вот она (Nginx + HHVM):
    http://demo.cscms.org
    admin:1111
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 16

      0
      Демки нет?
        0
        Только рабочие сайты.
        Есть идея сделать образ для Docker, это позволит поднять демку в один клик. Что скажете?
          +2
          Скажу что лучше просто демку, с админкой
            0
            Добавил в конце поста ссылку на демку, может потребоваться некоторое время перед тем как сайт заработает, так как изменил NS сервера.
        0
        дел
          0
          Еще один велосипед, благодаря которым потом говорят что в сообществе PHP не программисты. Ни о «стандарте» PSR-0, ни о PSR-1 автор видимо не слышал, код читать невозможно, когда в if происходит запрос в бд, который в чистом виде прописан прямо там же — это ужас пример.

          И как можно читать такое?

          На демо сайте что-то странное со временем (оно в разных форматах везде, и как пользователь может задать свою Timezone вообще непонятно)

          Иерархия ужасна, вообще нелогична. Хорошая попытка, но это использовать нельзя
            –1
            О PSR знаю прекрасно, собрались представители сообщества PHP и решили выработать компромиссные правила. Тем не менее это рекомендация, которая в некоторых местах мне не нравится. В связи с этим их конвенции не наследуются, я придерживаюсь другого, но тоже весьма однородного, стиля, что не является преступлением.

            На счёт запроса в БД — ничего ужасного, обычный запрос с prepared statements, PDO не используется, используется более быстрый вариант — чистые SQL запросы, которые читать, имхо, проще, да и синтаксис SQL знаком всем, даже тем кто не пишет на PHP. Тем более проще когда делаются сложные выборки из БД с участием нескольких таблиц. К тому же не нужно помнить о подводных камнях PDO, получаете ровно то, что просите.

            Читать можно легко и с удовольствием, если использовать IDE где одна табуляция имеет ширину 4 пробела, а не 8 как на GitHub.

            Время в комментариях отображается по разному, в зависимости от того, когда сделан комментарий. Если недавно — отображается время, если давно — то полная дата. Форматы настраиваются в локализации. Часовый пояс настраивается как на уровне всей системы тут, так и в профиле пользователя если он этого хочет тут.

            На счёт иерархии я бы хотел услышать подробнее.
              +2
              PSR «стандарты» были сделаны не просто так, а чтобы любой разработчик открыв любой проект, мог за пару минут разобраться в его структуре. К примеру PSR-0 либо PSR-4 дает нам единый стиль именования и размещения файлов компонента. PSR-1 задает стиль кодирования, хотите использовать табы — используйте Smart табуляцию. Именование переменных стоит вести в одном виде.

              Насчет запросов к БД, то возможно стоит использовать подход DRY? Не повторять одно и то же в каждом файле, как только что-то добавится в таблицу, вам надоест переписывать полностью во всех местах. Хотя бы сделать провайдеры либо репозитории (вынести эти все запросы согластно таблиц в свои места, и менять удобно, и код чище).

              Читать невозможно не только из-за табуляции, а из-за именования переменных, что обозначает переменная $L? А почему она не называется $foo? То-есть это нормально?

              Я о том, что стиль времени в виде 20:30 в посте, а 8:30 pm в комментариях это нормально? Вынести логику вывода в одно место, в виде хелпера, и то проще была бы.

              Все очень плохо, если одним предложением. Все раскидано в разных местах, очень много повторений, добавление поста в блог и редактирование в разных файлах, а по методам которые они вызывают там вообще удаление происходит (о_О). И если не заметить к примеру add_internal то можно подумать что он удаляет а не создает.

              Еще раз скажу, поддержка такого проекта будет стоить не малых средств и времени, так как правки нужно делать в вообще неожиданных местах.
                0
                Framework Interop Group как бы из названия намекает, для чего создана. Так как данный движок не является фреймворком в чистом виде, и не предполагает что ядро будут кастомизировать в каждом проекте — отсюда выплывает ряд соответствующих особенностей архитектуры, повышенный уровень связности, а как следствие меньше избыточных уровней абстракции и выше общее быстродействие.

                В большинстве модулей есть один или несколько классов, которые содержать ряд методов, и с помощью этих методов производятся всевозможные манипуляции. Таким образом явные запросы в БД в большинстве своем не пишутся где-либо за рамками этих классов. Например, https://github.com/nazar-pc/CleverStyle-CMS/tree/master/components/modules/Commentsмодуль комментариев, который интегрируется в блоги, и потенциально другие модули при необходимости. В нём запросы есть только в Comments.php, где лежит один класс, больше нигде запросов нет, только вызовы методов. К тому же, все названия полей перечисляются явно, потому при обновлении структуры БД места где новые поля не используются не сломаются, и не потребуют редактирования.

                Переменные с большой буквы (с одним исключением) именуются с большой буквы, к тому же если посмотреть на определение переменной:
                $L = Language::instance();
                

                Дополнительных вопросов возникнуть не должно. В данном конкретном случае название класса слишком длинное, если вам так больше понравится — можно использовать функцию __(), которая является оберткой, и привычна многим.

                По поводу даты — спасибо, вы нашли опечатку, исправлено.

                Как раз всё лежит в вполне предсказуемых местах. Всё что касается модуля только в его директории, системные классы и функции в core относительно корня. Добавление и редактирование поста в разных файлах, по скольку это разные страницы. Вв фреймворке, вероятнее всего, это были бы два метода (action) одного контроллера + две (или одна) view, а здесь это два файла. Непосредственно манипуляции всё равно производятся в одном классе, остальное — графическая обертка над этим + права доступа. На счёт удаления не понял о чём именно речь, поиск по проекту «add_internal» не нашел.
              0
              Вы можете назвать хотя бы пять CMS с нормальной иерархией, по стандарту PSR-0 или PSR-1, с адекватным (по вашему мнению) подходом к запросам БД, одновременно с этим, чтобы устройство CMS не было слишком сложным, и для поддержки проекта не потребовалось бы много средств и времени?
              Спрашиваю ради ознакомления, это не троллинг.
                0
                Я мало связан с CMS'ками, но тот же Drupal 8 был переписан используя Symfony. И поддержка PSR-1 в нем есть точно, а вот насчет PSR-0 — то вроде у модулей, внутри самой системы есть, а насчет остальных частей — не знаю. Ну а вообще, я стараюсь убеждать заказчиков, что CMS для чего-то отличного от блога — это плохо, даже для статических страниц.

                Сейчас сообщество PHP разделилось на два фронта:
                1. CMS разработчики — сам PHP знают очень мало, умеют на WP поставить плагины и, как они сами говорят «обернуть в AJAX». Самая многочисленная группа;
                2. PHP разработчики — это люди, которые пишут используя всю мощь текущих наработок PHP. Для них слова: Composer, PSR-4, Generator, Trait, ORM, и другие — это общеупотребимые слова.

                И эти два фронта в последнее время все дальше и дальше расходятся, что дальше может привести к созданию полноценного форка PHP, в котором PHP разработчики добьются добавления тех же: нативных аннотаций, type hint'ов для скалярных типов, и т.д.

                К чему я виду? К тому что обилие таких CMS'ок еще больше усугубляет такой раскол. Разработчики Drupal при представлении Drupal 8 кстати об этом всем говорили.
                  0
                  Если вы читали статью — то могли бы заметить, что от CMS тут больше названия чем чего-либо другого. Это CMF, ядро со всеми компонентами, которые нужны для разработки функциональности заказчика. Это предполагается как фундамент, который максимально прост, но при этом самодостаточен, работает и адекватно настроен из коробки, но позволяет переопределить практически всё что угодно при необходимости, даже системные классы, не редактируя при этом файлы ядра, и сохраняя возможность обновления.

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

                  Всё что нужно движку идет с ним в комплекте, тем не менее если движок увидит наличие composer.json — он включить его в дистрибутив, если его папка vendor будет обнаружена на установленной системе — автозагрузчик composer будет автоматически подключен, и все сторонние компоненты будут автоматически доступны ровно так же, как и компоненты ядра системы. Ядро вообще не против всего этого, просто оно само по себе в этом не нуждается, но при наличии прозрачно интегрируется и просто работает.

                  То есть тут используются новые штуки PHP, те же Trait, но при этом подход совсем другой, не предполагается что вы возьмете класс (или набор классов) для работы с пользователями, и замените его полностью на сторонний, либо начнете использовать в стороннем проекте. Тем не менее как показывает практика достаточно простой код легко переносится на другие платформы, и я не вижу здесь смысла писать слишком высокоабстрактный код, лишь бы предоставить мнимую переносимость и interoperability (не знаю как правильнее перевести), которая используется очень редко. Система вообще скорее для разработчика, которому нужна платформа с предоставлением нужных рабочих интерфейсов, которые не требуют километров yaml конфигов, формат которых нужно держать в памяти, а банально работают сразу. Разработчик занимается непосредственно бизнес-логикой, и я верю что это и должно быть подавляющим объемом написанного кода.
                    +1
                    Вы говорите, что мало связаны с CMS'ками, но беретесь судить что плохо/хорошо для них, оперируя понятиями, которые ближе для фреймворков и отдельных скриптов. Если вы убеждаете заказчиков, что CMS для чего-то отличного от блога — это плохо, значит вы относите себя ко второму типу разработчиков и не видите прослойки между первым и вторым типов в виде создателей CMS-велосипедов, которые пишут поделки, облегчающие и автоматизирующие типичные действия. Пишут сначала для себя, а затем, когда проект разрастается, уже и для других. В мире более 1000 CMS и каждый день появляются новые. Они в корне отличаются от скриптов и фреймворков. В первую очередь тем, что обычно не нужно лезть в код. И сайты можно создавать не зная PHP, соответственно и ориентация идет в первую очередь на пользователей не программистов. Есть системы модульные, позволяющие писать различные дополнения и для расширенной разработки на них достаточно знать, как эти дополнения разрабатывать. А есть и монолиты, которые, несмотря на старый ненормативный код, довольно шустро работают и позволяют сделать очень многое за очень малое время. Всё зависит от задачи.
                    Друпал же не относится к легким для освоения CMSкам, он скорее для программистов, особенно из-за многочисленных проблем с модулями и их устареванием/совместимостью/работоспособностью/безопасностью.
                0
                1. На счет предложения использовать отдельные компоненты и конкретно (https://github.com/nazar-pc/CleverStyle-CMS/blob/master/core/classes/Page/Includes_processing.php) — при поверхностном взгляде:
                • строка 77: сработает ли условие если в url путь указан в кавычках? ( url('some/image.jpg'))
                • ф-я `is_relative_path_and_exists` требует доработки, т.к. путь может быть и таким "//yastatic.net/bootstrap/3.1.1/css/bootstrap.min.css"


                2. В админке загрузил первый попавшийся файл в «загрузить и обносить систему» — словил белый экран (http://demo.cscms.org/admin/System/components/modules/update_system).

                дальше смотреть не стал.

                Вообще жаль, что тенденция такова, что каждый PHP разработчик пишет свою CMS, причем повторяя 90% функциональности, идей и ошибок. Направить бы усилия в одно русло. Это я говорю как человек, который тоже прикладывает руки к написанию CMS (не своей, но общественной).
                  0
                  1.
                  • Об кавычках, как одинарных, так и двойных беспокоится строка 79, решил таким образом упростить регулярку.
                  • в `is_relative_path_and_exists` последний компонент проверяет на то, что путь начинается со слэша, если так — то это абсолютный путь (относительно корня сайта, либо двойной для совсем внешних файлов — не важно), который мы игнорируем при минификации, то есть оставляем как есть, он при перемещении сжатого css в другую папку (с кэшем) не сломается

                  2. Это баг не движка, а HHVM, я его обнаружил уже после загрузки демки, баг в несовместимости реализации работы с phar файлами в PHP версии от Zend и HHVM, отрепортил разработчикам: https://github.com/facebook/hhvm/issues/4012, постараюсь заменить HHVM на PHP-FPM, чтобы демка была полнофункциональной.

                  Если ещё что-то заметите — буду рад узнать, если что можно писать в личку.
                    0
                    К стати, позиционирование в Cotonti, похоже, очень похоже на CleverStyle CMS. А вот код выглядит старо, либо мне просто так показалось.

                  Only users with full accounts can post comments. Log in, please.