Пишем простой, расширяемый движок. Начало.

Решил поделиться своими знаниями :) CMS на основе этого примера Я написал в 2007 году. После этого она претерпела кучу изменений. Внимание. Мне лень разбираться, почему тут конвертируются кавычки. Так что не забудьте изменить кавычки на стандартные. Ну и да. Тут может быть много ошибок. Потому что пишу Я это почти по памяти и не проверяю (да-да, Я вот такой вот хитрый и злой). Да и код оформлен в списках, что не шибко может быть удобно злобным копипастерам! :) Комментарии приветствуются.

Задача: написать несложную систему управления сайтом, чтобы можно было быстренько дописать нужные модули и внедрить их в систему.

Средства: крутиться это всё будет под Apache‘м в связке с PHP5 (ибо будет использоваться шаблонизатор на основе XSLT) должны быть установлены модули MySQLi, DOM) и MySQL‘ем. Разрабатываю Я на Eclipse IDE с утсановленным плагином PDT.  Я предполагаю, что те, кто это будут читать уже умеют поднять AMP на какой-либо платформе :) Если нет, то сначала подумайте — а надо ли оно вам? Если надо, Я как-нибудь распишу, как всё настроить :)

Что получим:

Это будет простенькая CMS, которая будет написана на PHP5 с использованием ООП. В качестве шаблонизатора мы будем использовать XSLT. Т.е. с помощью PHP подготавливаем XML’ку для последующей обработки XSL. В результате получаем HTML, который и засовываем в нужное место на страничке. Система будет работать используя mod_rewrite и поддерживать альясы страниц.

 

Создаём БД.

Нам потребуется пара табличек в БД. У всех таблиц будут префиксы, чтобы можно было избежать конфликта с другими похожими таблицами в базе.

Во-первых, надо будет где-то хранить обработчиков наших страниц. Обработчиками назовём те классы, который будут создавать нам страницу какого-то определённого типа. Т.е. новостную. Или карту сайта, к примеру. Можно это делать в одном файле, и его подключать. Недостаток этого в том, что каждый раз при добавлении нового обработчика надо будет закачивать обновлённый файл на сервер. При этом если другой программист добавит туда свой обработчик, а вы об этом знать не будете — вы его затрёте. И будет lulz :)

Во-вторых, надо будет где-то хранить сами страницы.

Итак, приступим. Создаём таблицу, где будем хранить обработчики и таблицу страниц:

CREATE TABLE `e_handlers` (
  `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, // первичный ключ
  `classname` varchar(50) NOT NULL,  // имя класса обработчика
  `classpath` varchar(255) NOT NULL,  // имя файла класса
  `description` varchar(255) DEFAULT '',  // описание, к примеру "простая страница"
  `enable` tinyint(4) DEFAULT 1 // доступен для создания страниц или нет
);
CREATE TABLE `e_pages` (
 `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, // первичный ключ
 `id_parent` int(11) DEFAULT '0',  // родитель страницы. для вложенности
 `id_handler` int(11) NOT NULL,  // ID обработчика страницы
 `is_mainpage` enum('0','1') DEFAULT '0',  // если это главная страница
 `alias` varchar(255) DEFAULT NULL,  // альяс страницы
 `title` varchar(255) DEFAULT NULL, // заголовок страницы
 `keywords` varchar(255) DEFAULT NULL,  // ключевые слова
 `description` varchar(255) DEFAULT NULL, // описание страницы
 `body` text, // собственно текст страницы
 `in_menu` enum('0','1') DEFAULT '1',  // показывать в меню сайта
 `title_menu` varchar(255) DEFAULT NULL,  // заголовок для меню
 `in_map` enum('0','1') DEFAULT '1',  // показывать в карте сайта
);

