Pull to refresh

Добавление своего функционала в UMI.CMS при помощи обработчиков событий

Reading time 12 min
Views 16K
В системе управления сайтами UMI.CMS изначально заложено разделение на основной движок сайта, который не трогается вэб-разработчиком (и который перезаписывается при обновлении системы), и дополнительный (кастомный) функционал, который уже разработчик сайта адаптирует под себя: собственные шаблоны дизайна, макросы (PHP-функции, вызываемые из шаблонов), собственные модули, если необходимо.

Однако, при разработке своего сайта бывают ситуации, когда надо изменить уже существующий функционал сайта:
  • добавить собственную логику импорта данных из XML;
  • выполнить какие-то действия при импорте данных;
  • выполнить какие-то действия при создании или изменении заказа;
  • выполнить какие-то действия по расписанию;
  • … и так далее.

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

Согласно документации в UMI.CMS есть два вида обработчиков событий:
  • системные — это предустановленные обработчики, которые прописываются при разработке модуля. Эти обработчики прописываются в файле events.php, который лежит в директории модуля. Для модулей, входящих в поставку UMI.CMS, этот файл изменять нельзя.
  • пользовательские — эти обработчики должны находиться в файле custom_events.php в директории модуля.

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

События могут вызываться до изменения объекта (т.н. режим before) и после изменения объекта (режим after). Не все события могут быть и «до», и «после», какие именно, надо смотреть по ссылке, приведенной ниже.

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

Пусть например мы разрабатываем интернет-магазин на базе UMI, и нам необходимо выполнить какие-то действия при изменении статуса заказа (например, отослать дополнительные емайлы, выставить какие-то другие поля в заказе и т.п. в зависимости от статуса). Смотрим, где мы можем «отловить» события изменения статуса заказа:
  • изменение статуса при создании заказа пользователем сайта — событие order-status-changed модуля Emarket;
  • изменение статуса из админки на странице подробной информации о заказе — системное событие systemModifyObject;
  • изменение статуса из админки на странице списка всех заказов — системное событие systemModifyPropertyValue;
  • изменение статуса при импорте его через XML (например, при выгрузке из 1С) — событие exchangeOnUpdateObject.

Таким образом, чтобы учесть все места изменения статуса заказа, придется писать 4 обработчика.

Остановимся подробнее на системном событии systemModifyPropertyValue. Упоминаний о нем я в документации не нашел, увидел его вызов только при анализе кода системы, а ведь без него обойтись в описываемой ситации достаточно сложно, поскольку администратор сайта (менеджер) может изменить данные заказа двумя способами:
  • на главной странице модуля «Интернет-магазин» в списке всех заказов — UMI позволяет редактировать статус и другие данные заказа прямо непосредственно в этом списке;
  • в окне подробной информации о заказе, которая открывается по клику на заказ.

И если пользоваться только обработчиком события systemModifyObject, то придется объяснять всем будущим администраторам сайта, что им ни в коем случае нельзя менять данные заказа из списка заказов, а только надо заходить в каждый заказ, и менять что-то там. Что, конечно же, очень неудобно и оставляет большую возможность ошибиться.

Системное событие systemModifyPropertyValue имеет следующие параметры:
  • entity — ссылка на объект, у которого меняется свойство;
  • property — имя изменяемого свойства;
  • oldValue — старое значение свойства;
  • newValue — новое значение свойства;

Обработчик этого события можно использовать не только при редактировании списка заказов в модуле «Интернет магазин», но и в других аналогичных списках объектов в админке UMI.CMS.

Как назначается обработчик событий, можно посмотреть, например здесь или здесь. Я же хочу показать более сложный пример: как при помощи создания обработчика события импорта данных добавить недостающий функционал в UMI — научить систему импортировать опционные свойства товара.

Пусть, например, мы разрабатываем интернет-магазин по продаже футболок. У нас есть установленная 1С «Управление торговлей», в которой мы собираемся вести учет товаров и заказов. В 1С заведена вся необходимая номенклатура товаров и желательно, чтобы она с минимальными действиями выгружалась на сайт.

