Немного истории
Разработкой CleverStyle CMS я занимаюсь уже 3 года. Необходимость возникла как раз из-за сложности и неудобства того, с чем я пытался работать. Разработка ведется в свободное время, а так же параллельно с проектами, которые используют CMS (добавляется та функциональность, которой не хватает). Это значит, что добавляются на самом деле нужные функции, под которые есть задачи, а так же именно в том виде, в котором они будут наиболее удобны в использовании. Основная идея — сделать работу очевидных вещей автоматической, а не совсем очевидных — предельно простой, при этом всегда есть возможность повлиять на стандартное поведение и скорректировать его любым нужным образом. Ну и последнее — огромная благодарность хабрасообществу за конструктивную критику прошлой статьи: вы помогли мне пересмотреть свои взгляды на некоторые вещи, стать лучшим программистом и изменить CleverStyle CMS в лучшую сторону.
Окружение
Очевидно, что нужен веб-сервер и БД с реквизитами доступа.
Для установки не нужен composer или ещё какие-то инструменты, даже архиватором пользоваться не нужно. Бросаете дистрибутив в корень будущего сайта и открываете его через браузер. Получаете такое окошко:
Заполнив все поля, получаете готовое к использованию окружение. Дистрибутив сам себя распакует, выставит настройки по умолчанию где нужно и удалит сам себя в целях безопасности. Уже начиная с этого этапа можно заметить простоту, не нужно копировать несколько десятков тысяч мелких файлов по ftp/ssh.
Структура будущего модуля
Со структурой системы можно познакомиться в wiki, но мы будем знакомиться с используемыми частями по ходу статьи.
Сам модуль будет состоять с класса Posts, который будет оберткой над БД и будет предоставлять простой интерфейс для управлением постами. Так же будут собственно страницы, доступные пользователю, API, и в конце концов будет несколько мета-файлов, которые будут объяснять движку некоторые детали работы и позволят собрать модуль в самостоятельно распространяемый дистрибутив.
Для начала создадим директорию components/modules/MyBlog для нашего будущего модуля.
БД
Пост в блоге будет иметь название, содержимое, автора и дату написания. Как приверженец графических инструментов — готовлю структуру в PhpMyAdmin и экспортирую:
CREATE TABLE IF NOT EXISTS `[prefix]myblog_posts` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `user` int(10) unsigned NOT NULL, `title` varchar(1024) NOT NULL, `text` text NOT NULL, `date` bigint(20) unsigned NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Префикс таблицы заменен на [prefix]
для универсальности, CMS подставит вместо него нужный сама.
Создаем файл components/modules/MyBlog/meta/install_db/posts/MySQLi.sql и вставляем туда полученный SQL. Таким образом, при установке модуля в админке будет создана необходимая таблица в БД. Аналогично создадим файл components/modules/MyBlog/meta/uninstall_db/posts/MySQLi.sql:
DROP TABLE `[prefix]myblog_posts`;
MySQLi — название движка БД, он пока единственный,
posts — произвольное название, с которым ассоциируется БД (может быть несколько для разных целей, вплоть до того, что часть таблиц будет на одном сервере в MySQL/MariaDB, а вторая — на другом сервере в PostgreSQL).
Название posts пропишем в components/modules/MyBlog/meta/db.json:
[ "posts" ]
На этом с БД всё. Таблица будет создаваться при установке модуля и удаляться при удалении модуля.
Класс Posts
Класс разместим в файле components/modules/MyBlog/Posts.php и поместим в пространство имен cs\modules\MyBlog — это позволит CMS найти его в случае необходимости.
namespace cs\modules\MyBlog; use cs\Config, cs\Cache\Prefix, cs\DB\Accessor, cs\Language, cs\User, cs\Singleton; /** * Class Posts for posts manipulation * * @method static \cs\modules\MyBlog\Posts instance($check = false) */ class Posts extends Accessor { use Singleton; /** * Cache object instance * * @var Prefix */ protected $cache; protected function construct () { /** * Save instance of cache object with prefix MyBlog (will be added to every item) */ $this->cache = new Prefix('MyBlog'); } /** * Required by abstract Accessor class * * @return int Database index */ protected function cdb () { return Config::instance()->module('MyBlog')->db('posts'); } /** * Get post * * @param int|int[] $id * * @return array|bool */ function get ($id) { if (is_array($id)) { foreach ($id as &$i) { $i = $this->get($i); } return $id; } $id = (int)$id; /** * Try to get item from cache, if not found - get it from database and save in cache */ return $this->cache->get("posts/$id", function () use ($id) { if ($data = $this->db()->qf([ //Readable database, Query, Fetch "SELECT `id`, `user`, `title`, `text`, `date` FROM `[prefix]myblog_posts` WHERE `id` = '%d' LIMIT 1", $id ])) { $L = Language::instance(); $data['datetime'] = $L->to_locale(date($L->_datetime_long, $data['date'])); $data['username'] = User::instance()->username($data['user']); } return $data; }); } /** * Add post * * @param string $title * @param string $text * * @return bool|int Id of created post or <b>false</b> on failure */ function add ($title, $text) { $user = User::instance()->id; //User id $title = xap($title); //XSS filter $text = xap($text, true); //XSS filter, allow html tags $date = TIME; //Current timestamp if ($this->db_prime()->q( //Writable database, Query "INSERT INTO `[prefix]myblog_posts` ( `user`, `title`, `text`, `date` ) VALUES ( '%d', '%s', '%s', '%d' )", $user, $title, $text, $date )) { /** * Delete total count of posts */ unset($this->cache->total_count); return $this->db_prime()->id(); } return false; } /** * Edit post * * @param int $id * @param string $title * @param string $text * * @return bool */ function set ($id, $title, $text) { $id = (int)$id; $title = xap($title); //XSS filter $text = xap($text, true); //XSS filter, allow html tags if ($this->db_prime()->q( //Writable database, Query "UPDATE `[prefix]myblog_posts` SET `title` = '%s', `text` = '%s' WHERE `id` = '%d' LIMIT 1", $title, $text, $id )) { /** * Delete cached item if any */ unset($this->cache->{"posts/$id"}); return true; } return false; } /** * Delete post * * @param int $id * * @return bool */ function del ($id) { $id = (int)$id; if ($this->db_prime()->q( "DELETE FROM `[prefix]myblog_posts` WHERE `id` = '%d' LIMIT 1" )) { /** * Delete cached item if any, and total count of posts */ unset( $this->cache->{"posts/$id"}, $this->cache->total_count ); return true; } return false; } /** * Get posts * * @param $page * * @return int[] */ function posts ($page = 1) { $from = ($page - 1) * 10 ?: 0; return $this->db()->qfas( //Readable database, Query, Fetch, Single, Array "SELECT `id` FROM `[prefix]myblog_posts` ORDER BY `id` DESC LIMIT $from, 10" ) ?: []; } /** * Get total count of posts * * @return int */ function total_count () { return $this->cache->get('total_count', function () { return $this->db()->qfs( //Readable database, Query, Fetch, Single "SELECT COUNT(`id`) FROM `[prefix]myblog_posts`" ) ?: 0; }); } }
Класс является оберткой над БД с кэшированием постов и их общего количества. Это класс одиночка, и имеет такие публичные методы:
- get
- add
- set
- del
- posts
- total_count
Сам класс не занимается проверкой прав доступа, а только корректностью вводимых данных.
Код достаточно просто написан и хорошо прокомментирован, а так же хорошо подхватывается IDE.
Интерфейс пользователя
В общем случае для маршрутизации используется простая json структура, которая описывает адреса страниц внутри модуля. Создадим файл components/modules/MyBlog/index.json который опишет маршрутизацию пользовательской части:
{ "list" : [], "post" : [ "view", "add", "edit", "delete" ] }
Таким образом, пути будут выглядеть так:
- MyBlog/list
- MyBlog/list/{page}
- MyBlog/post/add
- MyBlog/post/view/{id}
Соответственно, в директории модуля создаем следующую структуру файлов:
- list.php
- post/add.php
- post/delete.php
- post/edit.php
- post/view.php
Благодаря тому, что они описаны выше — CMS их вызовет на соответствующих страницах.
Пример файла list.php:
namespace cs\modules\MyBlog; use cs\Config, cs\Page, h; $rc = Config::instance()->route; $page = 1; if (isset($rc[1]) && $rc[1]) { $page = (int)$rc[1]; } $Page = Page::instance(); $Posts = Posts::instance(); $total_count = $Posts->total_count(); $Page->content( h::{'a.cs-button-compact'}( h::icon('plus').' Добавить пост', [ 'href' => 'MyBlog/post/add' ] ) ); if (!$total_count) { $Page->content( h::{'p.cs-center.uk-text-info'}('Пока нет постов') ); return; } $Page->title('Мой блог'); if ($page > 1) { $Page->title("Страница $page"); } $Page->content( h::{'section article.cs-myblog-posts'}( h::{'h1 a[href=MyBlog/post/$i[id]]'}('$i[title]'). h::div('$i[text]'). h::footer('$i[datetime], $i[username]'), [ 'insert' => $Posts->get($Posts->posts($page)) ] ). ( $total_count > 10 ? h::{'div.cs-center'}(pages($page, ceil($total_count / 10), function ($page) { return $page < 2 ? 'MyBlog' : "MyBlog/list/$page"; })) : '' ) );
Пространство имен у нас одно и то же практически во всех файлах модуля.
Config::instance()->route
— позволяет получить индексированный массив элементов пути страницы без учета названия модуля. В данном случае используется для того, чтобы определить какую страницу открывает пользователь, например для MyBlog/list/3
мы получим массив ['list', 3]
. В целом, упомянутые выше файлы являют собой представления + проверку прав доступа.
API
Да, внешний API мы в самом модуле использовать не будем, но всё же сделаем его (может кому-то пригодится). Сделаем самое простое — управление конкретными постами (например, для редактирования постов без перезагрузки страницы). Создадим в components/modules/MyBlog/api несколько файлов:
- index.delete.php
- index.get.php
- index.post.php
- index.put.php
Думаю, названия файлов рассказывают о себе достаточно. В самом простом случае у нас нет никакой структуры и index.json нам не нужен — мы просто создаем пачку index файлов для каждого типа запроса DELETE/GET/POST/PUT и CMS найдет эти файлы сама. Суффиксы можно использовать в API аналогичным образом и при вложенной структуре с index.json. Пример запроса к API:
POST api/MyBlog
{
«title»: «Blog post title»,
«text»: «Blog post content»
}
Можно отправлять JSON, если не забыть указать Content-type: application/json.
В ответ придет либо:
201 Created
…
{
«id»: «5»
}
Либо код и сообщение об ошибке.
Вот файл index.post.php, который этим занимается:
namespace cs\modules\MyBlog; use cs\Page; if (!isset($_POST['title'], $_POST['text'])) { error_code(400); return; } if ($post = Posts::instance()->add($_POST['title'], $_POST['text'])) { code_header(201); Page::instance()->json([ 'id' => $post ]); } else { error_code(500); }
Page::instance()->json()
позволяет отправлять данные как они есть (например массивы), а метод уже сам сделает JSON строку и добавит нужные заголовки. То же самое на счёт error_code()
, просто передайте код ошибки — всё остальное будет сделано автоматически.
Напоследок
Для того, чтобы собрать установочный дистрибутив нашего модуля создадим файл components/modules/MyBlog/meta.json с некоторой служебной информацией:
{ "package" : "MyBlog", "category" : "modules", "version" : "0.0.2", "description" : "Simple demo blog module", "author" : "Nazar Mokrynskyi", "website" : "cleverstyle.org/cms", "license" : "MIT License", "db_support" : [ "MySQLi" ], "provide" : "myblog", "optional" : [ "editor" ], "languages" : [ "Русский" ] }
Как положено — добавим components/modules/MyBlog/license.txt, а так же components/modules/MyBlog/prepare.php и components/modules/MyBlog/languages/Русский.json для того, чтобы сделать красивым заголовок страницы поста и локализировать название модуля.
Ну вот, когда всё готово — берем из репозитория CleverStyle CMS и добавляем себе в корень сайта:
- build
- install
- build.php
- install.php
Добавляем в .htaccess несколько строчек (чтобы движок не перехватывал обращения к этому файлу):
<Files build.php>
RewriteEngine Off
</Files>
Переходим по адресу build.php
, выбираем Module, в списке выбираем MyBlog, кликаем Build.
В корне сайта получаем файл MyBlog_0.0.2.phar.php
, который можно использовать для установки на другой копии системы.
Что можно ещё сделать
Если нужна стилизация постов:
- создать components/modules/MyBlog/includes/.htaccess
Allow from all
RewriteEngine Off - положить в components/modules/MyBlog/includes/css нужные файлы стилей, система их подхватит (аналогично JavaScript в components/modules/MyBlog/includes/js) и
обработаетфайлы в production объединяются и сжимаются с помощью gzip, а в css файлах дополнительно очищаются комментарии, и встраиваются включения такие как импортированные css стили, шрифты, изображения, таким образом всё находится в одном файле
Для этой статьи создан специальный тестовый репозиторий
Надеюсь, такой подход, когда используется минимум сопровождающего кода и структура проще некуда, придется по душе не только мне.
Спасибо за внимание, буду рад вопросам и конструктивной критике.
ссылка на оригинал статьи http://habrahabr.ru/post/196712/
Добавить комментарий