После чего добавляем вручную (админки то у нас нет ;( ) два обработчика и две страницы для начала работы. Один обработчик — это типовые страницы (назовём их так). Ну или просто страницы, которых можно наплодить сколько угодно и назовём его Parent. Второй обработчик — это обработчик главной страницы, назовём его Mainpage. Ведь мы можем захотеть, чтобы главная страница у нас отличалась от всех остальных, правда? И соотстветственно две страницы — просто страница и главная страница. Вот собственно и две самые необходимые нам таблицы.

mysql> INSERT INTO `e_handlers` VALUES(0,'Parent', 'Parent.class.php','Типовая страница',1); --обработчик Parent
mysql> INSERT INTO `e_handlers` VALUES(0,'Mainpage', 'Mainpage.class.php','Главная страница',1); --обработчик Mainpage
mysql> INSERT INTO `e_pages` VALUES(0,0,2,1,'','Главная страница сайта','','','Текст главной страницы','1','На главную','1'); --создаём главную страницу
mysql> INSERT INTO `e_pages` VALUES(0,0,2,0,'','страница сайта','','','Текст тестовой страницы страницы','1','Какая-то страничка','1'); --создаём тестовую страницу

Далее создаём структуру каталогов на диске

/ - корень проекта
/admin - административная часть
/class - папка с классами
/template - папка с шаблонами

Начинаем программирование :)

Создаём в корне файлы index.php, config.php, load.php, functions.php.

config.php:

  1. $const             = «»; // доступные константы (будет позже)
  2. $domain         = «http://localhost:2000/»; // домен, на котором крутится сайт
  3. $pfx            = «e»; // префикс базы данных
  4. $charset        = «UTF-8»; // кодировка для подстановки в html
  5. $db_charset        = «UTF8»; // кодировка для подключения к БД
  6. $db_user        = «user»; // имя пользователя БД
  7. $db_pass        = «user»; // пароль
  8. $db_host        = «localhost»; // хост БД
  9. $db_name        = «engine»; // сама БД
  10. $db = new mysqli(&$db_host, &$db_user, &$db_pass, &$db_name);
  11. $db->query(«SET NAMES {$db_charset}»);
  12. // загружаем все классы
  13. $classes_query_str = «SELECT `classpath`,`classname` FROM `{$pfx}_handlers`»;
  14. $classes_query = $db->query($classes_query_str);
  15. while($classes = $classes_query->fetch_object()) {
  16. if(file_exists($_SERVER[‘DOCUMENT_ROOT’].’/class/’.$classes->classpath))
  17. require_once $_SERVER[‘DOCUMENT_ROOT’].’/class/’.$classes->classpath;
  18. else
  19. exit(«Class {$classes->classname} not found»);
  20. }

index.php:

  1. <?php
  2. require_once ‘load.php’;
  3. ?>

load.php:

  1. <?php
  2. session_start();
  3. require_once $_SERVER[‘DOCUMENT_ROOT’].’/config.php’; // конфиг
  4. require_once $_SERVER[‘DOCUMENT_ROOT’].’/functions.php’; // файл с общими функциями
  5. // подключаем классы
  6. $classes_query_str = «SELECT `classpath` FROM `{$pfx}_handlers`»;
  7. $classes_query = $db->query($classes_query_str);
  8. while($classes = $classes_query->fetch_object())
  9. require_once $_SERVER[‘DOCUMENT_ROOT’].’/’.$classes->classpath;
  10. // создаем перменные альясов, страницы, и id страницы
  11. isset($_GET[‘page’]) ? $page = clearVar($_GET[‘page’]) : $page = 1;
  12. isset($_GET[‘alias’]) ? $alias = clearVar($_GET[‘alias’]) : $alias = »;
  13. isset($_GET[‘uid’]) ? $uid = clearVar($_GET[‘uid’]) : $uid = 0;
  14. $page = (int)$page;
  15. $uid = (int)$uid;
  16. //получаем информацию о странице
  17. $info_page = getPageInfo($alias,$uid,$page);
  18. // теперь вызываем класс
  19. if($info_page) {
  20. // ищем нужный обработчик
  21. $query_handler_str = «SELECT `classname` FROM `{$pfx}_handlers` WHERE `id` ={$info_page[‘id_handler’]}»;
  22. $query_handler = $db->query($query_handler_str);
  23. if($query_handler->num_rows < 1)
  24. exit(‘Error: no such handler’);
  25. else {
  26. $handler = $query_handler->fetch_object();
  27. $page = new $handler->classname($info_page);
  28. print $page->getContent();
  29. }
  30. }

