spec/lib/settings.cfm
2025-06-02 16:16:51 +03:00

217 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--- *** Refactoring required --->
<!--- v0.3
2017-08-30
Настройки вынесены в таблицу usr_settings в связи с миграцией репозитория пользователей в мастер справочники
Заодно cfqueryparam
модуль должен инкапсулировать работу с настройками
- хранение в сессии и в БД, синхронность с базой
- чтение и модификацию
- очистку(сброс)
При этом модуль ничего не должен знать о структуре самих настроек (фильтрах, сортировках и т.д.)
Жизненный цикл контейнера settings
1. Живет в сессии
2. Если в сессии нет settings - вычитывается из БД
3. Если и в БД нет - инициализируется и сразу сохраняется в БД (пустой)
4. При модификации синхронизируется с базой (пишет в нее) - нужно либо снаружи вызывать сохранение, либо инкапсулировать запись
(инкапсулировать запись - сильно усложнит код)
5. Для работы выдает КОПИЮ данных, соответственно, при сохранении должен ее синхронизировать???
Пока решили, что будем выдавать ссылку на объект в сессии, при этом нужно блок кода синхронизировать CFLOCK
При этом 2 проблемы:
- некрасивый код (лок на сессию руками, связь совершенно неявная)
- избыточная блокировка
- избыточное сохранение (можно было бы проверять на изменение)
чтобы действовать без блокировки, можно экспортировать объект в безопасный scope - переменные или реквест
Альтернативный синтаксис вызова
<m:settings key="page_name" output="settings"><!--- implicit get --->
<m:sortparam sort=#settings.sort# fieldcount=9 sortlength=2/><!--- any modification --->
</m:settings><!--- implicit sync --->
Разрешение конфликта версий при обновлении софта
Конфликт версий: код требует параметр, отсутствующий в сохраненной версии. Например, одна версия фильтра сохранила свои настройки,
а другая, более новая, пытается прочесть несуществующий ключ.
Другой вариант - несуществующее поле (убранное в очередной версии) - этот конфликт проявится только при запросе к БД.
Варианты решения
- создание параметра с дефолтными значениями при запросе (во все запросы добавить значения по умолчанию). Недостаток - возможно создание помойки
- контроль номера версии. Выглядит сложным в сопровождении
*** Возможно, это была плохая идея - разрешить доступ по длинному ключу. Когда доставали и записывали целиком объект, было меньше возможностей для ошибок
--->
<!---*** обработка вызова get-sync c закрывающим тегом --->
<cfparam name="ATTRIBUTES.action" type="string" default=""/>
<cfparam name="obj" type="struct" default=#structNew()#/>
<cfparam name="doSyncOnClose" type="boolean" default="No"/>
<cfif thisTag.executionMode IS "start">
<cfif ATTRIBUTES.action IS "">
<cfset ATTRIBUTES.action="get"/>
<cfset doSyncOnClose="Yes"/>
</cfif>
<cfelse>
<cfif doSyncOnClose>
<cfset ATTRIBUTES.action="sync"/>
<cfelse>
<cfexit method="exittag"/>
</cfif>
</cfif>
<cfset useDB="false"/><!--- --->
<cfswitch expression=#lCase(ATTRIBUTES.action)#>
<cfcase value="create"> <!--- конструктор --->
<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">
<cfset session.settings = retrieveSettings()>
</cflock>
</cfcase>
<cfcase value="sync">
<!--- Синхронизировать измененный объект (с сессией и персистенсом) --->
<cfparam name="ATTRIBUTES.key" type="string">
<cfif doSyncOnClose>
<cfif thisTag.executionMode IS "end">
<cfset obj=StructFind(CALLER, "#ATTRIBUTES.output#")/>
</cfif>
<cfelse>
<cfparam name="ATTRIBUTES.obj" type="struct">
<cfset obj=#ATTRIBUTES.obj#/>
</cfif>
<!---<cfdump var=#obj#/>--->
<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">
<!--- помещаем объект на свое место в структуре --->
<cfset container=#session.settings#>
<cfset cnt = listLen("#ATTRIBUTES.key#", ".")>
<cfloop index="i" from=1 to=#cnt#>
<cfset item = listGetAt(#ATTRIBUTES.key#, i, ".")>
<cfif i EQ cnt><!--- last element --->
<cfset structInsert(#container#, #item#, #obj#, "YES")>
<cfelse>
<!--- попытка перебдеть--->
<cfif NOT structKeyExists(#container#,#item#)>
<cfset structInsert(#container#,#item#,#structNew()#)>
</cfif>
<cfset container=structFind(container,item)>
</cfif>
</cfloop>
</cflock>
<!--- сохраняем настройки в персистенс --->
<cfset saveSettings(container)>
</cfcase>
<cfcase value="reset"><!--- *** надо было назвать reset_all или как-то так --->
<!--- Сбросить все пользовательские настройки --->
<cfset session.settings=structNew()>
<cfset saveSettings(session.settings)>
</cfcase>
<cfcase value="clear">
<!--- Очистить секцию пользовательских настроек (sort или filter)--->
<cfparam name="ATTRIBUTES.key" type="string">
<cfset targetFullName = "session.settings.#ATTRIBUTES.key#">
<cfset objName = listGetAt(ATTRIBUTES.key,1,".")>
<cfif isDefined(targetFullName)>
<cflock scope="SESSION" type="EXCLUSIVE" timeout="3">
<cfset evaluate("structClear(#targetFullName#)")>
<cfset obj = evaluate(#targetFullName#)>
<cfmodule template="settings.cfm" action="sync" key=#ATTRIBUTES.key# obj=#obj#><!--- синхронизация при очистке используется ---><!--- хорошо ли это? А вообще надо?--->
</cflock>
</cfif>
</cfcase>
<cfcase value="get">
<!---
Получить объект или секцию
Обязательный параметр key - имя ключа в структуре, начинающееся с имени объекта, через точку
В параметре output имя переменной, принимающей результат
--->
<cfparam name="ATTRIBUTES.key" type="string">
<cfparam name="ATTRIBUTES.output" type="string">
<cfif NOT isDefined("session.settings")><!--- *** container--->
<cfmodule template="settings.cfm" action="create"/>
</cfif>
<cfset targetFullName = "session.settings.#ATTRIBUTES.key#">
<cfset objName = listGetAt(ATTRIBUTES.key, 1, ".")>
<cfif (NOT structKeyExists(#session.settings#, objName))
OR structIsEmpty(structFind(#session.settings#, objName))>
<cfmodule template="init_settings.cfm" container=#session.settings# objName=#objName#/>
<!--- несколько неуклюже, лучше просто возвращать объект и потом класть в сессию --->
</cfif>
<cftry>
<cfset obj=evaluate(targetFullName)>
<cfcatch type="ANY">
<cfmodule template="init_settings.cfm" container=#session.settings# objName=#objName#/><!---*** грубо --->
<cfset obj=evaluate(targetFullName)>
</cfcatch>
</cftry>
<cfset setVariable("CALLER.#ATTRIBUTES.output#", #duplicate(obj)#)>
</cfcase>
<cfdefaultcase>
<cfthrow message="Unsupported action = #ATTRIBUTES.action# in settings.cfm">
</cfdefaultcase>
</cfswitch>
<!---***debug <cfoutput>#ATTRIBUTES.action# #thisTag.executionMode# #thisTag.hasendtag#</cfoutput>--->
<!--- --------------------------------------------------------------------------------- --->
<!--- private methods --->
<cffunction name="retrieveSettings" access="private">
<cfif useDB><!--- можно перегрузить методы для разных провайдеров персистенса, но пока смысла в этом не видно, код достаточно примитивен и управляем --->
<cfquery name="qGet" datasource="#request.DS#">
select settings from usr where usr_id='#request.usr_id#'
</cfquery>
<cfif len(qGet.settings) GT 0>
<cftry>
<CFWDDX ACTION='wddx2cfml' input=#qGet.settings# output='settings'/>
<cfreturn #settings#/>
<cfcatch type="Any">Err:
<cfoutput>#cfcatch.message#</cfoutput>
<cfabort/>
</cfcatch>
</cftry>
</cfif>
</cfif>
<cfreturn #structNew()#/>
</cffunction>
<cffunction name="saveSettings" access="private">
<cfargument name=settings>
<cfif !isNumeric(request.usr_id)
OR !(request.usr_id GT 0)
OR request.usr_id EQ request.ANONYMOUS_USR_ID
OR request.usr_id EQ request.UNDEFINED_USR_ID>
<cfreturn />
</cfif>
<cfif useDB>
<cfquery name="qCheckExistence" datasource="#request.DS#">
select count(*) as cnt from usr_serrings where usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>
</cfquery>
<CFWDDX ACTION='cfml2wddx' input=#session.settings# output='strSettings'/>
<cfif qCheckExistence.cnt EQ 0>
<cfquery name="qSave" datasource="#request.DS#">
insert int usr_settings (usr_id, settings)
values(<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>, <cfqueryparam cfsqltype="cf_sql_varchar" value='#strSettings#'/>)
</cfquery>
<cfelse>
<cfquery name="qSave" datasource="#request.DS#">
update usr_settings
set settings=<cfqueryparam cfsqltype="cf_sql_varchar" value='#strSettings#'/>
where usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value='#request.usr_id#'/>
</cfquery>
</cfif>
<cfelse>
<!--- do nothing --->
</cfif>
</cffunction>