BxDolRecommendation.php 13 KB

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