Skip to content

Commit

Permalink
Ticket unacms#4319 - Profiles: Improve Recommended profiles functiona…
Browse files Browse the repository at this point in the history
…lity.
  • Loading branch information
AntonLV committed Jun 20, 2023
1 parent 83eb53a commit 2aac8c4
Show file tree
Hide file tree
Showing 16 changed files with 818 additions and 10 deletions.
267 changes: 267 additions & 0 deletions inc/classes/BxDolRecommendation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
/**
* Copyright (c) UNA, Inc - https://una.io
* MIT License - https://opensource.org/licenses/MIT
*/

class BxDolRecommendation extends BxDolFactory implements iBxDolFactoryObject
{
protected $_bIsApi;

protected $_oDb;

protected $_iObject;
protected $_sObject;
protected $_aObject;
protected $_aCriteria;

protected $_iProfileId;

protected $_iPerPageDefault;
protected $_iReducerDefault;

protected function __construct($aObject)
{
parent::__construct();

$this->_bIsApi = bx_is_api();

$this->_aObject = $aObject['object'];
$this->_iObject = (int)$this->_aObject['id'];
$this->_sObject = $this->_aObject['name'];
$this->_aCriteria = $aObject['criteria'];

$this->_oDb = new BxDolRecommendationQuery();
$this->_oDb->init($this->_aObject);

$this->_iProfileId = bx_get_logged_profile_id();

$this->_iPerPageDefault = 4;
$this->_iReducerDefault = 10;
}

public static function getObjectInstance($sObject)
{
if(isset($GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject]))
return $GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject];

$aObject = BxDolRecommendationQuery::getObject($sObject);
if(!$aObject || !is_array($aObject) || empty($aObject['object']))
return false;

$sClass = 'BxTemplRecommendation';
if(!empty($aObject['object']['class_name'])) {
$sClass = $aObject['object']['class_name'];
if(!empty($aObject['object']['class_file']))
require_once(BX_DIRECTORY_PATH_ROOT . $aObject['object']['class_file']);
}

$o = new $sClass($aObject);
return ($GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject] = $o);
}

public static function updateData($iProfileId = 0)
{
if(!$iProfileId)
$iProfileId = bx_get_logged_profile_id();

BxDolCronQuery::getInstance()->addTransientJobService('recommendations_for_' . $iProfileId, ['system', 'updateRecommendations', [$iProfileId], 'TemplServiceProfiles']);
}

public function actionIgnore($iProfileId = 0, $iItemId = 0)
{
if(!$iProfileId)
$iProfileId = $this->_iProfileId;

if(!$iItemId)
$iItemId = bx_process_input(bx_get('id'), BX_DATA_INT);

if(!$this->ignore($iProfileId, $iItemId))
return ['msg' => '_sys_txt_error_occured'];

return ['code' => 0];
}

public function ignore($iProfileId, $iItemId)
{
$aItem = $this->_oDb->getItem($iProfileId, $this->_iObject, $iItemId);

$iReducer = !empty($aItem['item_reducer']) ? (int)$aItem['item_reducer'] : 1;
$iReducer *= $this->_iReducerDefault;

return $this->_oDb->update($iProfileId, $this->_iObject, $iItemId, ['item_reducer' => $iReducer]);
}

public function processCriteria($iProfileId, $iLimit = 100)
{
$aWeights = array_map(fn($fValue): float => $fValue * $iLimit, $this->_aObject['weights']);

$aCriterionItems = [];
foreach($this->_aCriteria as $sCriterion => $aCriterion) {
$iCriterionWeight = (float)$aCriterion['weight'];

$aParams = ['profile_id' => $iProfileId];
if(!empty($aCriterion['params'])) {
$aCriterionParams = unserialize($aCriterion['params']);
if(!empty($aCriterionParams) && is_array($aCriterionParams))
$aParams = array_merge($aParams, $aCriterionParams);
}

switch($aCriterion['source_type']) {
case 'sql':
$aCriterionItems[$sCriterion] = [];

if(empty($aCriterion['source']))
break;

$sQuery = bx_replace_markers($aCriterion['source'], $aParams);
if($this->_aObject['countable'])
$sQuery .= ' ORDER BY `value` DESC';
$sQuery .= ' LIMIT ' . $iLimit;

$aCriterionItems[$sCriterion] = $this->_oDb->getPairs($sQuery, 'id', 'value');
break;

case 'service':
//TODO: Realize this!
break;
}
}

$aResults = [];
foreach($aCriterionItems as $sCriteria => $aItems) {
$iCriteriaResults = 0;
foreach($aItems as $iId => $iValue) {
if(!isset($aResults[$iId]))
$aResults[$iId] = 0;

$aResults[$iId] += $iValue;

if(++$iCriteriaResults >= $aWeights[$sCriteria])
break;
}
}

$this->_oDb->clean($iProfileId, $this->_iObject);

foreach($aResults as $iId => $iValue)
$this->_oDb->add($iProfileId, $this->_iObject, $iId, $iValue);
}

