- PVSM.RU - https://www.pvsm.ru -
Давно удивляюсь, как, бывает, усложняют разработку современные фреймворки. Конечно, у меня нет права сказать, что они плохие, но и хорошими я их называть не могу. А всё вот почему: их цель — упростить и ускорить разработку, а так же каким-то образом стандартизировать и структурировать проект. Но, по моему скромному субъективному мнению, с первой половиной порой получается прямо противоположный эффект, пишется много кода, который сам по себе ничего не делает, а только обслуживает основной код. Эта статья — пример иного подхода к задаче разработки простого блога, используя не Zend Framework 2, как это сделал [1] rrromka [2], а собственную разработку CleverStyle CMS.
Разработкой CleverStyle CMS я занимаюсь уже 3 года. Необходимость возникла как раз из-за сложности и неудобства того, с чем я пытался работать. Разработка ведется в свободное время, а так же параллельно с проектами, которые используют CMS (добавляется та функциональность, которой не хватает). Это значит, что добавляются на самом деле нужные функции, под которые есть задачи, а так же именно в том виде, в котором они будут наиболее удобны в использовании. Основная идея — сделать работу очевидных вещей автоматической, а не совсем очевидных — предельно простой, при этом всегда есть возможность повлиять на стандартное поведение и скорректировать его любым нужным образом. Ну и последнее — огромная благодарность хабрасообществу за конструктивную критику прошлой статьи [3]: вы помогли мне пересмотреть свои взгляды на некоторые вещи, стать лучшим программистом и изменить CleverStyle CMS в лучшую сторону.
Очевидно, что нужен веб-сервер и БД с реквизитами доступа.
Для установки не нужен composer или ещё какие-то инструменты, даже архиватором пользоваться не нужно. Бросаете дистрибутив в корень будущего сайта и открываете его через браузер. Получаете такое окошко:
Заполнив все поля, получаете готовое к использованию окружение. Дистрибутив сам себя распакует, выставит настройки по умолчанию где нужно и удалит сам себя в целях безопасности. Уже начиная с этого этапа можно заметить простоту, не нужно копировать несколько десятков тысяч мелких файлов по ftp/ssh.
Со структурой системы можно познакомиться в wiki [4], но мы будем знакомиться с используемыми частями по ходу статьи.
Сам модуль будет состоять с класса Posts, который будет оберткой над БД и будет предоставлять простой интерфейс для управлением постами. Так же будут собственно страницы, доступные пользователю, API, и в конце концов будет несколько мета-файлов, которые будут объяснять движку некоторые детали работы и позволят собрать модуль в самостоятельно распространяемый дистрибутив.
Для начала создадим директорию components/modules/MyBlog [5] для нашего будущего модуля.
Пост в блоге будет иметь название, содержимое, автора и дату написания. Как приверженец графических инструментов — готовлю структуру в 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 [6] и вставляем туда полученный SQL. Таким образом, при установке модуля в админке будет создана необходимая таблица в БД. Аналогично создадим файл components/modules/MyBlog/meta/uninstall_db/posts/MySQLi.sql [7]:
DROP TABLE `[prefix]myblog_posts`;
MySQLi — название движка БД, он пока единственный,
posts — произвольное название, с которым ассоциируется БД (может быть несколько для разных целей, вплоть до того, что часть таблиц будет на одном сервере в MySQL/MariaDB, а вторая — на другом сервере в PostgreSQL).
Название posts пропишем в components/modules/MyBlog/meta/db.json [8]:
[
"posts"
]
На этом с БД всё. Таблица будет создаваться при установке модуля и удаляться при удалении модуля.
Класс разместим в файле components/modules/MyBlog/Posts.php [9] и поместим в пространство имен csmodulesMyBlog — это позволит CMS найти его в случае необходимости.
namespace csmodulesMyBlog;
use csConfig,
csCachePrefix,
csDBAccessor,
csLanguage,
csUser,
csSingleton;
/**
* Class Posts for posts manipulation
*
* @method static csmodulesMyBlogPosts 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;
});
}
}
Класс является оберткой над БД с кэшированием постов и их общего количества. Это класс одиночка [10], и имеет такие публичные методы:
Сам класс не занимается проверкой прав доступа, а только корректностью вводимых данных.
Код достаточно просто написан и хорошо прокомментирован, а так же хорошо подхватывается IDE.
В общем случае для маршрутизации используется простая json структура, которая описывает адреса страниц внутри модуля. Создадим файл components/modules/MyBlog/index.json [11] который опишет маршрутизацию пользовательской части:
{
"list" : [],
"post" : [
"view",
"add",
"edit",
"delete"
]
}
Таким образом, пути будут выглядеть так:
Соответственно, в директории модуля создаем следующую структуру файлов:
Благодаря тому, что они описаны выше — CMS их вызовет на соответствующих страницах.
Пример файла list.php [12]:
namespace csmodulesMyBlog;
use csConfig,
csPage,
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 мы в самом модуле использовать не будем, но всё же сделаем его (может кому-то пригодится). Сделаем самое простое — управление конкретными постами (например, для редактирования постов без перезагрузки страницы). Создадим в components/modules/MyBlog/api [13] несколько файлов:
Думаю, названия файлов рассказывают о себе достаточно. В самом простом случае у нас нет никакой структуры и 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 [14], который этим занимается:
namespace csmodulesMyBlog;
use csPage;
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 [15] с некоторой служебной информацией:
{
"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 [16], а так же components/modules/MyBlog/prepare.php [17] и components/modules/MyBlog/languages/Русский.json [18] для того, чтобы сделать красивым заголовок страницы поста и локализировать название модуля.
Ну вот, когда всё готово — берем из репозитория CleverStyle CMS [19] и добавляем себе в корень сайта:
Добавляем в .htaccess несколько строчек (чтобы движок не перехватывал обращения к этому файлу):
<Files build.php>
RewriteEngine Off
</Files>
Переходим по адресу build.php
, выбираем Module, в списке выбираем MyBlog, кликаем Build.
В корне сайта получаем файл MyBlog_0.0.2.phar.php
, который можно использовать для установки на другой копии системы.
Если нужна стилизация постов:
Allow from all
RewriteEngine Off
Для этой статьи создан специальный тестовый репозиторий [20]
Надеюсь, такой подход, когда используется минимум сопровождающего кода и структура проще некуда, придется по душе не только мне.
Спасибо за внимание, буду рад вопросам и конструктивной критике.
Автор: nazarpc
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/cms/45661
Ссылки в тексте:
[1] как это сделал: http://habrahabr.ru/post/192522/
[2] rrromka: http://habrahabr.ru/users/rrromka/
[3] прошлой статьи: http://habrahabr.ru/post/186056/
[4] wiki: https://github.com/nazar-pc/CleverStyle-CMS/wiki
[5] components/modules/MyBlog: https://github.com/nazar-pc/MyBlog/tree/master/components/modules/MyBlog
[6] components/modules/MyBlog/meta/install_db/posts/MySQLi.sql: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/meta/install_db/posts/MySQLi.sql
[7] components/modules/MyBlog/meta/uninstall_db/posts/MySQLi.sql: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/meta/uninstall_db/posts/MySQLi.sql
[8] components/modules/MyBlog/meta/db.json: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/meta/db.json
[9] components/modules/MyBlog/Posts.php: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/Posts.php
[10] одиночка: http://ru.wikipedia.org/wiki/Одиночка_(шаблон_проектирования)
[11] components/modules/MyBlog/index.json: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/index.json
[12] list.php: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/list.php
[13] components/modules/MyBlog/api: https://github.com/nazar-pc/MyBlog/tree/master/components/modules/MyBlog/api
[14] index.post.php: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/api/index.post.php
[15] components/modules/MyBlog/meta.json: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/meta.json
[16] components/modules/MyBlog/license.txt: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/license.txt
[17] components/modules/MyBlog/prepare.php: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/prepare.php
[18] components/modules/MyBlog/languages/Русский.json: https://github.com/nazar-pc/MyBlog/blob/master/components/modules/MyBlog/languages/Русский.json
[19] репозитория CleverStyle CMS: https://github.com/nazar-pc/CleverStyle-CMS
[20] специальный тестовый репозиторий: https://github.com/nazar-pc/MyBlog
[21] Источник: http://habrahabr.ru/post/196712/
Нажмите здесь для печати.