BxDolRecommendation.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. */
  6. class BxDolRecommendation extends BxDolFactory implements iBxDolFactoryObject
  7. {
  8. protected $_bIsApi;
  9. protected $_oDb;
  10. protected $_iObject;
  11. protected $_sObject;
  12. protected $_aObject;
  13. protected $_aCriteria;
  14. protected $_iProfileId;
  15. protected $_iReducerAdd;
  16. protected $_iReducerIgnore;
  17. protected $_iPerPageDefault;
  18. protected function __construct($aObject)
  19. {
  20. parent::__construct();
  21. $this->_bIsApi = bx_is_api();
  22. $this->_aObject = $aObject['object'];
  23. $this->_iObject = (int)$this->_aObject['id'];
  24. $this->_sObject = $this->_aObject['name'];
  25. $this->_aCriteria = $aObject['criteria'];
  26. $this->_oDb = new BxDolRecommendationQuery();
  27. $this->_oDb->init($this->_aObject);
  28. $this->_iProfileId = bx_get_logged_profile_id();
  29. $this->_iReducerAdd = 5;
  30. $this->_iReducerIgnore = 10;
  31. $this->_iPerPageDefault = 24;
  32. }
  33. public static function getObjectInstance($sObject)
  34. {
  35. if(isset($GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject]))
  36. return $GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject];
  37. $aObject = BxDolRecommendationQuery::getObject($sObject);
  38. if(!$aObject || !is_array($aObject) || empty($aObject['object']))
  39. return false;
  40. $sClass = 'BxTemplRecommendation';
  41. if(!empty($aObject['object']['class_name'])) {
  42. $sClass = $aObject['object']['class_name'];
  43. if(!empty($aObject['object']['class_file']))
  44. require_once(BX_DIRECTORY_PATH_ROOT . $aObject['object']['class_file']);
  45. }
  46. $o = new $sClass($aObject);
  47. return ($GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject] = $o);
  48. }
  49. public static function updateData($iProfileId = 0)
  50. {
  51. if(!$iProfileId) {
  52. if(isLogged())
  53. $iProfileId = bx_get_logged_profile_id();
  54. else
  55. return false;
  56. }
  57. $oCronQuery = BxDolCronQuery::getInstance();
  58. $sName = 'recommendations_for_' . $iProfileId;
  59. if(!$oCronQuery->isTransientJobService($sName))
  60. $oCronQuery->addTransientJobService($sName, ['system', 'update_data', [$iProfileId], 'TemplServiceRecommendations']);
  61. return true;
  62. }
  63. public function getConnection()
  64. {
  65. return $this->_aObject['connection'];
  66. }
  67. public function getContentInfo()
  68. {
  69. return $this->_aObject['content_info'];
  70. }
  71. public function getList($iProfileId = 0, $aParams = [])
  72. {
  73. if(!$iProfileId)
  74. $iProfileId = $this->_iProfileId;
  75. return $this->_oDb->get($iProfileId, $this->_aObject['id'], $aParams);
  76. }
  77. public function getCount($iProfileId = 0, $aParams = [])
  78. {
  79. if(!$iProfileId)
  80. $iProfileId = $this->_iProfileId;
  81. return $this->_oDb->get($iProfileId, $this->_aObject['id'], array_merge($aParams, ['count_only' => true]));
  82. }
  83. public function actionAdd($iProfileId = 0, $iItemId = 0)
  84. {
  85. if(!$iProfileId)
  86. $iProfileId = $this->_iProfileId;
  87. if(!$iItemId)
  88. $iItemId = bx_process_input(bx_get('id'), BX_DATA_INT);
  89. if(!$this->add($iProfileId, $iItemId))
  90. return ['code' => 1, 'msg' => '_sys_txt_error_occured'];
  91. return ['code' => 0];
  92. }
  93. public function actionIgnore($iProfileId = 0, $iItemId = 0)
  94. {
  95. if(!$iProfileId)
  96. $iProfileId = $this->_iProfileId;
  97. if(!$iItemId)
  98. $iItemId = bx_process_input(bx_get('id'), BX_DATA_INT);
  99. if(!$this->ignore($iProfileId, $iItemId))
  100. return ['code' => 1, 'msg' => '_sys_txt_error_occured'];
  101. return ['code' => 0];
  102. }
  103. public function add($iProfileId, $iItemId)
  104. {
  105. $aItem = $this->_oDb->getItem($iProfileId, $this->_iObject, $iItemId);
  106. /**
  107. * Action 'add' moves the item to the end not so much as 'ignore'.
  108. * For this purpose (+) is used instead of the (*). Is needed to hide
  109. * the recomended item. Isn't removed completely because friend request
  110. * can be rejected.
  111. */
  112. $iReducer = (int)$aItem['item_reducer'] + $this->_iReducerAdd;
  113. return $this->_oDb->update($iProfileId, $this->_iObject, $iItemId, ['item_reducer' => $iReducer]);
  114. }
  115. public function ignore($iProfileId, $iItemId)
  116. {
  117. $aItem = $this->_oDb->getItem($iProfileId, $this->_iObject, $iItemId);
  118. /**
  119. * Action 'ignore' moves the item to the end.
  120. * Repeated action do it much more, therefore (*) is used.
  121. */
  122. $iReducer = !empty($aItem['item_reducer']) ? (int)$aItem['item_reducer'] : 1;
  123. $iReducer *= $this->_iReducerIgnore;
  124. return $this->_oDb->update($iProfileId, $this->_iObject, $iItemId, ['item_reducer' => $iReducer]);
  125. }
  126. public function processCriteria($iProfileId, $iLimit = 100)
  127. {
  128. if(!$iProfileId)
  129. return 0;
  130. $aWeights = array_map(function($fValue) use ($iLimit) { return $fValue * $iLimit; } , $this->_aObject['weights']);
  131. $aCriterionItems = [];
  132. foreach($this->_aCriteria as $sCriterion => $aCriterion) {
  133. $aCriterionParams = $this->_getCriterionParams($iProfileId, $aCriterion['params']);
  134. $iCriterionWeight = (float)$aCriterion['weight'];
  135. switch($aCriterion['source_type']) {
  136. case 'sql':
  137. $aCriterionItems[$sCriterion] = [];
  138. if(empty($aCriterion['source']))
  139. break;
  140. $sQuery = bx_replace_markers($aCriterion['source'], $aCriterionParams);
  141. if($this->_aObject['countable']) {
  142. if(strpos($sQuery, '{order_by}') !== false)
  143. $sQuery = bx_replace_markers($sQuery, ['order_by' => '`value` DESC']);
  144. else
  145. $sQuery .= ' ORDER BY `value` DESC';
  146. }
  147. $sQuery .= ' LIMIT ' . $iLimit;
  148. $aCriterionItems[$sCriterion] = $this->_oDb->getPairs($sQuery, 'id', 'value');
  149. break;
  150. case 'service':
  151. $aCriterionItems[$sCriterion] = BxDolService::callSerialized($aCriterion['source'], $aCriterionParams);
  152. break;
  153. }
  154. }
  155. $aResults = [];
  156. foreach($aCriterionItems as $sCriteria => $aItems) {
  157. $iCriteriaResults = 0;
  158. foreach($aItems as $iId => $iValue) {
  159. if(!isset($aResults[$iId]))
  160. $aResults[$iId] = 0;
  161. $aResults[$iId] += $iValue;
  162. if(++$iCriteriaResults >= $aWeights[$sCriteria])
  163. break;
  164. }
  165. }
  166. $this->_oDb->clean($iProfileId, $this->_iObject);
  167. $oProfile = BxDolProfile::getInstance($iProfileId);
  168. $oConnection = BxDolConnection::getObjectInstance($this->getConnection());
  169. $bConnectionMutual = $oConnection->getType() == BX_CONNECTIONS_TYPE_MUTUAL;
  170. $aItems = $this->_oDb->getBy(['type' => 'profile_object_ids', 'profile_id' => $iProfileId, 'object_id' => $this->_iObject]);
  171. foreach($aItems as $aItem)
  172. if(!$oProfile->isActive($aItem['item_id']) || $oConnection->isConnected($iProfileId, $aItem['item_id'], $bConnectionMutual))
  173. $this->_oDb->delete($iProfileId, $this->_iObject, $aItem['item_id']);
  174. $aItemItt = $this->getItemsTypes(array_keys($aResults));
  175. $iResult = 0;
  176. foreach($aResults as $iId => $iValue) {
  177. if(!$oProfile->isActive($iId))
  178. continue;
  179. if($this->_oDb->add($iProfileId, $this->_iObject, $iId, (isset($aItemItt[$iId]) ? $aItemItt[$iId] : ''), $iValue))
  180. $iResult++;
  181. }
  182. return $iResult;
  183. }
  184. public function outputActionResult ($mixed, $sFormat = 'json')
  185. {
  186. switch ($sFormat) {
  187. case 'html':
  188. echo $mixed;
  189. break;
  190. case 'json':
  191. default:
  192. header('Content-Type: application/json; charset=utf-8');
  193. echo json_encode($mixed);
  194. }
  195. exit;
  196. }
  197. protected function _getContextName()
  198. {
  199. return str_replace('sys_', 'recom_', $this->_sObject);
  200. }
  201. protected function _getCriterionParams($iProfileId, $aParams)
  202. {
  203. $aResult = ['profile_id' => $iProfileId];
  204. if(empty($aParams))
  205. return $aResult;
  206. if(is_string($aParams))
  207. $aParams = unserialize($aParams);
  208. if(!empty($aParams) && is_array($aParams))
  209. $aResult = array_merge($aResult, $aParams);
  210. return $aResult;
  211. }
  212. /**
  213. * The first variant. Isn't used.
  214. */
  215. public function processCriteriaForSelection($iProfileId, $iStart, $iPerPage, &$bShowPaginate)
  216. {
  217. if(($iStartGet = bx_get('start')) !== false)
  218. $iStart = (int)bx_get('start');
  219. $aStarts = [];
  220. if(($sStartsGet = bx_get('starts')) !== false)
  221. $aStarts = array_combine(array_keys($this->_aCriteria), json_decode($sStartsGet, true));
  222. $aWeights = array_map(function($fValue) use ($iPerPage) { return $fValue * $iPerPage; }, $this->_aObject['weights']);
  223. $aCriterionItems = [];
  224. foreach($this->_aCriteria as $sCriterion => $aCriterion) {
  225. $aCriterionParams = $this->_getCriterionParams($iProfileId, $aCriterion['params']);
  226. $iCriterionWeight = (float)$aCriterion['weight'];
  227. $iCriterionStart = isset($aStarts[$sCriterion]) ? (int)$aStarts[$sCriterion] : $iStart * $iCriterionWeight;
  228. switch($aCriterion['source_type']) {
  229. case 'sql':
  230. $aCriterionItems[$sCriterion] = [];
  231. if(empty($aCriterion['source']))
  232. break;
  233. $sQuery = bx_replace_markers($aCriterion['source'], $aCriterionParams);
  234. if($this->_aObject['countable'])
  235. $sQuery .= ' ORDER BY `value` DESC';
  236. $sQuery .= ' LIMIT ' . $iCriterionStart . ', ' . $iPerPage;
  237. $aCriterionItems[$sCriterion] = $this->_oDb->getPairs($sQuery, 'id', 'value');
  238. break;
  239. case 'service':
  240. //TODO: Realize this!
  241. break;
  242. }
  243. }
  244. $aResults = [];
  245. $aHeap = [];
  246. $aStats = [];
  247. foreach($aCriterionItems as $sCriteria => $aItems) {
  248. $iCriteriaAdded = 0;
  249. $iCriteriaUpdated = 0;
  250. foreach($aItems as $iId => $iValue) {
  251. $bAddded = isset($aResults[$iId]);
  252. if(!$bAddded && $iCriteriaAdded < $aWeights[$sCriteria]) {
  253. $aResults[$iId] = $iValue;
  254. $iCriteriaAdded++;
  255. }
  256. else {
  257. if($bAddded) {
  258. $aResults[$iId] += $iValue;
  259. $iCriteriaUpdated++;
  260. }
  261. else
  262. $aHeap[$sCriteria][$iId] = $iValue;
  263. }
  264. }
  265. $aStats[$sCriteria] = $iCriteriaAdded + $iCriteriaUpdated;
  266. }
  267. $iResults = count($aResults);
  268. if($iResults < $iPerPage && !empty($aHeap)) {
  269. foreach($aHeap as $sCriteria => $aItems) {
  270. if(!$aItems)
  271. continue;
  272. arsort($aItems);
  273. foreach($aItems as $iId => $iValue) {
  274. $aResults[$iId] = $iValue;
  275. $aStats[$sCriteria]++;
  276. if(++$iResults == $iPerPage)
  277. break;
  278. }
  279. }
  280. }
  281. arsort($aResults);
  282. return $aResults;
  283. }
  284. }
  285. /** @} */