public function outputActionResult ($mixed, $sFormat = 'json')
{
switch ($sFormat) {
case 'html':
echo $mixed;
break;

case 'json':
default:
header('Content-Type: application/json; charset=utf-8');
echo json_encode($mixed);
}
exit;
}

protected function _getContextName()
{
return str_replace('sys_', 'recom_', $this->_sObject);
}

/**
* The first variant. Isn't used.
*/
public function processCriteriaForSelection($iProfileId, $iStart, $iPerPage, &$bShowPaginate)
{
if(($iStartGet = bx_get('start')) !== false)
$iStart = (int)bx_get('start');

$aStarts = [];
if(($sStartsGet = bx_get('starts')) !== false)
$aStarts = array_combine(array_keys($this->_aCriteria), json_decode($sStartsGet, true));

$aWeights = array_map(fn($fValue): float => $fValue * $iPerPage, $this->_aObject['weights']);

$aCriterionItems = [];
foreach($this->_aCriteria as $sCriterion => $aCriterion) {
$iCriterionWeight = (float)$aCriterion['weight'];

$aParams = ['profile_id' => $iProfileId];
if(!empty($aCriterion['params'])) {
$aCriterionParams = unserialize($aCriterion['params']);
if(!empty($aCriterionParams) && is_array($aCriterionParams))
$aParams = array_merge($aParams, $aCriterionParams);
}

$iCriterionStart = isset($aStarts[$sCriterion]) ? (int)$aStarts[$sCriterion] : $iStart * $iCriterionWeight;

switch($aCriterion['source_type']) {
case 'sql':
$aCriterionItems[$sCriterion] = [];

if(empty($aCriterion['source']))
break;

$sQuery = bx_replace_markers($aCriterion['source'], $aParams);
if($this->_aObject['countable'])
$sQuery .= ' ORDER BY `value` DESC';
$sQuery .= ' LIMIT ' . $iCriterionStart . ', ' . $iPerPage;

$aCriterionItems[$sCriterion] = $this->_oDb->getPairs($sQuery, 'id', 'value');
break;

case 'service':
//TODO: Realize this!
break;
}
}

$aResults = [];
$aHeap = [];
$aStats = [];
foreach($aCriterionItems as $sCriteria => $aItems) {
$iCriteriaAdded = 0;
$iCriteriaUpdated = 0;
foreach($aItems as $iId => $iValue) {
$bAddded = isset($aResults[$iId]);
if(!$bAddded && $iCriteriaAdded < $aWeights[$sCriteria]) {
$aResults[$iId] = $iValue;
$iCriteriaAdded++;
}
else {
if($bAddded) {
$aResults[$iId] += $iValue;
$iCriteriaUpdated++;
}
else
$aHeap[$sCriteria][$iId] = $iValue;
}
}

$aStats[$sCriteria] = $iCriteriaAdded + $iCriteriaUpdated;
}

$iResults = count($aResults);
if($iResults < $iPerPage && !empty($aHeap)) {
foreach($aHeap as $sCriteria => $aItems) {
if(!$aItems)
continue;

arsort($aItems);

foreach($aItems as $iId => $iValue) {
$aResults[$iId] = $iValue;

$aStats[$sCriteria]++;
if(++$iResults == $iPerPage)
break;
}
}
}

arsort($aResults);
return $aResults;
}
}

/** @} */
118 changes: 118 additions & 0 deletions inc/classes/BxDolRecommendationQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php defined('BX_DOL') or die('hack attempt');
/**
* Copyright (c) UNA, Inc - https://una.io
* MIT License - https://opensource.org/licenses/MIT
*
* @defgroup UnaCore UNA Core
* @{
*/

