Files
ipwhitelist-app/research/payg-sso-full/lib/settings.cfm
T

214 lines
10 KiB
Plaintext

<!--- *** 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 NOT request.usrAuthenticated()>
<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>