This commit is contained in:
2025-10-04 11:46:59 +07:00
commit 97427d7cff
498 changed files with 47596 additions and 0 deletions

View File

@@ -0,0 +1,297 @@
<?php
namespace Hura8\System\Model;
use Hura8\Database\iConnectDB;
use Hura8\System\IDGenerator;
class AuthModel
{
/* @var iConnectDB $db */
protected $db;
private $tb_login = '';
private $tb_access_code = '';
private $tb_onetime_key = '';
public function __construct($tb_login, $tb_access_code)
{
$this->tb_login = $tb_login;
$this->tb_access_code = $tb_access_code;
$this->db = get_db('', ENABLE_DB_DEBUG);
}
private const ACCESS_CODE_LENGTH = 30;
const ONE_TIME_KEY_LENGTH = 15;
public function checkOneTimeKey($auth_key) {
$db_response = $this->db->select(
$this->tb_onetime_key,
['user_id', 'user_name', 'client_id', 'create_time'],
[
'auth_key' => ["=", $auth_key],
],
'',
1
);
if($db_response->getCode()) {
return null;
}
$info = $db_response->getData();
if($info) {
// used ONCE and delete the key
$this->db->runQuery("DELETE FROM `".$this->tb_onetime_key."` WHERE `user_id` = ? ", ['s'], [$info['user_id']]);
return $info;
}
return false;
}
// auth key allows users to export data (i.e. export to excel) for offline use
public function createNewOneTimeKey($user_id) {
// and make a new one
$auth_key = IDGenerator::createStringId(self::ONE_TIME_KEY_LENGTH);
$this->db->insert($this->tb_onetime_key, [
'user_id' => $user_id,
'auth_key' => $auth_key,
'create_time' => CURRENT_TIME,
]);
return $auth_key;
}
// for all subsequent requests, API need to provide access-code in the request's header
// the server will verify the code
public function checkAccessCode($access_code) {
return $this->db->select(
$this->tb_access_code,
['user_id', 'create_time'],
[
'access_code' => ["=", $access_code],
],
'',
1
);
}
public function deleteAllAccessCode($user_id) {
$this->db->runQuery("DELETE FROM `".$this->tb_access_code."` WHERE `user_id` = ? ", ['s'], [$user_id]);
}
protected function createNewAccessCode($user_id) {
// when use login here, delete all other access code
$this->deleteAllAccessCode($user_id);
// and make a new one
$access_code = IDGenerator::createStringId(self::ACCESS_CODE_LENGTH);
$user_device = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
$db_response = $this->db->insert($this->tb_access_code, [
'user_id' => $user_id,
'access_code' => $access_code,
'user_device' => substr($user_device, 0, 150),
'create_time' => CURRENT_TIME,
]);
return $access_code;
}
/**
* allow to login via mobile
* - step 1: sms an OTP code to mobile number
* - step 2: verify OTP code (mobile number and otp code sent along in the form)
*/
public function checkLoginViaMobile($mobile, $otp) {
// todo
}
/**
* @description An OTP code is sent to user's email. This method helps user need not remember the password
* @param $email string
* @param $otp string
*/
public function checkLoginByOTP($user_id, $otp) {
$info = $this->db->select(
$this->tb_login,
[],
[
'user_id' => ["=", $user_id],
'login_otp' => ["=", $otp],
],
'',
1
);
if($info) {
// return to browser
return array(
'user_id' => $user_id,
'access_code' => $this->createNewAccessCode($user_id),
);
}
return false;
}
public function createLoginOTP($user_id) {
// check email exist
$info = $this->db->select(
$this->tb_login,
[],
[
'user_id' => ["=", $user_id],
],
'',
1
);
if($info) {
$otp = IDGenerator::createStringId(6);
$this->db->update(
$this->tb_login,
[
'login_otp' => $otp
],
[
'user_id' => $info['user_id'],
]
);
return $otp;
}
return false;
}
public function createOrUpdatePassword($user_id, $new_password) {
$query = $this->db->runQuery(
"SELECT `user_id` FROM `".$this->tb_login."` WHERE `user_id` = ? LIMIT 1 ",
['d'], [ $user_id ]
);
if($this->db->fetchAssoc($query)) {
return $this->updatePassword($user_id, $new_password);
}
return $this->createPassword($user_id, $new_password);
}
protected function createPassword($user_id, $new_password) {
return $this->db->insert(
$this->tb_login,
[
'user_id' => $user_id,
'password_hash' => $this->hashPassword($new_password),
'create_time' => CURRENT_TIME,
'create_by' => '',
]
);
}
protected function updatePassword($user_id, $new_password) {
return $this->db->update(
$this->tb_login,
[
'password_hash' => $this->hashPassword($new_password),
'last_update' => CURRENT_TIME,
'last_update_by' => ADMIN_NAME,
],
[
'user_id' => $user_id,
]
);
}
/**
* @param $user_id int
* @param $password string
* @return array|false
*/
public function checkLogin($user_id, $password) {
$info = $this->db->select(
$this->tb_login,
[ 'password_hash'],
[
'user_id' => ["=", $user_id],
],
'',
1
);
//test password
if($info && $this->verifyHash($password, $info['password_hash'])) {
$this->updateUserLogin($user_id);
// return to browser
return array(
'user_id' => $user_id,
'access_code' => $this->createNewAccessCode($user_id),
);
}
return false;
}
private function updateUserLogin($user_id){
return $this->db->update(
$this->tb_login,
[
'last_login_time' => CURRENT_TIME,
'last_login_ip' => USER_IP,
'last_login_device' => '',
'last_login_session_id' => \Hura8\System\Security\Session::id() ?: '',
'last_login_browser' => USER_AGENT,
],
[
'user_id' => $user_id,
]
);
}
/**
* @param $str string
* @return string
*/
private function hashPassword($str) {
return password_hash($str, PASSWORD_BCRYPT, array('cost' => 12 ));
}
/**
* 15-04-2016 verify string with given hash
* @param $str_to_verify string
* @param $hash string
* @return boolean
*/
private function verifyHash($str_to_verify, $hash) {
return (password_verify($str_to_verify, $hash));
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Hura8\System\Model;
use Hura8\Interfaces\AppResponse;
use Hura8\Interfaces\EntityType;
class DomainModel extends aEntityBaseModel
{
public function __construct() {
parent::__construct(EntityType::DOMAIN);
}
protected function extendedFilterOptions(): array
{
return [
];
}
public function addNewDomain($clean_domain, $language = DEFAULT_LANGUAGE){
return $this->create([
'domain' => $clean_domain,
'lang' => $language,
]);
}
protected function getInfoByDomain($clean_domain){
$query = $this->db->runQuery(
"SELECT * from ".$this->tb_entity." WHERE `domain` = ? LIMIT 1 ",
['s'], [ $clean_domain ]
);
return $this->db->fetchAssoc($query);
}
public function deleteDomain($clean_domain){
$domain_info = $this->getInfoByDomain($clean_domain);
if($domain_info) {
$this->delete($domain_info['id']);
}
return true;
}
public function setDomainMain($domain, $language){
$this->db->update(
$this->tb_entity,
[ "is_main" => 0, ],
[
"lang" => $language,
"is_main" => 1
]
);
$this->db->update(
$this->tb_entity,
[
"is_main" => 1,
"last_update" => CURRENT_TIME,
"last_update_by" => ADMIN_NAME,
],
[
"domain" => $domain,
]
);
return true;
}
public function setDomainLayout($domain, $layout = 'pc'){
$this->db->update(
$this->tb_entity,
[ "layout" => $layout, ],
[ "domain" => $domain, ]
);
return true;
}
protected function _buildQueryConditionExtend(array $condition) : ?array
{
$where_condition = "";
$bind_types = [];
$bind_values = [];
if(isset($condition['language']) && $condition['language']) {
$where_condition = " AND `lang` = ? ";
$bind_types[] = 's';
$bind_values[] = $condition['language'];
}
return [
$where_condition,
$bind_types,
$bind_values,
];
}
protected function beforeCreateItem(array $input_info) : AppResponse
{
$info = $input_info;
// if domain exist
if($this->getInfoByDomain($info['domain'])) {
return new AppResponse('error', "Domain exist");
}
$info['create_time'] = CURRENT_TIME;
$info['create_by'] = ADMIN_NAME;
return new AppResponse('ok', null, $info);
}
protected function afterCreateItem($new_item_id, $new_item_info)
{
// TODO: Implement afterCreateItem() method.
}
protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse
{
$info = $new_input_info;
$info['last_update'] = CURRENT_TIME;
$info['last_update_by'] = ADMIN_NAME;
return new AppResponse('ok', null, $info);
}
protected function afterUpdateItem($item_id, $old_item_info, $new_item_info)
{
// TODO: Implement afterUpdateItem() method.
}
protected function beforeDeleteItem($item_id, $item_info) : AppResponse
{
return new AppResponse('ok');
}
protected function afterDeleteItem($item_id, $item_info)
{
// TODO: Implement afterDeleteItem() method.
}
}

View File

@@ -0,0 +1,456 @@
<?php
namespace Hura8\System\Model;
use Hura8\Database\iConnectDB;
use Hura8\Interfaces\AppResponse;
use Hura8\Interfaces\EntityType;
use Hura8\Interfaces\iEntityLanguageModel;
use Hura8\Interfaces\iEntityModel;
use Hura8\System\Config;
use Hura8\System\Controller\UrlManagerController;
use Hura8\System\ModuleManager;
abstract class EntityLanguageModel implements iEntityLanguageModel
{
/* @var iConnectDB $db */
protected $db;
protected $entity_type = '';
protected $tb_entity = "";
protected $tb_entity_lang_template = "tb_entity_lang_template";
protected $tb_track_entity_lang = "tb_track_entity_lang";
// these fields are set in config file at: config/client/language_fields.php
protected $language_fields = [
//"title",
//"summary",
];
protected $tb_entity_lang = "";
protected $language = ''; // to be set by controllers
protected $allow_richtext_fields = []; // only table's column fields in this list allowed to retain html tags, else strip all
public function __construct($entity_type, $tb_entity = "", $allow_richtext_fields = [])
{
$this->entity_type = $entity_type;
$this->tb_entity = $this->inferTbEntity($tb_entity);
$this->tb_entity_lang = $this->tb_entity."_lang";
$this->language_fields = (Config::getEntityLanguageFields())[$this->tb_entity] ?? [];
if(!sizeof($this->language_fields)) {
die("No language_fields found for ".$this->tb_entity." on file: config/client/language_fields.php");
}
$this->allow_richtext_fields = $allow_richtext_fields;
$this->db = get_db('', ENABLE_DB_DEBUG);
}
protected function inferTbEntity(string $tb_entity = '') : string
{
if(!$tb_entity) {
// auto infer
$tb_name = str_replace("-", "_", preg_replace("/[^a-z0-9_\-]/i", "", $this->entity_type));
return "tb_".strtolower($tb_name);
}
return $tb_entity;
}
// set language to be used by model
public function setLanguage($language) : bool
{
$this->language = $language;
return true;
}
public function createTableLang(): AppResponse {
$result = $this->db->checkTableExist($this->tb_entity_lang);
// check if table exist
if(!$result) {
$this->db->runQuery("CREATE TABLE `".$this->tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` ");
// recheck
$result = $this->db->checkTableExist($this->tb_entity_lang);
}
return new AppResponse($result ? 'ok': 'error');
}
public function recreateTableLang() {
if($this->db->checkTableExist($this->tb_entity_lang)) {
$this->db->runQuery("DROP TABLE `".$this->tb_entity_lang."` ");
}
$this->db->runQuery("CREATE TABLE `".$this->tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` ");
}
// any fields to have language: title, summary, description, price, etc...
public function setLanguageFields(array $language_fields) {
$this->language_fields = $language_fields;
}
public function getLanguageFields() : array {
return $this->language_fields;
}
protected function beforeCreateItem(array $input_info) {
$info = [];
foreach ($input_info as $key => $value) {
if(!in_array($key, $this->language_fields)) continue;
$info[$key] = $value;
}
return (sizeof($info) > 0) ? $info : false;
}
protected function beforeUpdateItem($current_item_info, $new_input_info) {
$info = [];
$has_change = false;
foreach ($new_input_info as $key => $value) {
if(!in_array($key, $this->language_fields)) continue;
if(!isset($current_item_info[$key]) || $current_item_info[$key] != $value) {
$has_change = true;
}
$info[$key] = $value;
}
return ($has_change) ? $info : false;
}
public function getEntityType() : string
{
return $this->entity_type;
}
public function getTranslatedIds(): array
{
$query = $this->db->runQuery(
"SELECT `item_id` FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `lang` = ? LIMIT 50000",
['s', 's'],
[ $this->entity_type, $this->language]
);
return array_map(function ($item) {
return $item['item_id'];
}, $this->db->fetchAll($query));
}
/**
* @description prevent xss attack by stripping all html tags from un-permitted fields (which mostly are)
* - only fields in ::allow_richtext_fields can keep html tags. Example: description
* - if value is array (example data=['title' => '', 'description' => html tags ]) ...
then to keep `description`, both `data` and `description` must be in the ::allow_richtext_fields
because this method checks on array value recursively
* @param array $input_info
* @return array
*/
protected function cleanRichtextFields(array $input_info) : array {
$cleaned_info = [];
foreach ($input_info as $key => $value) {
if(in_array($key, $this->allow_richtext_fields)) {
$cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : $value;
}else{
$cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : strip_tags($value);
}
}
return $cleaned_info;
}
public function update($id, array $new_input_info, $search_keyword = "") : AppResponse
{
$new_input_info = $this->cleanRichtextFields($new_input_info);
// clean url_index if any
if(isset($new_input_info['url_index']) && $new_input_info['url_index']) {
$new_input_info['url_index'] = UrlManagerController::create_url_index($new_input_info['url_index']);
// create url for this language if url_index setup
if(in_array('url_index', $this->language_fields) && isset($new_input_info['url_index'])) {
$request_path = $this->updateUrl($id, $new_input_info['url_index']);
$new_input_info['request_path'] = $request_path;
// request_path must be auto allowed in language_fields if url_index in there. If not, update the language_fields
// so the beforeCreateItem method can accept it
// request_path is needed for PublicEntityBaseControllerTraits to build public urls for items in when view the language
if(!in_array('request_path', $this->language_fields)){
$this->setLanguageFields(array_merge($this->language_fields, ['request_path']));
}
}
}
$current_info = $this->getInfo($id);
if(!$current_info) {
$data = $this->beforeCreateItem($new_input_info);
if(!$data) {
//debug_var($new_input_info);
//die("EntityLanguageModel ".$this->entity_type.": beforeCreateItem no data");
return new AppResponse("error", "EntityLanguageModel ".$this->entity_type.": beforeCreateItem no data");
}
// create
$this->db->insert(
$this->tb_entity_lang,
array(
"id" => $id,
"lang" => $this->language,
"data" => $data,
"create_time" => CURRENT_TIME,
"create_by" => ADMIN_NAME,
"last_update" => CURRENT_TIME,
"last_update_by" => ADMIN_NAME,
)
);
// track item
$this->db->insert(
$this->tb_track_entity_lang,
array(
"item_type" => $this->entity_type,
"item_id" => $id,
"lang" => $this->language,
"create_time" => CURRENT_TIME,
"create_by" => ADMIN_NAME,
)
);
return new AppResponse("ok");
}
// update
$new_change_info = $this->beforeUpdateItem($current_info, $new_input_info);
if(!$new_change_info) {
return new AppResponse("error", "EntityLanguageModel ".$this->entity_type.": Nothing changed");
}
$updated_info = [
"data" => array_merge($current_info, $new_change_info), // make sure update 1 key won't affect the other keys in the language
"last_update" => CURRENT_TIME,
"last_update_by" => ADMIN_NAME,
];
$result = $this->db->update(
$this->tb_entity_lang,
$updated_info,
array(
"id" => $id,
"lang" => $this->language,
)
);
return new AppResponse("ok", null, $result);
}
protected function updateUrl($id, $lang_url_index) {
/*
Steps:
- infer entity model -> get default url setting (url_type, request_path, ...)
- find RequestPathConfig for entity
- create RequestPath for this language
- insert/update in tb-url
'url_type' => $url_type,
'request_path' => $request_path ,
'id_path' => $id_path,
'redirect_code' => $redirect_code,
*/
$baseModel = $this->createEntityBaseModelInstance();
$base_info = $baseModel->getInfo($id);
if(!$base_info || !$base_info['request_path']) {
return false;
}
//debug_var($base_info['request_path']);
$objUrlManager = new UrlManagerController();
$item_url_info = $objUrlManager->getUrlInfoByRequestPath($base_info['request_path']);
//debug_var($item_url_info);
$url_module = $item_url_info['module'] ;
$url_view = $item_url_info['view'] ;
$module_routing = ModuleManager::getModuleRouting($url_module);
$request_path_config = isset($module_routing[$url_view]) ? $module_routing[$url_view]['url_manager']['request_path'] : '';
if(!$request_path_config) {
return false;
}
$request_path = UrlManagerController::translateRequestPathConfig($request_path_config, $id, $lang_url_index);
if($request_path == $item_url_info['request_path']) {
return false;
}
$id_path = $item_url_info['id_path']."/_l:".$this->language;
$objUrlManager->createUrl($item_url_info['url_type'], $request_path, $id_path, 0);
return $request_path;
}
protected function createEntityBaseModelInstance() : iEntityModel
{
$class = EntityType::getModelClass($this->entity_type);
if(class_exists($class)) {
return _init_class($class);
}
die($class." not exist!");
}
// delete all languages for items
public function deleteAll($id): AppResponse
{
$this->db->runQuery(
"DELETE FROM `".$this->tb_entity_lang."` WHERE `id` = ? ",
['d'],
[$id]
);
$this->db->runQuery(
"DELETE FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `item_id` = ? ",
['s', 's'],
[ $this->entity_type, $id]
);
return new AppResponse("ok");
}
public function delete($id): AppResponse
{
$result = $this->db->runQuery(
"DELETE FROM `".$this->tb_entity_lang."` WHERE `id` = ? AND `lang` = ? LIMIT 1 ",
['d', 's'],
[$id, $this->language], true
);
if($result) {
$this->db->runQuery(
"DELETE FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `item_id` = ? AND `lang` = ? LIMIT 1 ",
['s', 's', 's'],
[ $this->entity_type ,$id, $this->language],
true
);
}
return new AppResponse("ok", null, $result);
}
public function getListByIds(array $list_id): array
{
if(!sizeof($list_id)) return array();
list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_id, 'int');
$bind_types[] = "s";
$bind_values = $list_id;
$bind_values[] = $this->language;
$query = $this->db->runQuery(
" SELECT * FROM ".$this->tb_entity_lang." WHERE `id` IN (".$parameterized_ids.") AND `lang` = ? ",
$bind_types,
$bind_values
);
$item_list = [];
foreach ($this->db->fetchAll($query) as $item) {
$item_list[$item['id']] = $this->formatItemInfo($item);
}
return $item_list;
}
public function getInfo($id) : ?array
{
$query = $this->db->runQuery(
"SELECT * FROM `".$this->tb_entity_lang."` WHERE `id` = ? AND `lang` = ? LIMIT 1 ",
['d', 's'],
[$id, $this->language]
) ;
if( $item_info = $this->db->fetchAssoc($query)){
return $this->formatItemInfo($item_info);
}
return null;
}
protected function formatItemInfo(array $item_info) : array
{
return ($item_info['data']) ? \json_decode($item_info['data'], true) : [];
}
public function getTotal(array $condition): int
{
$query = $this->db->runQuery(
" SELECT COUNT(*) AS total FROM `".$this->tb_entity_lang."` WHERE `lang` = ? " ,
['s'], [ $this->language ]
);
$total = 0;
if ($rs = $this->db->fetchAssoc($query)) {
$total = $rs['total'];
}
return $total;
}
// get empty/default item for form
public function getEmptyInfo() : array {
$empty_info = [
"id" => 0,
"lang" => $this->language,
"create_time" => 0,
"create_by" => "",
"last_update" => 0,
"last_update_by" => "",
];
foreach ($this->language_fields as $field) {
$empty_info[$field] = '';
}
return $empty_info;
}
}

View File

@@ -0,0 +1,341 @@
<?php
namespace Hura8\System\Model;
use Hura8\Database\iConnectDB;
use Hura8\System\Security\DataClean;
use Hura8\System\Security\DataType;
class RelationModel
{
/* @var $db iConnectDB */
protected $db;
protected $tb_relation = "tb_relation";
protected $main_item_type= '';
protected $main_item_id= 0;
public function __construct($main_item_type, $main_item_id = 0) {
$this->db = get_db('', ENABLE_DB_DEBUG);
$this->main_item_type = $main_item_type;
$this->main_item_id = $main_item_id;
}
//given related_type and type_id, get all the main item ids
public function getMainItems($related_item_type, $related_item_id = 0, array $condition = [], $return_type = 'list') {
$where_clause = " AND `main_item_type` = '" . $this->db->escape($this->main_item_type) . "'
AND `main_item_id` = '".intval($this->main_item_id)."' ";
$where_clause .= " AND `related_item_type` = '" . $this->db->escape($related_item_type) . "' ";
if($related_item_id) $where_clause .= " AND `related_item_id` = '".intval($related_item_id)."' ";
//excluded_ids
if(isset($condition["excluded_ids"]) && $condition["excluded_ids"] ){
$list_ids = DataClean::makeListOfInputSafe(explode(",", $condition["excluded_ids"]), DataType::INTEGER);
if(sizeof($list_ids)) $where_clause .= " AND `related_item_id` NOT IN (".join(',', $list_ids ).") ";
}
//included_ids
if(isset($condition["included_ids"]) && $condition["included_ids"] ){
$list_ids = DataClean::makeListOfInputSafe(explode(",", $condition["included_ids"]), DataType::INTEGER);
if(sizeof($list_ids)) $where_clause .= " AND `related_item_id` IN (".join(',', $list_ids ).") ";
}
if($return_type == 'total') {
$query = $this->db->query("
SELECT COUNT(*) as total FROM `".$this->tb_relation."`
WHERE 1 " . $where_clause . " ");
$total = 0;
if ($rs = $this->db->fetchAssoc($query)) {
$total = $rs['total'];
}
return $total;
} else {
$page = isset($condition['page']) ? intval($condition['page']) : 1;
$numPerPage = isset($condition['numPerPage']) ? intval($condition['numPerPage']) : 10;
$query = $this->db->query("
SELECT `related_item_id` FROM `". $this->tb_relation ."`
WHERE 1 " . $where_clause . "
ORDER BY `ordering` DESC, `id` DESC
LIMIT " . ($page - 1) * $numPerPage . ", ". $numPerPage ."
");
$result = array();
foreach ( $this->db->fetchAll($query) as $info ) {
$result[] = $info['related_item_id'];
}
return $result;
}
}
public function updateOrdering($related_item_id, $new_order) {
$this->db->runQuery(
"UPDATE `".$this->tb_relation."` SET
`ordering` = ?
WHERE `main_item_type` = ? AND `main_item_id` = ? AND `related_item_id` = ? ",
['d', 's', 'd', 'd'],
[ $new_order, $this->main_item_type, $this->main_item_id, $related_item_id ]
);
return [
'status' => 'success',
'message' => ''
];
}
//@warn: this does not check if records exist.
public function create(array $related_items, $both_way_relation = true) {
$build_insert = array();
foreach ($related_items as $item) {
if(!$this->checkExist($this->main_item_type, $this->main_item_id, $item['type'], $item['id'])) {
$build_insert[] = [
"main_item_type" => $this->main_item_type,
"main_item_id" => intval($this->main_item_id),
"related_item_type" => $item['type'],
"related_item_id" => intval($item['id']),
"create_time" => CURRENT_TIME,
"create_by" => ADMIN_NAME,
];
}
//if 2-way relation, create item->main
if($both_way_relation) {
if(!$this->checkExist($item['type'], $item['id'], $this->main_item_type, $this->main_item_id)) {
$build_insert[] = [
"main_item_type" => $item['type'],
"main_item_id" => intval($item['id']),
"related_item_type" => $this->main_item_type,
"related_item_id" => intval($this->main_item_id),
"create_time" => CURRENT_TIME,
"create_by" => ADMIN_NAME,
];
}
}
}
if(sizeof($build_insert)) {
$this->db->bulk_insert($this->tb_relation, $build_insert);
return [
'status' => 'success',
'message' => ''
];
}
return [
'status' => 'error',
'message' => 'Đã tồn tại'
];
}
public function checkExist($main_item_type, $main_item_id, $related_item_type, $related_item_id) {
// itself
if($main_item_type == $related_item_type && $main_item_id == $related_item_id) {
return true;
}
$query = $this->db->runQuery(
"SELECT id FROM `".$this->tb_relation."`
WHERE `main_item_type` = ? AND `main_item_id` = ? AND `related_item_type` = ? AND `related_item_id` = ?
LIMIT 1" ,
[
's', 'd', 's', 'd'
],
[
$main_item_type,
intval($main_item_id),
$related_item_type,
intval($related_item_id)
]
);
if($rs = $this->db->fetchAssoc($query)) {
return $rs['id'];
}
return false;
}
//remove a related-item
public function remove($related_item_type, $related_item_id, $remove_both_way = true) {
$main_item_query = ( $this->main_item_id ) ? " `main_item_id` = '".intval($this->main_item_id)."' AND " : "";
$this->db->query("DELETE FROM `".$this->tb_relation."` WHERE
`main_item_type` = '" . $this->db->escape($this->main_item_type) . "' AND
". $main_item_query ."
`related_item_type` = '".$this->db->escape($related_item_type)."' AND
`related_item_id` = '".intval($related_item_id)."' ");
if($remove_both_way) {
$related_item_query = ( $this->main_item_id ) ? " `related_item_id` = '".intval($this->main_item_id)."' AND " : "";
$this->db->query("DELETE FROM `".$this->tb_relation."` WHERE
`related_item_type` = '" . $this->db->escape($this->main_item_type) . "' AND
" . $related_item_query . "
`main_item_type` = '".$this->db->escape($related_item_type)."' AND
`main_item_id` = '".intval($related_item_id)."' ");
}
return [
'status' => 'success',
'message' => ''
];
}
//remove all relate items
public function truncate() {
$this->db->runQuery(
"DELETE FROM `".$this->tb_relation."` WHERE `main_item_type` = ? AND `main_item_id` = ? ",
[ 's', 's' ],
[ $this->main_item_type, $this->main_item_id ]
);
}
//count related items
public function getRelatedItemCount() {
$query = $this->db->runQuery(
"
SELECT `related_item_type`, COUNT(`related_item_id`) AS total FROM `". $this->tb_relation ."`
WHERE `main_item_type` = ? AND `main_item_id` = ?
GROUP BY `related_item_type`
",
[ 's', 's' ], [ $this->main_item_type, $this->main_item_id ]
);
$result = array();
foreach ( $this->db->fetchAll($query) as $info ) {
$result[$info['related_item_type']] = $info['total'];
}
return $result;
}
//extend getRelatedItems to get for list of main_item_ids
public function getRelatedItemsForList(array $main_item_list_ids, array $related_item_types = []) {
if(!sizeof($main_item_list_ids)) return [];
$bind_values = $main_item_list_ids;
list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($main_item_list_ids, 'int');
$condition = " AND `main_item_id` IN (". $parameterized_ids .") ";
$bind_types[] = 's';
$bind_values[] = $this->main_item_type;
$condition .= " AND `main_item_type` = ? ";
if(sizeof($related_item_types)) {
$type_condition = [];
foreach ($related_item_types as $_type) {
$type_condition[] = " `related_item_type` = ? ";
$bind_types[] = 's';
$bind_values[] = $_type;
}
$condition .= " AND ( ".join(' OR ', $type_condition )." ) ";
}
$query = $this->db->runQuery("
SELECT
`main_item_id` ,
`related_item_type`,
`related_item_id`,
`ordering`
FROM `". $this->tb_relation ."`
WHERE 1 ". $condition ."
ORDER BY `ordering` DESC, `id` DESC
LIMIT 5000
", $bind_types, $bind_values);
$result = array();
foreach ( $this->db->fetchAll($query) as $info ) {
$result[$info['main_item_id']][$info['related_item_type']][$info['related_item_id']] = [
"item_id" => $info['related_item_id'],
"ordering" => $info['ordering'],
];
}
//final result
$final_result = [];
foreach ($main_item_list_ids as $_id) {
$final_result[$_id] = (isset($result[$_id])) ? $result[$_id] : false;
}
return $final_result;
}
//get all related items and group them by type
public function getRelatedItems(array $related_item_types = []) {
$bind_types = ['s', 's'];
$bind_values = [$this->main_item_type, $this->main_item_id];
$condition = "";
if(sizeof($related_item_types)) {
$type_condition = [];
foreach ($related_item_types as $_type) {
$type_condition[] = " `related_item_type` = ? ";
$bind_types[] = 's';
$bind_values[] = $_type;
}
$condition .= " AND ( ".join(' OR ', $type_condition )." ) ";
}
$query = $this->db->runQuery("
SELECT
`related_item_type`,
`related_item_id`,
`ordering`
FROM `". $this->tb_relation ."`
WHERE `main_item_type` = ? AND `main_item_id` = ? ". $condition ."
ORDER BY `ordering` DESC, `id` DESC
LIMIT 5000
", $bind_types, $bind_values );
$result = array();
foreach ( $this->db->fetchAll($query) as $info ) {
$result[$info['related_item_type']][$info['related_item_id']] = [
"item_id" => $info['related_item_id'],
"ordering" => $info['ordering'],
];
}
//check if we get only single type, then return all the results
if(sizeof($related_item_types) == 1) {
$related_item_type = $related_item_types[0];
return isset($result[$related_item_type]) ? $result[$related_item_type] : array();
}
return $result;
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace Hura8\System\Model;
use Hura8\Database\iConnectDB;
use Hura8\System\Security\DataClean;
use Hura8\System\Security\DataType;
class SettingModel
{
protected $db;
protected $tb_setting = 'tb_settings';
const KEY = [
'pcbuilder_config' => 'pcbuilder_config',
];
public function __construct(){
$this->db = get_db('', ENABLE_DB_DEBUG);
}
public function getAll(){
$query = $this->db->runQuery(
"SELECT * FROM `".$this->tb_setting."` WHERE 1 ORDER BY `id` DESC LIMIT 100 "
);
$result = [];
foreach ( $this->db->fetchAll($query) as $rs ){
if($rs['setting_value']) {
$rs['setting_value'] = \unserialize($rs['setting_value']);
}
$result[] = $rs;
}
return $result;
}
// get a list of keys
public function getList(array $keys){
$conditions = [];
$bind_types = [];
$bind_values = [];
foreach ($keys as $key) {
$conditions[] = " `setting_key`= ? ";
$bind_types[] = 's';
$bind_values[] = $key;
}
if(sizeof($conditions)) {
$query = $this->db->runQuery(
" SELECT `setting_key`, `setting_value` FROM `".$this->tb_setting."` WHERE ".join(" OR ", $conditions)." LIMIT 100 ",
$bind_types, $bind_values
);
$result = [];
foreach ( $this->db->fetchAll($query) as $rs ){
$result[$rs['setting_key']] = \unserialize($rs['setting_value']);
}
return $result;
}
return [];
}
// get a single key, = false if not found and no default is set
public function get($key, $default = null){
$key = $this->cleanKey($key);
if(!$key) return null;
$query = $this->db->runQuery(
"SELECT `setting_value` FROM `".$this->tb_setting."` WHERE `setting_key`= ? LIMIT 1 ",
['s'], [ $key ]
);
if($rs = $this->db->fetchAssoc($query)){
return \unserialize($rs['setting_value']);
}
return ($default) ?: null;
}
public function delete($key){
return $this->db->runQuery(
"DELETE FROM `" . $this->tb_setting . "` WHERE `setting_key` = ? LIMIT 1 ",
['s'], [ $key ]
);
}
// update or create
public function updateOrCreate($key, $value = null, $comment = ''){
$key = $this->cleanKey($key);
if(!$key) return false;
if($this->checkKeyExist($key)) {
return $this->db->update(
$this->tb_setting ,
[
'setting_value' => ($value) ? \serialize($value) : null,
'comment' => subString($comment, 80),
'last_update' => CURRENT_TIME,
'last_update_by' => ADMIN_NAME,
],
[
'setting_key' => $key,
]
);
}
return $this->db->insert(
$this->tb_setting ,
[
'setting_key' => $key,
'setting_value' => ($value) ? \serialize($value) : null,
'comment' => subString($comment, 80),
'create_time' => CURRENT_TIME,
'create_by' => ADMIN_NAME,
'last_update' => CURRENT_TIME,
'last_update_by' => ADMIN_NAME,
]
);
}
public function populateKeys(array $key_list) {
$build_inserts = [];
foreach ($key_list as $key) {
if( ! $this->checkKeyExist($key)) {
$build_inserts[] = [
"setting_key" => $key,
"create_time" => CURRENT_TIME,
"create_by" => 'system',
];
}
}
if(sizeof($build_inserts)) {
$this->db->insert($this->tb_setting, $build_inserts);
}
return sizeof($build_inserts);
}
protected function checkKeyExist($key) : bool {
$key = $this->cleanKey($key);
if(!$key) return false;
$query = $this->db->runQuery(
" SELECT `id` FROM `".$this->tb_setting."` WHERE `setting_key`= ? LIMIT 1 ", ['s'], [ $key ]
);
return (bool) $this->db->fetchAssoc($query);
}
// only allow some characters
protected function cleanKey($key) {
return DataClean::makeInputSafe($key, DataType::ID);
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace Hura8\System\Model;
use Hura8\Interfaces\AppResponse;
use Hura8\Interfaces\TableName;
class UrlModel extends aEntityBaseModel
{
private $tb_url = TableName::URL; ///
private $tb_url_meta = TableName::URL_META; ///
public function __construct() {
parent::__construct("url");
}
public function getUrlByIdPath($id_path) {
$query = $this->db->runQuery(
"SELECT * FROM `".$this->tb_url."` WHERE `id_path` = ? LIMIT 1 ",
['s'], [ $id_path ]
);
return $this->db->fetchAssoc($query);
}
public function deleteByType(string $url_type) {
$this->db->runQuery(
"DELETE FROM `".$this->tb_url."` WHERE `url_type` = ? ",
['s'], [ $url_type ]
);
}
public function deleteByRequestPath($request_path) {
$this->db->runQuery(
"DELETE FROM `".$this->tb_url."` WHERE `request_path_index` = ? LIMIT 1 ",
['s'], [ md5($request_path) ]
);
}
public function getUrlByRequestPath($request_path) {
$query = $this->db->runQuery(
"SELECT * FROM `".$this->tb_url."` WHERE `request_path_index` = ? LIMIT 1 ",
['s'], [ md5($request_path) ]
);
return $this->db->fetchAssoc($query);
}
//02-12-2015
public function getUrlMetaInfo($request_path){
return $this->db->getItemInfo($this->tb_url_meta, md5($request_path), 'request_path_index');
}
public function getUrlMetaInfoById($id){
return $this->db->getItemInfo($this->tb_url_meta, $id, 'id');
}
public function getEmptyUrlMetaInfo() {
return array(
'id' => 0,
'request_path' => '',
'h1' => '',
'meta_title' => '',
'meta_keyword' => '',
'meta_description' => '',
'og_image' => '',
'summary' => '',
'description' => '',
);
}
//02-12-2015
public function createUrlMeta(array $info){
//prevent duplication
if($this->getUrlMetaInfo($info['request_path'])) return false;
if(!defined("ADMIN_NAME")) define("ADMIN_NAME", "system");
$copy = $info;
$copy['request_path_index'] = md5($info['request_path']);
$copy['last_update'] = CURRENT_TIME;
$copy['last_update_by'] = ADMIN_NAME;
return $this->db->insert(
$this->tb_url_meta ,
$copy
);
}
public function updateUrlMeta($id, array $info){
$copy = $info;
$copy['last_update'] = CURRENT_TIME;
$copy['last_update_by'] = ADMIN_NAME;
return $this->db->update(
$this->tb_url_meta ,
$copy,
[
'id' => $id,
]
);
}
protected function _buildQueryConditionExtend(array $condition): ?array
{
/*$condition = array(
"letter" => "",
);*/
$catCondition = [];
$bind_types = [];
$bind_values = [];
//loc theo is_redirect
if(isset($condition["request_path"]) && $condition["request_path"]) {
$path = preg_replace("/[^a-z0-9_\.\/\-]/i", '', $condition["request_path"]);
if($path) $catCondition[] = " AND `request_path` LIKE '%".$path."%' ";
}
//loc theo is_redirect
if(isset($condition["is_redirect"]) && $condition["is_redirect"]) {
if($condition["is_redirect"] == 1) $catCondition[] = " AND `url_type` = 'redirect' ";
else if($condition["is_redirect"] == -1) $catCondition[] = " AND `url_type` != 'redirect' ";
}
return array( join(" ", $catCondition), $bind_types, $bind_values);
}
protected function beforeCreateItem(array $input_info): AppResponse
{
$request_path = $input_info['request_path'] ?? '';
$id_path = $input_info['id_path'] ?? '';
if(!$request_path || $this->getUrlByRequestPath($request_path)) {
return new AppResponse('error', "request_path exist");
}
if($id_path && $this->getUrlByIdPath($id_path)) {
return new AppResponse('error', "id path exist");
}
$admin_name = (defined("ADMIN_NAME")) ? ADMIN_NAME : "system";
$info = $input_info;
$info['request_path_index'] = md5($info['request_path']);
$info['create_time'] = CURRENT_TIME;
$info['create_by'] = $admin_name;
$info['last_update'] = CURRENT_TIME;
$info['last_update_by'] = $admin_name;
return new AppResponse('ok', null, $info);
}
protected function afterCreateItem($new_item_id, $new_item_info)
{
// TODO: Implement afterCreateItem() method.
}
protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info): AppResponse
{
$info = $new_input_info;
unset($info['id_path']);
if(isset($info['request_path']) && $info['request_path']) {
$info['request_path_index'] = md5($info['request_path']);
}
$info['last_update'] = CURRENT_TIME;
$info['last_update_by'] = ADMIN_NAME;
return new AppResponse('ok', null, $info);
}
protected function afterUpdateItem($item_id, $old_item_info, $new_item_info)
{
// TODO: Implement afterUpdateItem() method.
}
protected function beforeDeleteItem($item_id, $item_info): AppResponse
{
return new AppResponse('ok');
}
protected function afterDeleteItem($item_id, $item_info)
{
// TODO: Implement afterDeleteItem() method.
}
protected function extendedFilterOptions(): array
{
return [];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Hura8\System\Model;
use Hura8\Database\iConnectDB;
use Hura8\Interfaces\AppResponse;
/**
* @date 21-Dec-2023
* @description various utility for management and setup
*/
class UtilityModel
{
protected $db;
protected $tb_entity_lang_template = "tb_entity_lang_template";
public function __construct($db_id = '') {
$this->db = get_db($db_id, ENABLE_DB_DEBUG);
}
public function createTableLang($tb_entity_lang): AppResponse {
$result = $this->db->checkTableExist($tb_entity_lang);
// check if table exist
if(!$result) {
$this->db->runQuery("CREATE TABLE `".$tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` ");
// recheck
$result = $this->db->checkTableExist($tb_entity_lang);
}
return new AppResponse($result ? 'ok': 'error');
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Hura8\System\Model;
use Hura8\Database\iConnectDB;
use Hura8\Interfaces\TableName;
class WebUserModel
{
protected $user_browser_id = '';
protected $user_db_id = 0;
private $tb_user = TableName::WEB_USER; //"web_user_info";
protected $db;
public function __construct($user_browser_id)
{
$this->db = get_db('', ENABLE_DB_DEBUG);
$this->user_browser_id = preg_replace("/[^a-z0-9]/i", "", $user_browser_id);
$this->user_db_id = (defined("USER_ID")) ? USER_ID : 0;
}
public function getValue($key) {
$key = $this->cleanKey($key);
$query = $this->db->runQuery("SELECT `content` FROM `".$this->tb_user."`
WHERE `user_id` = ? AND `key` = ?
LIMIT 1 ", ['s', 's'], [ $this->user_browser_id , $key ]) ;
if($rs = $this->db->fetchAssoc($query )){
return \unserialize($rs['content']);
}
return null;
}
public function setValue($key, $value) {
$key = $this->cleanKey($key);
if($row_id = $this->checkKey($key)) {
return $this->db->update(
$this->tb_user ,
[
'content' => \serialize($value) ,
'user_db_id' => $this->user_db_id ,
'last_update' => CURRENT_TIME ,
],
[
'id' => $row_id,
]
);
}
return $this->db->insert(
$this->tb_user ,
[
'user_id' => $this->user_browser_id ,
'user_db_id' => $this->user_db_id,
'key' => $key,
'content' => \serialize($value),
'create_time' => CURRENT_TIME ,
'last_update' => CURRENT_TIME,
]
);
}
//delete all user history
public function deleteUser($key) {
$this->db->runQuery(
"DELETE FROM `".$this->tb_user."` WHERE `user_id` = ? LIMIT 1 ",
['s'],
[ $this->user_browser_id ]
) ;
}
protected function cleanKey($key) {
return preg_replace("/[^a-z0-9]/i", "", $key);
}
protected function checkKey($key) {
$query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_user."`
WHERE `user_id` = ? AND `key` = ?
LIMIT 1", ['s', 's'], [ $this->user_browser_id , $key ] ) ;
if($rs = $this->db->fetchAssoc($query )){
return $rs['id'];
}
return false;
}
}

View File

@@ -0,0 +1,292 @@
<?php
namespace Hura8\System\Model;
use Hura8\Interfaces\AppResponse;
use Hura8\System\Security;
use Hura8\System\Security\DataType;
abstract class aCategoryBaseModel extends aEntityBaseModel
{
public function __construct($entity_type, $tb_name = "") {
parent::__construct($entity_type, $tb_name);
}
protected function extendedFilterOptions() : array
{
return [
// empty for now
];
}
// get all categories and group by parents
public function getAllByParent(array $condition = array()) : array
{
$item_list = array(); // parentId=>array(childId)
foreach ( $this->getAll($condition) as $item ) {
$item_list[$item['parent_id']][$item['id']] = $item;
}
return $item_list;
}
// get all categories
public function getAll(array $condition = array()){
/*$condition = [
"parent_id" => 1,
"status" => 1,
];*/
$bind_types = [];
$bind_values = [];
$where_clause = '';
if(isset($condition['status'])) {
$where_clause .= " AND `status` = ? ";
$bind_types[] = 'd';
$bind_values[] = intval($condition['status']);
}
$query = $this->db->runQuery(
" SELECT * FROM `". $this->tb_entity ."` WHERE 1 ".$where_clause." ORDER BY `ordering` DESC LIMIT 5000 ",
$bind_types, $bind_values
);
return $this->db->fetchAll($query);
}
protected function beforeCreateItem(array $input_info) : AppResponse
{
$parent_id = isset($input_info['parent_id']) ? $input_info['parent_id'] : 0;
$api_key = (isset($input_info['api_key']) && $input_info['api_key']) ? $input_info['api_key'] : $input_info['title'];
$api_key = Security\DataClean::makeInputSafe($api_key, DataType::ID);
$api_key = $this->createUniqueAPIKey(0, $api_key);
if(!isset($input_info['url_index']) || !$input_info['url_index']) {
$input_info['url_index'] = $this->createUniqueUrlIndex(0, $input_info['title']);
}else{
$input_info['url_index'] = $this->createUniqueUrlIndex(0, $input_info['url_index']);
}
$info = array_merge($input_info, array(
"parent_id" => $parent_id,
"api_key" => $api_key,
"create_by" => ADMIN_NAME,
"create_time" => CURRENT_TIME,
"last_update_by" => ADMIN_NAME,
"last_update" => CURRENT_TIME,
) );
return new AppResponse('ok', null, $info);
}
protected function afterCreateItem($new_item_id, $new_item_info) {
//update path&child
$this->updatePath($new_item_id);
$this->updateChild($new_item_id);
$this->updateChild($new_item_info['parent_id']);
}
protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse
{
$info = $new_input_info;
if(isset($info['url_index'])) {
if(!$info['url_index']) {
$info['url_index'] = $this->createUniqueUrlIndex($item_id, $info['title']);
}else{
$info['url_index'] = $this->createUniqueUrlIndex($item_id, $info['url_index']);
}
}
$info['last_update'] = CURRENT_TIME;
$info['last_update_by'] = ADMIN_NAME;
return new AppResponse('ok', null, $info);
}
protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) {
//update cat-path for category
$this->updatePath($item_id);
$this->updateChild($item_id);
//update child_ids for new/old parents and parents of parent
if($new_item_info['parent_id'] != $old_item_info['parent_id']) {
$this->updateChild($new_item_info['parent_id']);
$this->updateChild($old_item_info['parent_id']);
}
}
protected function afterDeleteItem($item_id, $item_info){
$this->updateChild($item_info["parent_id"]);
}
//create an unique request-path
protected function createUniqueAPIKey($id, $api_key){
//if exist and belong other id, create a new one
$query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_entity."` WHERE `api_key` = ? LIMIT 1 ", ['s'], [$api_key]) ;
if($info = $this->db->fetchAssoc($query)){
if($info['id'] != $id) {
$new_api_key = $api_key."-1";
return $this->createUniqueAPIKey($id, $new_api_key);
}
}
return $api_key;
}
protected function updatePathAndChildAll($id, $child_ids, $parent_id, $old_parent_id) {
$this->updatePathAndChild($id);
//update for childs
$list_child_to_update = array_filter(explode(",", $child_ids));
foreach($list_child_to_update as $_id) {
if($_id != $id) $this->updatePathAndChild($_id);
}
//cap nhat lai child list cua danh muc old_parent and new parent id, and parent of these parents
$query = $this->db->runQuery(
"SELECT cat_path FROM `". $this->tb_entity ."` WHERE `id`= ? OR `id`= ? ",
['d', 'd'], [$parent_id, $old_parent_id]
);
$cat_path_all = join(":", array_map(function ($item){
return $item['cat_path'];
} , $this->db->fetchAll($query)));
$list_parent_to_update = array_unique(array_filter(explode(":", $cat_path_all)));
foreach($list_parent_to_update as $_id) {
if($_id > 0) $this->updatePathAndChild($_id);
}
}
public function updatePathAndChild($id) {
if(!$id) return false;
$new_cat_path = $this->findCatPath($id);
$new_child_list = $this->findChildList($id);
$is_parent = ( $new_child_list === $id) ? 0 : 1;
return $this->db->update(
$this->tb_entity ,
[
'cat_path' => $new_cat_path,
'child_ids' => $new_child_list,
'is_parent' => $is_parent,
],
[
'id' => $id,
]
);
}
protected function updatePath($id) {
if(!$id) return false;
$new_cat_path = $this->findCatPath($id);
return $this->db->update(
$this->tb_entity ,
[
'cat_path' => $new_cat_path,
],
[
'id' => $id,
]
);
}
//update childs for current id and its parents and its parents' parents....
protected function updateChild($id) {
if(!$id) return false;
$query = $this->db->runQuery("SELECT `cat_path` FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1 ", ['d'], [$id]) ;
if($item_info = $this->db->fetchAssoc($query)){
$cat_id_list = array_filter(explode(":", $item_info['cat_path']));
foreach ($cat_id_list as $_id) {
$new_child_list = $this->findChildList($_id);
$is_parent = ( $new_child_list === $_id) ? 0 : 1;
$this->db->update(
$this->tb_entity ,
[
'child_ids' => $new_child_list,
'is_parent' => $is_parent,
],
[
'id' => $_id,
]
);
}
}
return true;
}
protected function _buildQueryConditionExtend(array $condition) : ?array
{
$catCondition = [];
$bind_types = [];
$bind_values = [];
return array(join(" ", $catCondition), $bind_types, $bind_values);
}
//build category path 0:parent:categoryId
protected function findCatPath($categoryId){
$path = ":".$categoryId;
$query = $this->db->runQuery("SELECT `parent_id` FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1", ['d'], [$categoryId]);
if($rs = $this->db->fetchAssoc($query)){
if($rs['parent_id']) $path .= $this->findCatPath($rs['parent_id']);
}
return $path;
}
//lay toan bo cac muc la con
protected function findChildList($p_category_id){
$all_categories = $this->getAllByParent();
$list = $p_category_id;
if(isset($all_categories[$p_category_id])) {
foreach ( $all_categories[$p_category_id] as $rs ) {
$list .= ",". $this->findChildList($rs['id']);
}
}
return $list;
}
protected function getAllParent(array $condition = []) {
$cache_key = 'all-category-by-parent-'.$this->getEntityType();
return self::getCache($cache_key, function () use ($condition){
return $this->getAllByParent($condition);
});
}
}

View File

@@ -0,0 +1,428 @@
<?php
namespace Hura8\System\Model;
use Hura8\Database\iConnectDB;
use Hura8\Database\MysqlValue;
use Hura8\Interfaces\iSearch;
abstract class aEntityBaseModel
{
/* @var iConnectDB $db */
protected $db;
protected $entity_type = '';
protected $tb_entity = "";
protected $allow_richtext_fields = []; // only table's column fields in this list allowed to retain html tags, else strip all
protected $base_filter_condition_keys = [
'q' => '', // keyword search
'q_options' => [
'field_filters' => [],
'fulltext_fields' => ['keywords'],
'limit_result' => 2000
], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000)
'featured' => '', // 1|-1
'status' => '', // 1|-1
'excluded_ids' => null, // [id1, id2, ...] or null
'included_ids' => null, // [id1, id2, ...] or null
];
/* @var ?iSearch $iSearchModel */
protected $iSearchModel;
public function __construct(
$entity_type,
$tb_entity = "",
$iSearchModel = null,
$allow_richtext_fields = []
) {
$this->entity_type = $entity_type;
$this->db = get_db('', ENABLE_DB_DEBUG);
$this->iSearchModel = $iSearchModel;
$this->allow_richtext_fields = $allow_richtext_fields;
if($tb_entity) {
$this->tb_entity = $tb_entity;
}else{
//auto infer
$tb_name = str_replace("-", "_", preg_replace("/[^a-z0-9_\-]/i", "", $entity_type));
$this->tb_entity = "tb_".strtolower($tb_name);
}
}
/**
* @description to be extended by extending class
* besides the $base_filter_condition_keys which allows to find items in ::getList($condition)
* example:
[
'category' => array[],
'brand' => array[],
]
* @return array
*/
abstract protected function extendedFilterOptions() : array ;
/**
* @description utility to get all possible entity filter during module development
* @return array
*/
public function getAllowedFilterConditionKeys(): array
{
return array_merge(
$this->extendedFilterOptions(),
$this->base_filter_condition_keys
);
}
public function setRichtextFields(array $allow_richtext_fields = []) {
$this->allow_richtext_fields = $allow_richtext_fields;
}
/**
* @description
* @param array $filter_condition
* @return array|null array($string_where, $bind_types, $bind_values );
*/
protected function _buildQueryConditionExtend(array $filter_condition): ?array {
return [];
}
/**
* @param string $sort_by
* @return string
*/
protected function _buildQueryOrderBy(string $sort_by = "new") {
return " `id` DESC ";
}
/**
* @param array $item_info
* @return array
*/
protected function formatItemInList(array $item_info) : array {
return $item_info;
}
/**
* @param array $item_info
* @return array|null
*/
protected function formatItemInfo(array $item_info) : ?array {
return $item_info;
}
public function getEntityType() : string {
return $this->entity_type;
}
/**
* @description prevent xss attack by stripping all html tags from un-permitted fields (which mostly are)
* - only fields in ::allow_richtext_fields can keep html tags. Example: description
* - if value is array (example data=['title' => '', 'description' => html tags ]) ...
then to keep `description`, both `data` and `description` must be in the ::allow_richtext_fields
because this method checks on array value recursively
* @param array $input_info
* @return array
*/
protected function cleanRichtextFields(array $input_info) : array {
$cleaned_info = [];
foreach ($input_info as $key => $value) {
if($value instanceof MysqlValue) {
$cleaned_info[$key] = $value;
} else {
if(in_array($key, $this->allow_richtext_fields)) {
$cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : $value;
}else{
$cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : strip_tags($value);
}
}
}
return $cleaned_info;
}
public function getListByIds(array $list_id, array $condition = array()) : array
{
if(!sizeof($list_id)) {
return [];
}
list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_id, 'int');
$where_clause = "";
$bind_values = $list_id;
if(isset($condition['status'])) {
$where_clause .= " AND `status` = ? ";
$bind_types[] = 'd';
$bind_values[] = intval($condition['status']);
}
$query = $this->db->runQuery(
" SELECT * FROM ".$this->tb_entity." WHERE `id` IN (".$parameterized_ids.") ".$where_clause,
$bind_types,
$bind_values
);
$item_list = [];
foreach ($this->db->fetchAll($query) as $item) {
$item_list[$item['id']] = $this->formatItemInList($item);
}
return $item_list;
}
public function getInfo($id) : ?array
{
$query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1 ", ['d'], [$id]) ;
if( $item_info = $this->db->fetchAssoc($query)){
return $this->formatItemInfo($item_info);
}
return null;
}
/**
* @description utility to inspect the actual filters which will be used in getList by Model
* @param array $condition
* @return array
*/
public function getQueryCondition(array $condition) : array
{
return $this->_buildQueryCondition($condition);
}
public function getTotal(array $condition) : int
{
list( $where_clause, $bind_types, $bind_values) = $this->_buildQueryCondition($condition);
$query = $this->db->runQuery(
" SELECT COUNT(*) as total FROM `".$this->tb_entity."` WHERE 1 " . $where_clause,
$bind_types, $bind_values
);
$total = 0;
if ($rs = $this->db->fetchAssoc($query)) {
$total = $rs['total'];
}
return $total;
}
public function getList(array $condition) : array
{
list( $where_clause, $bind_types, $bind_values) = $this->_buildQueryCondition($condition);
//debug_var([$where_clause, $bind_types, $bind_values]);
$numPerPage = (isset($condition['numPerPage']) && $condition['numPerPage'] > 0 ) ? intval($condition['numPerPage']) : 20 ;
$page = (isset($condition['page']) && $condition['page'] > 0 ) ? intval($condition['page']) : 1 ;
$sort_by = (isset($condition['sort_by']) && $condition['sort_by']) ? $condition['sort_by'] : 'new' ;
$order_by = $this->_buildQueryOrderBy($sort_by);
$query = $this->db->runQuery(
"SELECT * FROM ".$this->tb_entity." WHERE 1 ". $where_clause . "
ORDER BY ".$order_by."
LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage ,
$bind_types,
$bind_values
) ;
$item_list = array();
$counter = ($page-1) * $numPerPage;
foreach ( $this->db->fetchAll($query) as $item_info ) {
$counter += 1;
$item = $item_info;
$item['counter'] = $counter;
$item_list[] = $this->formatItemInList($item);
}
return $item_list;
}
//to avoid duplicate codes, extended class should implement this method for extra filter keys
// which are not in the base filter ::_buildQueryConditionBase
protected function _buildQueryCondition(array $filter_condition)
{
// these keys are for pagination and ordering list, which are commonly included in the $filter_condition
$excluded_keys = [
'numPerPage', 'page', 'sort_by'
];
$base_filter_conditions = [];
$extend_filter_conditions = [];
foreach ($filter_condition as $key => $value) {
if(in_array($key, $excluded_keys)) {
continue;
}
if(array_key_exists($key, $this->base_filter_condition_keys) ) {
$base_filter_conditions[$key] = $value;
}else{
$extend_filter_conditions[$key] = $value;
}
}
list($base_where, $base_bind_types, $base_bind_values) = $this->_buildQueryConditionBase($base_filter_conditions);
list(
$extend_where,
$extend_bind_types,
$extend_bind_values
) = $this->_buildQueryConditionExtend($extend_filter_conditions) ?: [ '', null, null];
if($extend_where) {
return array(
join(" ", [$base_where, $extend_where]),
array_merge($base_bind_types, $extend_bind_types),
array_merge($base_bind_values, $extend_bind_values),
);
}
return array(
$base_where,
$base_bind_types,
$base_bind_values
);
}
protected function _buildQueryConditionBase(array $filter_conditions)
{
/*
$filter_conditions = [
'q' => '', // keyword search
'q_options' => [
'field_filters' => [],
'fulltext_fields' => ['keywords'],
'limit_result' => 2000
], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000)
'featured' => '', // 1|-1
'status' => '', // 1|-1
'excluded_ids' => [], // [id1, id2, ...] or null
'included_ids' => [], // [id1, id2, ...] or null
];
*/
$catCondition = [];
$bind_types = [];
$bind_values = [];
//Tim kiem theo tu khoa
if(isset($filter_conditions["q"]) && $filter_conditions["q"] && $this->iSearchModel ){
$q_options = $filter_conditions["q_options"] ?? [];
$field_filters = $q_options['field_filters'] ?? [];
$fulltext_fields = $q_options['fulltext_fields'] ?? [];
$limit_result = $q_options['limit_result'] ?? 2000;
$match_result = $this->iSearchModel->find($filter_conditions["q"], $field_filters, $fulltext_fields, $limit_result);
list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($match_result, 'int');
if($parameterized_ids) {
$catCondition[] = " AND `id` IN (".$parameterized_ids.") ";
$bind_types = array_merge($bind_types, $sub_bind_types);
$bind_values = array_merge($bind_values, $match_result);
}else{
$catCondition[] = " AND `id` = -1 ";
}
}
//loc theo featured
if(isset($filter_conditions["featured"]) && $filter_conditions["featured"]) {
$catCondition[] = " AND `is_featured` = ? ";
$bind_types[] = 'd';
$bind_values[] = ($filter_conditions["featured"] == 1) ? 1 : 0;
}
//loc theo status
if(isset($filter_conditions["status"]) && $filter_conditions["status"]) {
$catCondition[] = " AND `status` = ? ";
$bind_types[] = 'd';
$bind_values[] = ($filter_conditions["status"] == 1) ? 1 : 0;
}
// excluded_ids
if(isset($filter_conditions['excluded_ids']) && is_array($filter_conditions['excluded_ids']) ) {
list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($filter_conditions['excluded_ids'], 'int');
if($parameterized_ids) {
$catCondition[] = " AND `id` NOT IN (".$parameterized_ids.") ";
$bind_types = array_merge($bind_types, $sub_bind_types);
$bind_values = array_merge($bind_values, $filter_conditions['excluded_ids']);
}
}
// included_ids
if(isset($filter_conditions['included_ids']) && is_array($filter_conditions['included_ids']) ) {
list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($filter_conditions['included_ids'], 'int');
if($parameterized_ids) {
$catCondition[] = " AND `id` IN (".$parameterized_ids.") ";
$bind_types = array_merge($bind_types, $sub_bind_types);
$bind_values = array_merge($bind_values, $filter_conditions['included_ids']);
}else{
$catCondition[] = " AND `id` = -1 ";
}
}
return array(
join(" ", $catCondition),
$bind_types,
$bind_values
);
}
// get empty/default item for form
public function getEmptyInfo($addition_field_value = []) : array
{
$table_info = $this->db->getTableInfo($this->tb_entity);
$empty_info = [];
foreach ($table_info as $field => $field_info) {
$empty_info[$field] = ( in_array($field_info['DATA_TYPE'], ['int', 'float', 'mediumint', 'smallint', 'tinyint']) ) ? 0 : '' ;
}
if(sizeof($addition_field_value)) {
return array_merge($empty_info, $addition_field_value);
}
return $empty_info;
}
}

View File

@@ -0,0 +1,530 @@
<?php
namespace Hura8\System\Model;
use Hura8\Interfaces\AppResponse;
use Hura8\System\Language;
use Hura8\Database\iConnectDB;
use Hura8\Interfaces\iSearch;
use Hura8\Traits\ClassCacheTrait;
abstract class aSearchBaseModel implements iSearch
{
use ClassCacheTrait;
/* @var iConnectDB $db */
protected $db;
protected $tb_main = "";
protected $tb_fulltext = "";
// if false then search 'nhà tôi' = 'nha toi', if true then 'nhà tôi' != 'nha toi'
// require rebuilding search if change this value
protected $search_vietnamese = false;
// require rebuilding search if change this value
protected $star_search = true; //values: 1|0, enable star search on fulltext: abc* -> matches: abcd, abc, abcde
// require rebuilding search if change this value
protected $min_star_search_length = 2; //min star word length: result: abcd -> keywords: ab, abc, abcd NOT a
// define list of fields to be the filters
protected $config_filter_fields = [
//format: field_name => map table_name.field
//'price' => "tb_product.price",
//'quantity' => "tb_product.quantity",
];
protected $config_fulltext_fields = [
//format: field_name => map [table_name.field]
//"product_keywords" => ["tb_product.title", "tb_product.model", "tb_product.sku"],
//"category_keywords" => ["tb_category.title"],
];
public function __construct(
$tb_main,
array $config_fulltext_fields ,
array $config_filter_fields = []
) {
// ovewrite default
if(defined('CONFIG_STAR_SEARCH')) $this->star_search = CONFIG_STAR_SEARCH;
if(defined('CONFIG_STAR_SEARCH_MIN_LENGTH')) $this->min_star_search_length = CONFIG_STAR_SEARCH_MIN_LENGTH;
if(defined('CONFIG_SEARCH_VIETNAMESE')) $this->search_vietnamese = CONFIG_SEARCH_VIETNAMESE;
$this->db = get_db('', ENABLE_DB_DEBUG);
$this->config_fulltext_fields = $config_fulltext_fields;
$this->config_filter_fields = $config_filter_fields;
$this->tb_main = $tb_main;
$part_name = str_replace("tb_", "", preg_replace("/[^a-z0-9_]/i", "", $tb_main));
$this->tb_fulltext = "tb_search_".strtolower($part_name);
}
/**
* @description get filter fields
* @param array[string]
*/
public function getFilterFields(): array {
return $this->config_filter_fields;
}
/**
* @description get fulltext fields
* @param array[string]
*/
public function getFulltextFields(): array {
return $this->config_fulltext_fields;
}
public function getSampleData() : array {
$query = $this->db->runQuery(
"SELECT * FROM `".$this->tb_fulltext."` ORDER BY `item_id` DESC LIMIT 10"
);
return $this->db->fetchAll($query);
}
public function getTableName() : string {
return $this->tb_fulltext;
}
/**
* @param array $field_filters ["price" => [">", 100], "brand" => ["=", 1]]
* @return array
*/
protected function _buildFieldFilters(array $field_filters = []) {
$permit_operations = [">","=","<",">=","<=", "BETWEEN", "IN"];
$where_clause = [];
$bind_types = [];
$bind_values = [];
foreach ($field_filters as $field => $info) {
list($operation, $value) = $info;
$operation = strtoupper($operation);
if(!in_array(strtolower($operation), $permit_operations)) continue;
if($operation == 'BETWEEN') {
// $value must be array(value_int_1, value_int_2)
if(is_array($value) && sizeof($value) == 2) {
list($value_1, $value_2) = $value;
if(is_int($value_1) && is_int($value_2) && $value_1 < $value_2) {
$where_clause[] = " AND `".$field."` BETWEEN (?, ?) ";
$bind_types[] = "d";
$bind_types[] = "d";
$bind_values[] = $value_1;
$bind_values[] = $value_2;
}
}
continue;
}
if($operation == 'IN') {
// $value must be array(value1, value2)
if(is_array($value) && sizeof($value) > 0) {
$parameterized = [];
foreach ($value as $_v) {
$parameterized[] = "?";
$bind_types[] = "s";
$bind_values[] = $_v;
}
$where_clause[] = " AND `".$field."` IN (".join(",", $parameterized).") ";
}
continue;
}
// default operation, comparison requires value to be a digit
$where_clause[] = " AND `".$field."` ".$operation." ? ";
$bind_types[] = "d";
$bind_values[] = $value;
}
return array(join(" ", $where_clause), $bind_types, $bind_values);
}
/**
* @param $keyword
* @param array $field_filters ["price" => [">", 100], "brand" => ["=", 1]]
* @param array $fulltext_fields ['title_keywords', "category_keywords"]
* @param int $limit_result
* @return int[]
*/
public function find($keyword, array $field_filters = [], array $fulltext_fields = [], $limit_result = 2000) : array
{
if(!sizeof($fulltext_fields)) $fulltext_fields = array_keys($this->config_fulltext_fields);
$cache_key = md5("find-".$keyword);
return static::getCache($cache_key, function () use ($keyword, $field_filters, $fulltext_fields, $limit_result){
$keyword_clean = ($this->search_vietnamese) ? $this->make_text_search_vn($keyword) : $this->make_text_clean($keyword) ; //make_text_search_vn
$keyword_clean_ele = array_filter(explode(" ", $keyword_clean));
$build_boolean_search = "+".join(" +", $keyword_clean_ele);
if(!$limit_result || $limit_result > 5000) $limit_result = 5000;
$limit_condition = ($limit_result > 0) ? " LIMIT ".$limit_result : "";
list($where_clause, $bind_types, $bind_values) = $this->_buildFieldFilters($field_filters);
//validate search fields
$validated_fulltext_fields = array_filter($fulltext_fields, function ($item) { return array_key_exists($item, $this->config_fulltext_fields); });
if(!sizeof($validated_fulltext_fields)) {
return [];
}
$query = $this->db->runQuery(
"SELECT `item_id` FROM `".$this->tb_fulltext."`
WHERE MATCH(".join(',', $validated_fulltext_fields).") AGAINST('".$build_boolean_search."' IN BOOLEAN MODE)
".$where_clause."
ORDER BY `item_id` DESC
".$limit_condition."
",
$bind_types, $bind_values
);
return array_map(function ($item){ return $item['item_id']; }, $this->db->fetchAll($query) );
});
}
protected function getItemInfo($item_id)
{
$query = $this->db->runQuery(
"SELECT * FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1",
['d'], [$item_id]
);
return $this->db->fetchAssoc($query);
}
protected function checkExist($item_id) : bool
{
$query = $this->db->runQuery(
"SELECT `item_id` FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1",
['d'], [$item_id]
);
return (bool) $this->db->fetchAssoc($query);
}
// get all table_fields to watch which will affect the search
protected function getWatchTableFields() {
$result = [];
foreach ($this->config_filter_fields as $field => $table_field ) {
$result[] = $table_field;
}
foreach ($this->config_fulltext_fields as $field => $table_fields ) {
$result = array_merge($result, $table_fields);
}
return $result;
}
/**
* @description update or create item if not exist
* @param $item_id
* @param array $table_field_values
$table_field_values = [
"tb_product.price" => 2000000,
"tb_product.title" => "Máy tính ABC",
"tb_category.price" => "Máy tính",
"tb_product.field_not_be_update" => "Mmodel",
];
* @return AppResponse
*/
public function updateItem($item_id, array $table_field_values = []) : AppResponse
{
$fulltext_fields = [];
foreach ($this->config_fulltext_fields as $fulltext_field => $fulltext_map_fields ) {
foreach ($fulltext_map_fields as $_table_field) {
if(array_key_exists($_table_field, $table_field_values)) {
$fulltext_fields[$fulltext_field][$_table_field] = $table_field_values[$_table_field];
}
}
}
//echo "fulltext_fields";
//debug_var($fulltext_fields);
//echo "<br />";
$field_filters = [];
foreach ($this->config_filter_fields as $filter_field => $filter_map_field ) {
if(array_key_exists($filter_map_field, $table_field_values)) {
$field_filters[$filter_field] = $table_field_values[$filter_map_field];
}
}
//echo "field_filters";
//debug_var($field_filters);
// nothing to update
if(!sizeof($fulltext_fields) && !sizeof($field_filters)) {
return new AppResponse('error', "nothing to update" );
}
// create entry if not exist
if(!$this->checkExist($item_id)) {
$this->db->insert($this->tb_fulltext, ['item_id' => $item_id, 'create_time' => CURRENT_TIME]);
}
$current_info = $this->getItemInfo($item_id);
if(!$current_info) {
return new AppResponse('error', "Cannot find record for ".$item_id );
}
// update
$updated_fulltext = [];
foreach ($this->config_fulltext_fields as $_filter_field => $_n ) {
if(!isset($fulltext_fields[$_filter_field])) continue;
$current_filter_field_base = ($current_info[$_filter_field."_base"]) ? \json_decode($current_info[$_filter_field."_base"], true) : [];
$new_filter_field_base = $current_filter_field_base;
foreach ($fulltext_fields[$_filter_field] as $_table_field => $_new_value) {
$new_filter_field_base[$_table_field] = $_new_value;
}
if(json_encode($new_filter_field_base) != json_encode($current_filter_field_base)) {
$updated_fulltext[$_filter_field."_base"] = $new_filter_field_base;
$updated_fulltext[$_filter_field] = ($this->search_vietnamese) ? $this->make_text_search_vn(join(" ", array_values($new_filter_field_base))) : $this->make_text_clean(join(" ", array_values($new_filter_field_base)));
}
}
$updated_filters = [];
foreach ($this->config_filter_fields as $_filter_field => $_table_field ) {
if(!isset($field_filters[$_filter_field])) continue;
if($field_filters[$_filter_field] != $current_info[$_filter_field] ) {
$updated_filters[$_filter_field] = $field_filters[$_filter_field];
}
}
// nothing to update
if(!sizeof($updated_filters) && !sizeof($updated_fulltext)) {
return new AppResponse('error', "nothing to update" );
}
$updated_info = array_merge($updated_filters, $updated_fulltext);
$updated_info['last_update'] = CURRENT_TIME;
//echo "updated_info = <br /> ";
//debug_var($updated_info);
$this->db->update(
$this->tb_fulltext,
$updated_info,
['item_id' => $item_id]
);
return new AppResponse('ok');
}
public function deleteItem($item_id) : AppResponse
{
$this->db->runQuery(
"DELETE FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1",
['d'], [$item_id]
);
return new AppResponse('ok');
}
public function deleteItems(array $item_list_ids) : AppResponse
{
list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($item_list_ids, "int");
$this->db->runQuery(
"DELETE FROM `".$this->tb_fulltext."` WHERE `item_id` IN (".$parameterized_ids.") ",
$bind_types,
$item_list_ids
);
return new AppResponse('ok');
}
public function recreateTableSearch() {
if($this->db->checkTableExist($this->tb_fulltext)) {
$this->db->runQuery(
"DROP TABLE `".$this->tb_fulltext."` "
);
}
$this->createTableSearch();
}
public function createTableSearch() {
// check if table exist
if($this->db->checkTableExist($this->tb_fulltext)) {
return true;
}
if(!$this->db->checkTableExist($this->tb_main)) {
return false;
}
$sql = "CREATE TABLE `".$this->tb_fulltext."` ( `item_id` INT(11) UNSIGNED NOT NULL DEFAULT '0', ";
// add fields
foreach ($this->config_filter_fields as $_filter_field => $_map_table_field ) {
list($_table, $_f) = explode(".", $_map_table_field);
$_table_info = $this->db->getTableInfo($_table);
$column_default = $_table_info[$_f]['COLUMN_DEFAULT'];
$default_value = (in_array($_table_info[$_f]['DATA_TYPE'], ['mediumint', 'int', 'tinyint', 'double', 'float'])) ? 0 : $column_default;
$sql .= " `".$_filter_field."` ".$_table_info[$_f]['COLUMN_TYPE']." NOT NULL DEFAULT '".$default_value."', ";
}
foreach ($this->config_fulltext_fields as $_filter_field => $_t ) {
$sql .= " `".$_filter_field."` TEXT NULL , ";
// create field-data to compare new values before re-indexing
$sql .= " `".$_filter_field."_base` TEXT NULL , ";
}
$sql .= " `create_time` int(11) NOT NULL DEFAULT '0', ";
$sql .= " `last_update` int(11) NOT NULL DEFAULT '0', ";
// index
foreach ($this->config_filter_fields as $_filter_field => $_data_type ) {
$sql .= " INDEX (`".$_filter_field."`), ";
}
// fulltext on separate columns
foreach ($this->config_fulltext_fields as $_filter_field => $_t ) {
$sql .= " FULLTEXT INDEX (`".$_filter_field."` ) , ";
}
// create full text for all columns combined, so we can do query match(col_1, col_2) against keyword
$_filter_fields = array_keys($this->config_fulltext_fields);
if(sizeof($_filter_fields)) $sql .= " FULLTEXT INDEX (".join(", ", $_filter_fields).") , ";
$sql .= " PRIMARY KEY ( `item_id` ) ";
$sql .= " ) ENGINE=InnoDB DEFAULT CHARSET=utf8";
//return $sql;
$this->db->runQuery($sql);
// add foreign key constraint
$this->db->runQuery("
ALTER TABLE `".$this->tb_fulltext."` ADD FOREIGN KEY (`item_id`) REFERENCES `".$this->tb_main."`(`id`) ON DELETE CASCADE;
");
return true;
}
//11-11-2012 chuyen text thanh tieng viet voi dau chuyen doi: vd. nhà = nhas
//because mysql allow fulltext search with minimum 4 characters, we have a trick: add hura to everyone what has length < 4
protected function make_text_search_vn($text, $enable_star = false){
$text = Language::convertText($text);
$text = preg_replace("@[^a-z0-9\s]@si", " ", strtolower($text));
//$text = str_replace(" "," ",$text);
$text_ele = array_filter(explode(" ", $text));
$new_text = "";
foreach($text_ele as $ele) {
if($ele == '/' ) continue; //skip this character if it stands alone
if(strlen($ele) < 4 || $ele == 'plus') $new_text .= " hura".$ele;
else $new_text .= " ".$ele;
}
return trim($new_text);
}
//11-11-2012 chuyen text thanh tieng viet khong dau: vd. nhà = nha
/*
@variable:
- $text: text to be index
//16-11-2012
- $enable_star:
true: allow search abcd by word: ab or abc
false: not enabled
- $min_len: if $enable_star = true, abcd -> tao thanh: ab, abd, abcd (if $min_len = 2)
*/
protected function make_text_clean($_text, $enable_star = false){
//$text = Language::chuyenKhongdau(Language::convert_lower($text));
$text = Language::chuyenKhongdau($_text);
$text = preg_replace("@[^a-z0-9\s]@si", " ", strtolower($text));
$text_ele = array_filter(explode(" ", $text));
$new_text = "";
foreach($text_ele as $ele) {
if($this->star_search && $enable_star) {
$word_list = static::create_star_search($ele, $this->min_star_search_length);
foreach($word_list as $new_ele) {
if(strlen($new_ele) < 4 || $ele == 'plus') $new_text .= " hura".$new_ele;
else $new_text .= " ".$new_ele;
}
}else{
if(strlen($ele) < 4 || $ele == 'plus') $new_text .= " hura".$ele;
else $new_text .= " ".$ele;
}
}
return trim($new_text);
}
//16-11-2012
//create all possible combination for a word for star search: abcd -> tao thanh: ab, abd, abcd, bc, bcd, cd (if $min_len = 2)
protected function create_star_search($word, $min_len = 2){
if($min_len < 2) $min_len = 2; //in case missing value for CONFIG_STAR_SEARCH_MIN_LENGTH
$word_len = strlen($word);
$result = array();
if($word_len <= $min_len) return array($word);
for($i = $min_len; $i < $word_len; $i ++ ){
$result[] = substr($word, 0, $i);
}
$result[] = $word;
//09-01-2012
//reduce $word 1 character to create new word and create combination from there
$new_word = substr($word, 1);
$new_result = self::create_star_search($new_word, $min_len = 2);
foreach($new_result as $el){
$result[] = $el;
}
return array_unique($result);
}
}