class BxDolRecommendationQuery extends BxDolDb
{
public static $sTableObjects = 'sys_objects_recommendation';
public static $sTableCriteria = 'sys_recommendation_criteria';
public static $sTableData = 'sys_recommendation_data';

protected $_aObject;

public function __construct()
{
parent::__construct();

$this->_aObject = [];
}

static public function getObjects($bActiveOnly = true)
{
$sWhereClause = "";
if($bActiveOnly)
$sWhereClause = " AND `active` = 1";

return BxDolDb::getInstance()->getAllWithKey("SELECT * FROM `" . self::$sTableObjects . "` WHERE 1" . $sWhereClause, 'name');
}

static public function getObject($sName)
{
$oDb = BxDolDb::getInstance();

$aObject = $oDb->getRow("SELECT * FROM `" . self::$sTableObjects . "` WHERE `name` = :name", ['name' => $sName]);
if(!$aObject || !is_array($aObject))
return false;

$aCriteria = $oDb->getAllWithKey("SELECT * FROM `" . self::$sTableCriteria . "` WHERE `object_id` = :object_id AND `weight` > 0 AND `active` = 1 ORDER BY `weight` DESC", 'name', ['object_id' => $aObject['id']]);
if(!$aCriteria || !is_array($aCriteria))
return false;

$aObject['weights'] = $oDb->getPairs("SELECT `name`, `weight` FROM `" . self::$sTableCriteria . "` WHERE `object_id` = :object_id AND `weight` > 0 AND `active` = 1 ORDER BY `weight` DESC", 'name', 'weight', ['object_id' => $aObject['id']]);

return [
'object' => $aObject,
'criteria' => $aCriteria
];
}

public function init($aObject = [])
{
if(empty($aObject) || !is_array($aObject))
return;

$this->_aObject = $aObject;
}

public function clean($iProfileId, $iObjectId, $bAll = false)
{
$sWhereClause = "";
if(!$bAll)
$sWhereClause = " AND `item_reducer` = 0";

return $this->query("DELETE FROM `" . self::$sTableData . "` WHERE `profile_id` = :profile_id AND `object_id` = :object_id" . $sWhereClause, [
'profile_id' => $iProfileId,
'object_id' => $iObjectId
]);
}

public function add($iProfileId, $iObjectId, $iItemId, $iItemValue)
{
return $this->query("INSERT INTO `" . self::$sTableData . "` (`profile_id`, `object_id`, `item_id`, `item_value`) VALUES (:profile_id, :object_id, :item_id, :item_value) ON DUPLICATE KEY UPDATE `item_value` = :item_value", [
'profile_id' => $iProfileId,
'object_id' => $iObjectId,
'item_id' => $iItemId,
'item_value' => $iItemValue
]) !== false;
}

public function update($iProfileId, $iObjectId, $iItemId, $aSet)
{
if(empty($aSet) || !is_array($aSet))
return false;

return $this->query("UPDATE `" . self::$sTableData . "` SET " . $this->arrayToSQL($aSet) . " WHERE `profile_id` = :profile_id AND `object_id` = :object_id AND `item_id` = :item_id ", [
'profile_id' => $iProfileId,
'object_id' => $iObjectId,
'item_id' => $iItemId
]) !== false;
}

public function get($iProfileId, $iObjectId, $iStart = 0, $iPerPage = 0)
{
$sLimitClause = "";
if($iPerPage)
$sLimitClause = " LIMIT " . $iStart . ", " . $iPerPage;

return $this->getPairs("SELECT `item_id` AS `id`, (`item_value` - `item_reducer`) AS `value` FROM `" . self::$sTableData . "` WHERE `profile_id` = :profile_id AND `object_id` = :object_id AND (`item_value` - `item_reducer`) > 0 ORDER BY `value` DESC" . $sLimitClause, 'id', 'value', [
'profile_id' => $iProfileId,
'object_id' => $iObjectId
]);
}

public function getItem($iProfileId, $iObjectId, $iItemId)
{
return $this->getRow("SELECT * FROM `" . self::$sTableData . "` WHERE `profile_id` = :profile_id AND `object_id` = :object_id AND `item_id` = :item_id", [
'profile_id' => $iProfileId,
'object_id' => $iObjectId,
'item_id' => $iItemId
]);
}
}

/** @} */
Loading

0 comments on commit 2aac8c4

Please sign in to comment.