Текущие изменения в коде и архитектуре движка

Этот пост больше предназначен для тех, кто использует ЛС на уровне кода. Постараюсь ввести в курс того, что происходит сейчас в SVN проекта над кодом и архитектурой.

Сейчас наметились три основные изменения:
  • изменения логики работы с БД и кешем
  • создание универсальных комментариев
  • возможность обрабатывать Ajax запросы прямо в экшенах

Теперь более подробно о каждом.

Изменения логики работы с БД и кешем.
Сюда входит избавление в запросах к БД от JOIN'ов, теперь при запросе тянутся только ID необходимых сущностей, а дальше в модулях они(сущности) «обрастают» необходимыми данными. В результате мы получаем один запрос на выборку списка основных данных и по одному запросу на каждый набор дополнительных данных. Дополнительные наборы данных получаются путем мульти-запросов к кешу и БД. Преимущества такого подхода в том, что данные будут более активно кешироваться, а запросы к БД быстрее исполняться. Например, теперь при голосовании за топик кеш страницы со списком топиков сбрасываться не будет, сбросится только кеш конкретного топика.

В коде это выглядит так:
получение автора блога из сущности топика
$oTopic->getBlog()->getOwner()
получение дополнительных данных

	public function GetTopicsAdditionalData($aTopicId,$aAllowData=array('user','blog','vote','favourite','comment_new')) {
		func_array_simpleflip($aAllowData);
		if (!is_array($aTopicId)) {
			$aTopicId=array($aTopicId);
		}
		/**
		 * Получаем "голые" топики
		 */
		$aTopics=$this->GetTopicsByArrayId($aTopicId);
		/**
		 * Формируем ID дополнительных данных, которые нужно получить
		 */
		$aUserId=array();
		$aBlogId=array();		
		foreach ($aTopics as $oTopic) {
			if (isset($aAllowData['user'])) {
				$aUserId[]=$oTopic->getUserId();
			}
			if (isset($aAllowData['blog'])) {
				$aBlogId[]=$oTopic->getBlogId();
			}			
		}
		/**
		 * Получаем дополнительные данные
		 */
		$aTopicsVote=array();
		$aFavouriteTopics=array();
		$aTopicsQuestionVote=array();
		$aTopicsRead=array();
		$aUsers=isset($aAllowData['user']) && is_array($aAllowData['user']) ? $this->User_GetUsersAdditionalData($aUserId,$aAllowData['user']) : $this->User_GetUsersAdditionalData($aUserId);
		$aBlogs=isset($aAllowData['blog']) && is_array($aAllowData['blog']) ? $this->Blog_GetBlogsAdditionalData($aBlogId,$aAllowData['blog']) : $this->Blog_GetBlogsAdditionalData($aBlogId);		
		if (isset($aAllowData['vote']) and $this->oUserCurrent) {
			$aTopicsVote=$this->GetTopicsVoteByArray($aTopicId,$this->oUserCurrent->getId());			
			$aTopicsQuestionVote=$this->GetTopicsQuestionVoteByArray($aTopicId,$this->oUserCurrent->getId());
		}	
		if (isset($aAllowData['favourite']) and $this->oUserCurrent) {
			$aFavouriteTopics=$this->GetFavouriteTopicsByArray($aTopicId,$this->oUserCurrent->getId());	
		}
		if (isset($aAllowData['comment_new']) and $this->oUserCurrent) {
			$aTopicsRead=$this->GetTopicsReadByArray($aTopicId,$this->oUserCurrent->getId());	
		}
		/**
		 * Добавляем данные к результату - списку топиков
		 */
		foreach ($aTopics as $oTopic) {
			if (isset($aUsers[$oTopic->getUserId()])) {
				$oTopic->setUser($aUsers[$oTopic->getUserId()]);
			} else {
				$oTopic->setUser(null); // или $oTopic->setUser(new UserEntity_User());
			}
			if (isset($aBlogs[$oTopic->getBlogId()])) {
				$oTopic->setBlog($aBlogs[$oTopic->getBlogId()]);
			} else {
				$oTopic->setBlog(null); // или $oTopic->setBlog(new BlogEntity_Blog());
			}
			if (isset($aTopicsVote[$oTopic->getId()])) {
				$oTopic->setUserIsVote(true);
				$oTopic->setUserVoteDelta($aTopicsVote[$oTopic->getId()]->getDelta());
			} else {
				$oTopic->setUserIsVote(false);
			}
			if (isset($aFavouriteTopics[$oTopic->getId()])) {
				$oTopic->setIsFavourite(true);
			} else {
				$oTopic->setIsFavourite(false);
			}			
			if (isset($aTopicsQuestionVote[$oTopic->getId()])) {
				$oTopic->setUserQuestionIsVote(true);
			} else {
				$oTopic->setUserQuestionIsVote(false);
			}
			if (isset($aTopicsRead[$oTopic->getId()]))	{		
				$oTopic->setCountCommentNew($oTopic->getCountComment()-$aTopicsRead[$oTopic->getId()]->getCommentCountLast());
				$oTopic->setDateRead($aTopicsRead[$oTopic->getId()]->getDateRead());
			} else {
				$oTopic->setCountCommentNew(0);
				$oTopic->setDateRead(date("Y-m-d H:i:s"));
			}						
		}
		return $aTopics;
	}

	public function GetTopicsByArrayId($aTopicId) {
		if (!is_array($aTopicId)) {
			$aTopicId=array($aTopicId);
		}
		$aTopicId=array_unique($aTopicId);
		$aTopics=array();
		$aTopicIdNotNeedQuery=array();
		/**
		 * Делаем мульти-запрос к кешу
		 */
		$aCacheKeys=func_build_cache_keys($aTopicId,'topic_');
		if (false !== ($data = $this->Cache_Get($aCacheKeys))) {			
			/**
			 * проверяем что досталось из кеша
			 */
			foreach ($aCacheKeys as $sValue => $sKey ) {
				if (array_key_exists($sKey,$data)) {	
					if ($data[$sKey]) {
						$aTopics[$data[$sKey]->getId()]=$data[$sKey];
					} else {
						$aTopicIdNotNeedQuery[]=$sValue;
					}
				} 
			}
		}
		/**
		 * Смотрим каких топиков не было в кеше и делаем запрос в БД
		 */		
		$aTopicIdNeedQuery=array_diff($aTopicId,array_keys($aTopics));		
		$aTopicIdNeedQuery=array_diff($aTopicIdNeedQuery,$aTopicIdNotNeedQuery);		
		$aTopicIdNeedStore=$aTopicIdNeedQuery;
		if ($data = $this->oMapperTopic->GetTopicsByArrayId($aTopicIdNeedQuery)) {
			foreach ($data as $oTopic) {
				/**
				 * Добавляем к результату и сохраняем в кеш
				 */
				$aTopics[$oTopic->getId()]=$oTopic;
				$this->Cache_Set($oTopic, "topic_{$oTopic->getId()}", array("topic_update_{$oTopic->getId()}"), 60*60*24*4);
				$aTopicIdNeedStore=array_diff($aTopicIdNeedStore,array($oTopic->getId()));
			}
		}
		/**
		 * Сохраняем в кеш запросы не вернувшие результата
		 */
		foreach ($aTopicIdNeedStore as $sId) {
			$this->Cache_Set(null, "topic_{$sId}", array("topic_update_{$sId}"), 60*60*24*4);
		}	
		/**
		 * Сортируем результат согласно входящему массиву
		 */
		$aTopics=func_array_sort_by_keys($aTopics,$aTopicId);
		return $aTopics;		
	}