В файл functions.php добавляем следующие функции:

  1. /**
  2. * Выводит информацию о переменной
  3. * @param $text информация для вывода
  4. * @param $exit прекращать работу после вывода
  5. */
  6. function vardump($text, $exit = false) {
  7. print ‘<pre>’;
  8. var_dump($text);
  9. print ‘</pre>’;
  10. if($exit)
  11. exit;
  12. }
  13. /**
  14. * Вырезает из переменной ненужные символы
  15. * @param $var переменная для очистки
  16. * @return чистая переменная
  17. */
  18. function clearVar($var) {
  19. return str_replace(‘»‘,»»,str_replace(«‘»,»»,stripcslashes(trim(htmlspecialchars($var)))));
  20. }
  21. /**
  22. * Получаем информацию по странице
  23. * @param $alias альяс для страницы в базе данных
  24. * @param $uid id страницы в базе данных
  25. * @param $page номер страницы в базе данных
  26. * @return массив со всеми значениями
  27. */
  28. function getPageInfo($alias,$uid,$page) {
  29. global $db,$pfx;
  30. // если установлен alias
  31. if($alias)
  32. $query_page_str = «SELECT * FROM `{$pfx}_pages` WHERE `alias` = ‘{$alias}’ AND `is_delete` = ‘0’»; // запрос по альясу
  33. else // иначе
  34. $query_page_str = «SELECT * FROM `{$pfx}_pages` WHERE `is_mainpage` = ‘1’»; // запрос главной страницы
  35. $query_page = $db->query(&$query_page_str);
  36. if($query_page->num_rows < 1) { // а если ничего нету
  37. if($uid) {
  38. $query_page_str = «SELECT * FROM `{$pfx}_pages` WHERE `id` = {$uid}»;  // пытаемся найти страницу с таким id
  39. $query_page = $db->query(&$query_page_str);
  40. if($query_page->numRows < 1) {
  41. print ‘No pages found by UID ;(‘;
  42. return false;
  43. }
  44. }
  45. else  {
  46. print ‘No pages found ;(‘;
  47. return false;
  48. }
  49. }
  50. $info = $query_page->fetch_object();
  51. ($uid == 0) ? $info->page_id = 0 : $info->page_id = &$uid;
  52. $info->page_alias = &$alias;
  53. $info->page_pagenum = &$page;
  54. return $info;
  55. }

 

Теперь в папке /class создаём два файла:

Parent.class.php:

  1. <?php
  2. class Parent {
  3. var $template_page = ‘template/Parent/page.php’;
  4. var $pageinfo = »;
  5. var $template_menu = ‘template/Parent/menu.xsl’;
  6. /**
  7. * устанавливает данные о странице, которые будут доступны в классе
  8. */
  9. function __construct($info) {
  10. $this->pageinfo = &$info;
  11. }
  12. /**
  13. * Составляет страницу
  14. * @return unknown_type
  15. */
  16. function getContent() {
  17. global $domain;
  18. ob_start();
  19. $head =  $this->getDoctype();
  20. $head .= $this->getTitle();
  21. $head .= $this->getMetaInf();
  22. $head .= $this->getIncludes();
  23. $menu = $this->getMenu();
  24. $body = $this->getBody();
  25. require_once $this->template_page;
  26. $page = ob_get_contents();
  27. $page .= $this->getLastTag();
  28. ob_clean();
  29. $page = str_replace(«~/»,$domain,$page);
  30. print $page;
  31. }
  32. /**
  33. * Возвращает DOCTYPE
  34. * @return XHTML1.0 DOCTYPE
  35. */
  36. function getDoctype() {
  37. return ‘<!DOCTYPE html PUBLIC «-//W3C//DTD XHTML 1.0 Transitional//EN» «http://www.w3.org/TR/xhtml1
  38. /DTD/xhtml1-transitional.dtd»>’.»n»;
  39. }
  40. /**
  41. * Возвращает заголовок страницы
  42. * @return заголовок страницы
  43. */
  44. function getTitle() {
  45. global $db,$pfx;
  46. $head = ‘<html xmlns=»http://www.w3.org/1999/xhtml» xml:lang=»en» lang=»ru»>’.»n»;
  47. $head .= «t<head>n»;
  48. $title_query_str = «SELECT `title`,`title_menu` FROM `{$pfx}_pages` WHERE `id` = {$this->pageinfo[‘id’]}»;
  49. $title_query = $db->query(&$title_query_str);
  50. $title = $title_query->fetch_object;
  51. isset($title->title) ? $title_str = $title->title : $title_str = $title->title_menu;
  52. $head .= «tt<title>{$title_str}</title>n»;
  53. return $head;
  54. }
  55. /**
  56. * Добавляет META информацию на страницу: кодировка, автор, генератор, ключевые слова, описание
  57. * @return META тэги
  58. */
  59. function getMetaInf() {
  60. global $pfx,$db,$charset;
  61. $query_meta_str = «SELECT `keywords`,`description` FROM `{$pfx}_pages` WHERE `id` =
  62. {$this->pageinfo[‘id’]} «;
  63. $query_meta = $db->query(&$query_meta_str);
  64. $meta = $query_meta->fetch_object();
  65. $kw = &$meta->keywords;
  66. $dsc = &$meta->description;
  67. $meta = «tt<meta name=»Author» content=»Maksim Voloshin» />n»;
  68. $meta .= «tt<meta name=»GENERATOR» content=»The best engine :) » />n»;
  69. $meta .= «tt<meta name=»Author e-mail» content=»me@maksimvoloshin.ru» />n»;
  70. $meta .= «tt<meta http-equiv=»Content-Type» content=»text/html;charset={$charset}» />n»;
  71. $meta .= «tt<meta name=»keywords» content=»{$kw}» />n»;
  72. $meta .= «tt<meta name=»description» content=»{$dsc}» />n»;
  73. return $meta;
  74. }
  75. /**
  76. * Подключает CSS и javascript файлы
  77. * @return нужные тэги
  78. */
  79. function getIncludes() {
  80. $style = «tt<link rel=»Stylesheet» href=»~/template/style.css» type=»text/css» />n»;
  81. $style .= «tt<link rel=’favorite icon’ type=’image/gif’ href=’~/favicon.gif’ />n»;
  82. $js = «tt».'<script type=»text/javascript» language=»JavaScript» src=»~/template/script.js»></script>’.»n»;
  83. $closehead = «t</head>nt<body>n»;
  84. return $style.$js.$closehead;
  85. }
  86. function getMenu() {
  87. global $pfx,$db;
  88. $query_menu_str = «SELECT `id`,`title_menu`,`title`,`alias` FROM `{$pfx}_pages` WHERE `in_menu` = ‘1’»;
  89. $query_menu = $db->query(&$query_menu_str);
  90. $doc = new DOMDocument(«1.0″,»utf-8»);
  91. $rootnode = $doc->createElement(«root»);
  92. $root = $doc->appendChild($rootnode);
  93. while($menu = $query_menu->fetch_object())) {
  94. $menuitem = $doc->createElement(«menu»);
  95. if($this->pageinfo[‘id’] == $menu->id)
  96. $menuitem->setAttribute(«active»,»active_menu»);
  97. else
  98. $menuitem->setAttribute(«active»,»nonactive_menu»);
  99. if(!empty($menu->alias))
  100. $menuitem->setAttribute(«link»,»~/».$menu->alias.»/»);
  101. else
  102. $menuitem->setAttribute(«link»,»~/page/».$menu->id.»/»);
  103. if(!empty($menu->title_menu))
  104. $menuitem->appendChild($doc->createTextNode($menu->title_menu));
  105. else
  106. $menuitem->appendChild($doc->createTextNode($menu->title));
  107. $root->appendChild($menuitem);
  108. }
  109. $xsl = new DOMDocument;
  110. $xsl->load($this->template_menu);
  111. $xslt = new XSLTProcessor;
  112. $xslt->importStylesheet($xsl);
  113. return $xslt->transformToXML($doc);
  114. }
  115. /**
  116. * Переопредлять в новых классах
  117. * @return
  118. */
  119. function getBody() {
  120. global $db,$pfx;
  121. $query_body = $db->query(«SELECT `body` FROM `{$pfx}_pages` WHERE `id` = {$this->pageinfo[‘id’]}»);
  122. $body_arr = $query_body->fetch_object();
  123. return $body_arr->body;
  124. }
  125. }