Специфика продажи подобных товаров такова, что у нас есть, например, модель «Футболка Дольче», у которой есть конкретные продаваемые единицы:
  • Футболка Дольче Белая, 40 размер;
  • Футболка Дольче Белая, 48 размер;
  • Футболка Дольче Красная, 44 размер;
  • … и так далее.

То есть множество позиций данной модели, отличающиеся цветом и размером (а возможно и ценой, для определенных комбинаций). В терминах 1С это «характеристики номенклатуры». А с точки зрения интернет-магазина мы хотим, чтобы у нас была одна страница модели «Футболка Дольче», на которой покупатель мог бы выбрать себе цвет и размер и создать с ними заказ.

С точки зрения 1С кажется все просто. Мы ставим в настройках номенклатуры галочку «характеристики». Делаем выгрузку товаров сначала на диск, смотрим получившийся XML-файл (offers.xml), предложения нужные эти есть, радуемся и делаем выгрузку на сайт. И тут понимаем, что радовались рано. Сами товары добавились, а вот их характеристики (что у конкретной футболки есть десяток предложений с разными комбинациями цветов и размеров) — нет.

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

Особенности импорта из 1С в UMI описаны в документации. Чтобы добавить свой функционал при импорте данных надо модифицировать шаблон импорта /xsl/import/custom/commerceML2.xsl, а также добавить свой обработчик события импорта.

Модифицируем шаблон импорта:
Шаблон импорта
	<xsl:template match="Классификатор">
		...
		<!-- описание типа для справочника вариантов характеристик футболок -->
		<type id="charaсteristics-kinds" title='Справочник для поля "Характеристики"' parent-id="root-guides-type" guide="guide">
			<base/>
			<fieldgroups>
				<group name="charaсteristics_kinds">
					<field name="1c_id" title="Идентификатор в 1С" visible="visible">
						<type name="Строка" data-type="string"/>
					</field>
					<field name="color" title="Цвет" field-type-id="3" visible="visible" required="required" >
						<type id="3" name="Строка" data-type="string"/>
					</field>
					<field name="size" title="Размер" field-type-id="3" visible="visible" required="required" >
						<type id="3" name="Строка" data-type="string"/>
					</field>
				</group>
			</fieldgroups>
		</type>
		<!-- тип для товара -->
		<type id="shirts" title='1C: Футболки' parent-id="root-catalog-object-type">
			<base module="catalog" method="object">Объект каталога</base>
			<fieldgroups>
				...
				<!-- Опционные свойства -->
				<group name="optioned_properties" title="Опционные свойства">
					<field name="charaсteristics" title="Характеристики" visible="visible" guide-id="charakteristics-kinds">
						<type name="Составное" data-type="optioned"  multiple="multiple" />
					</field>
				</group>
			</fieldgroups>
		</type>
		...
	</xsl:template>

	<!-- шаблон для описания товара - указываем, что страницы товара будут создаваться с нашим типом -->
	<xsl:template match="Товары/Товар">
		...
		<page id="{Ид}" parentId="{$group_id}" type-id="shirts">
			...
		</page>
		...
	</xsl:template>

	<!-- Предложения -->
	<xsl:template match="ПакетПредложений">
		<meta>
			<source-name>commerceML2</source-name>
		</meta>
		<objects>
			<xsl:apply-templates select="Предложения/Предложение"  mode="objects"/>
		</objects>
		<pages>
			<xsl:apply-templates select="Предложения/Предложение"  mode="items"/>
		</pages>
	</xsl:template>

	<!-- описание объектов справочников -->
	<xsl:template match="Предложения/Предложение"  mode="objects">
		<object id="{substring-after(Ид,'#')}" name="{Наименование}" type-id="charakteristics-kinds">
			<properties>
                <group name="charaсteristics_kinds">
					<property name="1c_id" type="string"  is-public="1" visible="visible">
						<title>Идентификатор в 1С</title>
						<value><xsl:value-of select="Ид" /></value>
					</property>
                	<xsl:apply-templates select="ХарактеристикиТовара/ХарактеристикаТовара[Наименование = 'Цвет']" mode="color" />
                	<xsl:apply-templates select="ХарактеристикиТовара/ХарактеристикаТовара[Наименование = 'Размер']" mode="size" />
				</group>
			</properties>
		</object>
	</xsl:template>

	<xsl:template match="ХарактеристикиТовара/ХарактеристикаТовара" mode="color" >
		<property name="color" type="string"  is-public="1" visible="visible">
			<title><xsl:value-of select="Наименование" /></title>
			<value><xsl:value-of select="Значение" /></value>
		</property>
	</xsl:template>

	<xsl:template match="ХарактеристикиТовара/ХарактеристикаТовара" mode="size" >
		<property name="size" type="string"  is-public="1" visible="visible">
			<title><xsl:value-of select="Наименование" /></title>
			<value><xsl:value-of select="Значение" /></value>
		</property>
	</xsl:template>

	<!-- описание страниц, ссылающихся на эти объекты -->
	<xsl:template match="Предложения/Предложение" mode="items">
		<page id="{substring-before(Ид,'#')}" update-only="1">
			<properties>
				<group name="optioned_properties" title="Опционные свойства">
					<property name="charaсteristics" type="optioned" is-public="1" visible="visible">
						<title>Характеристики</title>
							<value>
								<option int="{Количество}" float="{Цены/Цена/ЦенаЗаЕдиницу}">
									<object id="{substring-after(Ид,'#')}" name="{Наименование}" type-id="charakteristics-kinds" />
								</option>
							</value>
					</property>
				</group>
			</properties>
		</page>
	</xsl:template>