Думаю всё должно быть понятно :) Но есть один нюанс, библиотека dkCache не поддерживает мульти-запросы к кешу через memcache. Поэтому сейчас работаю псевдо мульти-запросы, в дальнейшем планирую доработать её, т.к. в этом огромный плюс и memcache этим выгодно отличается от файлового кеширования. Так что обладателям файлового кеша придеться смириться с возросшим в разы числом запросов к кешу.

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

Возможность обрабатывать Ajax запросы прямо в экшенах.
В экшены добавлен функционал по обработке Ajax запросов и отдачи выходных данных в формате jsHttpRequest и JSON. Это позволит более удобно писать ajax обработчики и в ряде случаев не дублировать код. Пример обработки ajax запроса в экшене можно посмотреть на примере добавление комментариев в ActionBlog.class.php. Причем добавление комментария может происходит и с отключенным JavaScript у клиента. Во Viiew появились для этого новые методы:

	/**
	 * Устанавливает тип отдачи при ajax запросе, если null то выполняется обычный вывод шаблона в браузер
	 *
	 * @param unknown_type $sResponseAjax
	 */
	public function SetResponseAjax($sResponseAjax='jsHttpRequest') {
		/**
		 * Проверка на безопасную обработку ajax запроса
		 */
		if ($sResponseAjax) {
			$this->Security_ValidateSendForm();
		}		
		$this->sResponseAjax=$sResponseAjax;
	}

	/**
	 * Загружаем переменную в ajax ответ
	 *
	 * @param unknown_type $sName
	 * @param unknown_type $value
	 */
	public function AssingAjax($sName,$value) {
		$this->aVarsAjax[$sName]=$value;
	}
