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