При помощи данного шаблона создается отдельный справочник (Справочник для поля «Характеристики»), в который записываются все пришедшие варианты характеристик из 1С. Кроме того, модифицируется тип для объекта каталога (товара), чтобы добавить туда опционные свойства. Таким образом, сами характеристики мы уже загрузили на сайт, осталось только написать код, который бы сопоставил товару характеристики.

Добавляем свой обработчик событий. Для этого создаем в папке /classes/modules/exchange файл custom_events.php c кодом
<?php
new umiEventListener("exchangeOnUpdateElement", "exchange", "onImportElement");
new umiEventListener("exchangeOnAddElement", "exchange", "onImportElement");
?>

Таким образом, мы задали, что как при создании, так и при обновлении элемента (страницы товара) при импорте будет вызван метод onImportElement. Напишем код этого метода в файле __custom.php:
Код обработчика события
	/**
	 * Обработчик события импорта страницы товара из 1С
	 * @param e - ссылка на экземпляр события
	 */
	public function onImportElement($e) {
		if($e->getMode() == "after") {
			//добавляем опционные свойства
			$this->addOptionedProperties($e);
		}
	}

	/**
	 * Добавляет в случае необходимости опционные свойства. Этот функционал отсутствует в оригинальном
	 * импортере UMI
	 * @param e - ссылка на экземпляр события
	 */
	function addOptionedProperties($e) {
		$hierarchy = umiHierarchy::getInstance();
		$element = $e->getRef('element');
		if (!$element instanceof umiHierarchyElement
				|| $element->getMethod() != 'object') {
			//это не страница товара
			return false;
		}
		$object_id = $element->objectId;
		//XML DOM node с данными данного товара
		$element_info = $e->getParam('element_info');
		$properties = $element_info->getElementsByTagName('property');
		$propertiesSize = $properties->length;
		$types = umiObjectTypesCollection::getInstance();
		//обрабатываем все свойства товара из XML
		foreach($properties as $key => $info) {
			$old_name = $info->getAttribute('name');
			//получаем внутреннее имя свойства
			$name = self::translateName($old_name);
			$nl =  $info->getElementsByTagName("value");
			if (!$nl->length) {
				//не найдено значение свойства в XML
				continue;
			}
			$value_node = $nl->item(0);
			//получаем ссылку на соответствующий тип данных и соответствующее поле товара
			$type_id = ($element instanceof umiHierarchyElement) ? $element->getObjectTypeId() : $element->getTypeId();
			$type = umiObjectTypesCollection::getInstance()->getType($type_id);
			$field_id = $type->getFieldId($name, false);
			$field = umiFieldsCollection::getInstance()->getField($field_id);
			if (!$field instanceof umiField) {
				continue;
			}
			switch($field->getDataType()) {
				//нам надо обработать только опционные свойства, так как остальные уже обработаны движком UMI
				case "optioned":
					//storing old settings
					$oldForce = umiObjectProperty::$USE_FORCE_OBJECTS_CREATION;
					umiObjectProperty::$USE_FORCE_OBJECTS_CREATION = false;
					//находим справочник, на который ссылается поле
					$objectsCollection = umiObjectsCollection::getInstance();
					$guideItems = $objectsCollection->getGuidedItems($field->getGuideId());
					$options = $value_node->getElementsByTagName("option");
					$items = Array();
					foreach($options as $option) {
						//в поле int у нас хранится число товаров на складе соответствующего цвета и размера
						$int = $option->hasAttribute("int") ? $option->getAttribute("int") : null;
						//в поле float у нас хранится цена товара
						$float = $option->hasAttribute("float") ? $option->getAttribute("float") : null;
						$objects = $option->getElementsByTagName("object");
						foreach($objects as $object) {
							$objectId = $object->hasAttribute("id") ? $object->getAttribute("id") : null;
							$objectName = $object->hasAttribute("name") ? $object->getAttribute("name") : null;
							$objectTypeId = $object->hasAttribute("type-id") ? $object->getAttribute("type-id") : null;
							//создаем опционное свойства
							$item = Array();
							$item["int"] = (int)$int;
							$item["float"] = (float)$float;
							$item["varchar"] = $objectName;
							//находим объект, на который ссылается property
							foreach($guideItems as $key => $value) {
								if($value == $objectName) {
									//ссылка на id объекта справочника должна быть именно типа int, иначе не работает
									$item["rel"] = (int)$key;
									break;
								}
							}
							$items[] = $item;
						}
					}
					//обновим поле объекта
					$entityId = $element->getId();
					if($element instanceof umiHierarchyElement) {
						$entityId = $element->getObject()->getId();
					}
					$pageObject = $objectsCollection->getObject($entityId);
					//мы должны добавить объекты к уже существующим, если они есть
					$existingItems = $pageObject->getValue($name);
					$newItems = Array();
					if($existingItems) {
						//добавляем те объекты из имеющихся, которых нет в новых
						foreach($existingItems as $existingItem) {
							$found = false;
							foreach($items as $item) {
								if($item["rel"] == $existingItem["rel"]) {
									$found = true;
									break;
								}
							}
							if(!$found) {
								$newItems[] = $existingItem;
							}
						}
					}
					//теперь добавим все новые
					foreach($items as $item) {
						$newItems[] = $item;
					}
					$pageObject->setValue($name, $newItems);
					$pageObject->commit();
					//restoring settings
					umiObjectProperty::$USE_FORCE_OBJECTS_CREATION = $oldForce;
					break;
			}
		}
	}

	/**
	 * Оригинальный UMI-шный метод из кода модуля импорта данных
	 */
	protected static function translateName($name) {
		$name = umiHierarchy::convertAltName($name, "_");
		$name = umiObjectProperty::filterInputString($name);
		if(!strlen($name)) $name = '_';
		$name = substr($name, 0, 64);
		return $name;
	}


В приведенном коде после создания или добавления товара (проверка на after) мы читаем из XML свойства товара и при наличии опционных свойств добавляем в товар соответствующие опционные свойства из справочника «Справочник для поля Характеристики».

После добавления указанного кода и повторной выгрузки товаров на сайт видим, что у каждого товара появились позиции, каждая со своим цветом и размером. То есть указанная задача достингута, и теперь у нас система UMI.CMS умеет импортировать из 1С опционные свойства товара.

Таким образом, события в UMI — это весьма мощный инструмент, и с помощью грамотно добавленных своих обработчиков событий можно существенно расширить функционал сайта, при этом не меняя ни единой строчки системного кода CMS и сохраняя возможность системных обновлений.
Tags:
Hubs:
+2
Comments 8
Comments Comments 8

Articles