А использовать это так:

	/**
	 * Обработка добавление комментария к топику через ajax
	 *
	 */
	protected function AjaxAddComment() {
		$this->Viewer_SetResponseAjax();
		$this->SubmitComment(); // здесь происходит обычная обработка добавления комментария
	}


Вот такие дела сейчас происходят :) Принимаю вопросы и предложения по сабжу и вообще по архитектуре.

UPD также меня постоянно посещают мысли избавиться от констант в конфиге и вынести настройки в массив $CONFIG либо БД :)

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

avatar
Вроде всё нормально, только движок превращается в нечто монструозное с кешем, который можно будет запускать чуть ли только на VPS как минимум :)

А по поводу архитектуры — вроде пока всё утраивает. от только вопрос. Почему не все конфиги модулей обрабатываются? Конкретно константы.

зы. и правильный ход в сторону упрощения и систематизации AJAX. В теории можно в будущем вообще всё подгружать частично кусками шаблонов.
avatar
Почему не все конфиги модулей обрабатываются? Конкретно константы.
подробнее
avatar
Пример config/modules/module/config.table.php
avatar
не стал этого делать, т.к. функционал полностью покрывается config/modules/module/config.php + далеко не каждый модуль имеет свои таблицы
avatar
Просто мне оказалось странным частичная реализация логики… если есть config.php, config.route.php и т.д. То почему нет tables? Не логично, хотя действительно не критично. Я например объявил константу прямо в модуле))
avatar
Я например объявил константу прямо в модуле))
этого делать не нужно
avatar
Предлагаешь в конфиге? Хотя впрцинипе разницы не вижу кардинальной.
avatar
конфиг для того и сделан, чтоб было удобно и быстро провести конфигурирование/настройку параметров, а не лезть в код модуля для смены названия таблицы.
avatar
Эту таблицу буду менять только я. А тот кто захочет её сменить — без трда найдёт оъявление в инициализации модуля. Но хорошо, в будущем перенесу конечно.
avatar
Эту таблицу буду менять только я
с таким подходом далеко не уехать… таблицу будет менять тот, кто использует модуль, если конечно ты хочешь дать этот модуль кому то использовать
avatar
Вы путаете понятия… обычный пользователь этого ни разу не будет делать, да даже разработчики не будут (смысла нет абсолютно). Но а вообще я уже сказал что перенесу.

зы. Но вы меня не переубедили.
avatar
«Единообразно — значит красиво» (с) армейская поговорка :)
avatar
Изначально не уточнил что константы таблиц БД, пардонь…
avatar
Вроде всё нормально, только движок превращается в нечто монструозное с кешем, который можно будет запускать чуть ли только на VPS как минимум :)

