Files
xstore/inc/Hura8/System/Model/EntityLanguageModel.php
2025-10-04 11:46:59 +07:00

457 lines
14 KiB
PHP

<?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;
}
}