Теперь создаём в папке /template папку Parent, в ней создаём файлы page.php и menu.xsl. Первый будет шаблоном страницы, второй будет шаблоном меню.

page.php:

  1. <?php echo $head; ?>
  2. <?php echo $menu; ?>
  3. <div>
  4. <?php echo $body; ?>
  5. </div>

menu.xsl:

  1. <?xml version=»1.0″ encoding=»UTF-8″?>
  2. <xsl:stylesheet version=»1.0″ xmlns:xsl=»http://www.w3.org/1999/XSL/Transform»>
  3. <xsl:output method=»xml» omit-xml-declaration=»yes» />
  4. <xsl:template match=»/root/menu»>
  5. <div>
  6. <a href=»{@link}» class=»{@active}»>
  7. <xsl:value-of select=»text()» />
  8. </a>
  9. </div>
  10. </xsl:template>
  11. </xsl:stylesheet>

Так как у нас в системе зарегистрирован класс для главной страницы, просто определяем его. Создаём в папке /class файл Mainpage.class.php:

  1. <?php
  2. class Mainpage extends Parent {
  3. }
  4. ?>

Работать над ним мы будем как-нибудь потом. В этом классе, к примеру, можно переопределить шаблон старницы $template_page и в результате будет использоваться он, а не тот, который задан в Parent.