Да, мне тоже интересно, какова генеральная линия развития проекта? Значит ли все вышескзанное, что есть четкая нацеленность на то, что это заведомо будет «тяжелое» приложение, расчитанное на приличный хостинг с мемкешем, сфинксом и проч. серьезными атрибутами?
avatar
Ну вообще, для маленьких социалок подойдет
ning и vsevteme.ru

А если нужно кастомайзное решение на приличное кол-во народа, то livestreet идеален.
Правда, нужно составлять мануалы ко всему.
avatar
Я правильно понял, что то — не движки, а ресурсы а-ля жж и т.п., где каждый по желанию заводит своё комьюнити?
avatar
ага) маломасштабируемое решение… при необходимости расширяться исходники тебе не выдадут.

Хотя и можно свой домен привязать
avatar
надеюсь нет, просто с мемкешем он будет работать шустрее. Сейчас стоит задача сделать из него именно framework, максимально удобный для реализации своих идей на базе LS
avatar
Отлично! Хотел было вопрос по объему таблицы с комментами задать, но передумал — должно хватить. :-)
avatar
хороший вопрос, т.к. именно он изначально повлиял на разделение комментов по разным таблицам(topic,talk). Для проектов с большим объемом контента разделить комменты на несколько таблиц по типам не составит труда, достаточно подменять названия таблиц от типа и учесть это при запросе коммента по ID. Т.е. тем кому нужно могут без больших затрат разделить комменты.
avatar
UPD также меня постоянно посещают мысли избавиться от констант в конфиге и вынести настройки в массив $CONFIG либо БД :)


ДАВНО пора! Но, только не в БД.
Вообще, в идеале, лучше сделать несколько файлов настроек: глобальные настройки и возможность в модуле иметь свой файл настроек.
avatar
Да, больше склоняюсь к $CONFIG с возможностью переопределения. Стандартный конфиг в поставке ЛС config.php + пустой config.local.php, где юзер может изменить/добавить настройки не боясь, что они потеряются при обновлении двига + конфиги модулей(они уже есть)
avatar
мб все же лучше статический класс в качестве хранителя настроек ?) чтоб в каждом методе не пришлось писать global $CONFIG
avatar
Да, не соит конфиги в БД совать. А использование $CONFIG даст больше гибкости.
И еще — может, не стоит всегда подгружать все-все конфиг-скрипты для всех модулей? Общие конфиги грузить всегда, а те, что к конкрентным модулям относятся, можно подгружать при инициализации соотвествующих классов. Когда модулей не много, это не суть важно. Но мы же все считаем, что проект будет развиваться и расти и модулей со временм будет дофига :)
avatar
вообще, уже частично так и есть:)
avatar
М-м-м, а «частично» — это как? Я что-то пропустил?
avatar
Модули подгружаются «на лету»… Осталось привязать к ним конфиги:)
avatar
А, ну дык, и я про то — модули подгружаются, когда надо, а конфиги — всегда чохом. Надо бы устаканть это дело.
avatar
вот тута:
trac.assembla.com/livestreet/browser/trunk/classes/engine/Engine.class.php#L169

осталось там же конфиги подгружать по названию модуля, а в LoadConfig() грузить тока autoLoad

Макс, ты тут? чего думаешь?:)
avatar
согласен
avatar
Блин, а я вот честно говоря с удовольствием использовал фичу того, что конфиги грузятся все. В результате у меня появился конфиг, который не относится ни к одному из модулей, но он мне очень нужен. Как мне быть?
avatar
avatar
Ааа… то есть он как раз постоянно будет грузиться, верно? Отлично!

Я как-то первый раз прочитав про config.local.php не отразил, что от не зависит от модулей. :-)
avatar
Макс, а можешь внести некоторые изменения касательно класса oTopic?

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

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

$oTopic->setExternalObject($oPhotoCollage);
$oTopic->setExternalDataType("photocollage");


2. Потом в списке топиков (в шаблоне) мы можем забирать каждый объект:

{if $oTopic->getExternalDataType()}
  {assign var=sIncludeType value=$oTopic->getExternalDataType()}
  {assign var=sIncludeFile value="topic."|cat $sIncludeType|cat ".tpl"}
  {include file=$sIncludeFile}
{/if}
Соответственно, подружаем модульный шаблон. На странице топика можно вставить такой же код.

Вот собственно и всё..
avatar
будет поддержка кастомных сущностей наследуемых от дефолтных, там можно будет вносить любые изменения
только получение объекта сущности будет происходит через обертку в $oEngine, например $oEngine::GetEntity('TopicBlabla',aParam);
avatar
Ну так в любом случае нужно как-то определять какая сущность(вид топика) выводится в списке топиков:)
avatar
я это к тому, что методы
$oTopic->setExternalObject($oPhotoCollage);
$oTopic->setExternalDataType("photocollage"); 
можно будет без проблем добавить в кастомном классе. Или я не уловил твоей мысли? )
avatar
Я к тому что на уровне смарти тебе всё равно придется узнавать к какому классу у тебя принадлежит oTopic в foreach:)

По этому нужен будет метод, который возвращает название класса объекта. Кстати, уже есть TopicType, я про него забыл. Тока его надо расширить в бд, чего-нибудь сделав с ENUM…

А данные в новые классы грузить надо через хуки… я не совсем понимаю, как у тебя это будет происходит.

1. Вот в topic.class.php у нас появился массив id.
2. Мы подтянули к нему данные для объекта топик в цикле
3. Мы берем эту коллекцию топиков в экшене или блоке и выводим в смарти.

На каком этапе у нас будет создаваться дочерний класс через хук? На этапе 2, в цикле?
avatar
да, т.е. в тех же местах, где создаются объекты сущностей сейчас
avatar
Раз пошла такая пьянка и вопрос все же относится к архитектуре, Макс, может заменим is_administartor, is_moderator на одно поле Role? :-)
avatar
может
avatar
А, во, Макс!

Не мог бы ты config.php приспособить для запуска через шел?
Во-первых там нет массива $_SERVER
Так что можно вставить

if (isset($_SERVER['HTTP_HOST']))
	define('DIR_WEB_ROOT','http://'.$_SERVER['HTTP_HOST']); // полный WEB адрес сайта
else
	define('DIR_WEB_ROOT',""); // тут он нам не нужен

Либо убрать вообще DIR_WEB_ROOT при запуске из консоли
А дальше вместо
$_SERVER['DOCUMENT_ROOT']

Ставить

if (isset($sDirRoot))
	define('DIR_SERVER_ROOT',$sDirRoot); // полный путь до сайта в файловой системе
else
	define('DIR_SERVER_ROOT',$_SERVER['DOCUMENT_ROOT']."/ls-video"); // полный путь до сайта в файловой системе

с учетом того, что $sDirRoot мы устанавливаем в запускаемом файле…

Либо нужно делить конфигурацию путей и другие константы на разные конфиги…
avatar
дак
$_SERVER['HTTP_HOST'] и $_SERVER['DOCUMENT_ROOT']
используются исключительно для «быстрой» настройки, всем рекомендую там прописывать пути руками :)
avatar
Кстати, в Router.class.php тоже есть

$sReq=preg_replace("/\/+/",'/',$_SERVER['REQUEST_URI']);


Надо его не грузить чтоли…
комментарий был удален
комментарий был удален
комментарий был удален
комментарий был удален
avatar
Ну мне показалось, что структура LS кардинально переделывается, поэтому предположил, что могут быть затруднения при апдейте :)
  • dig
  • 0
avatar
Модификации сразу сольются — факт. А вот базу можно конвертнуть ВСЕГДА.
avatar
Это не может не радовать! =))
комментарий был удален
комментарий был удален
avatar
Я конечно извиняюсь, что врываюсь в Ваш разговор с простым вопросом :) Но всё же — когда планируется выпуск новой версии?
avatar
А рак на горе уже свистнул? Если серьезно, то смотрим и радуемся.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.