Осталось определить правила rewrite’а. Для этого в директиве DocumentRoot апачи должно стоять AllowOverride All. Создаём файл .htaccess в корне нашего сайта со следующим содержимым:

  1. RewriteEngine On
  2. #страницы
  3. RewriteRule ^(.*)/(.*)/$ index.php?alias=$1&uid=$2 [QSA,L]
  4. RewriteRule ^(.*)/$ index.php?alias=$1 [QSA,L]

QSA — позволит нам добавлять к этому правилу GET переменные из запроса. Т.е. /page/1/?a=1 превратится в index.php?alias=page&uid=1&a=1.

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

И да. Если сайт лежит не в корне — то надо сделать RewriteBase /dirname или добавлять dirname в правила.

Ещё можно создать в папке /template файлы style.css (и определить там active_menu и nonactive_menu) и script.js — а то firebug ругаться будет (у вас ведь стоит firebug, а то как же вы верстаете? :) )

Для чего Я сделал функцию vardump — смотри тут

Ну-с. Момент истины. Заходим на http://localhost и видим либо меню из двух пунктов и текст: «Текст главной страницы», либо кучу ошибок, которые можно постить тут :)

 

P.S. Всем доброго утра =D

$db = new mysqli(&$db_host, &$db_user, &$db_pass, &$db_name);

Читайте также:

комментариев 6

  1. Марат:

    Движок не работает. Куча ошибок и предупреждений.
    https://prnt.sc/ikcg1j
    https://prnt.sc/ikcgaa
    https://cloud.mail.ru/public/7hjx/gYYs4QUM7
    Можешь подправить всё, чтобы заработало и отправить архивчик на мыло?

  1. 08 Сен 2011

    […] Скоро будет ещё одна статья про создание простого раширяемого движка. […]

  2. 23 Мар 2015

    […] Скоро будет ещё одна статья про создание простого раширяемого движка. […]

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *