log(__METHOD__, 'BEGIN'); $object_name = $this->get_object_name(); $field_name = $this->framework->get_extra_data_field_name(); $key = $this->get_key_name(); $this->log(__METHOD__, "object name: $object_name"); $this->log(__METHOD__, "field name: $field_name"); $this->log(__METHOD__, "key name: $key"); // cas particulier du DI if ($object_name == 'dossier_instruction') { $object_name = 'dossier'; } // si le champ n'a pas été défini dans la table de définition des champs dynamiques if (! $this->is_index_defined($object_name, $field_name, $key)) { // on le défini $this->setup_index($object_name, $field_name, $key); } $this->log(__METHOD__, 'END'); } /** * Renvoi le nom de l'index pour l'objet, le champ et la clé spécifiés, * * @param string $object_name Le nom de l'objet métier concerné * @param string $field_name Le nom du champ dynamique * @param string $key Le nom de la clé concernée * * @return string */ protected function get_index_name(string $object_name, string $field_name, string $key) { return $object_name.'_'.$field_name.'_idx_gin_on_'.$key; } /** * Renvoi 'true' si l'index est défini pour l'objet, le champ et la clé spécifiés, * 'false' sinon. * * @param string $object_name Le nom de l'objet métier concerné * @param string $field_name Le nom du champ dynamique * @param string $key Le nom de la clé concernée * * @return bool */ protected function is_index_defined(string $object_name, string $field_name, string $key) { $index_name = $this->get_index_name($object_name, $field_name, $key); $sql = sprintf(" SELECT COUNT(indexname) FROM pg_indexes WHERE schemaname = '%s' AND tablename = '%s' AND indexname = '%s'; ", $this->framework->db->escapeSimple('openads'), $this->framework->db->escapeSimple($object_name), $this->framework->db->escapeSimple($index_name)); $qres = $this->framework->get_one_result_from_db_query($sql, array('origin' => __METHOD__)); $nb_idx = $qres['result']; if ($qres['code'] !== 'OK') { // TODO log throw new RuntimeException("Failed SQL: $sql"); } return $nb_idx !== false && is_numeric($nb_idx) && intval($nb_idx) === 1; } protected function setup_index(string $object_name, string $field_name, string $key) { $index_name = $this->get_index_name($object_name, $field_name, $key); $sql = sprintf("CREATE INDEX %s ON %s%s USING GIN ((%s->'%s'->'value'))", $this->framework->db->escapeSimple($index_name), DB_PREFIXE, $this->framework->db->escapeSimple($object_name), $this->framework->db->escapeSimple($field_name), $this->framework->db->escapeSimple($key)); $qres = $this->framework->execute_db_query($sql, array('origin' => __METHOD__)); if ($qres['code'] == 'KO') { $err_msg = "Échec de la création de l'index GIN pour la table '$object_name'". " ($field_name->$key)."; $err_msg_detail = $qres['message'] ?? ''; $this->log( __METHOD__, $err_msg .' '.sprintf(__("Détail: %s."), $err_msg_detail), 'CRITICAL'); throw new RuntimeException( $err_msg .' '.sprintf(__("Détail: %s."), $err_msg_detail)); } } /** * Retire les balises HTML d'un message * * @param string $html_msg Le message avec des balises HTML * * @return string Le message sans les balises HTML */ protected function remove_html_tags(string $html_msg) { return strip_tags( str_replace('..', '.', str_replace('. . ', '. ', str_replace(array('
', '
', '
'), '. ', $html_msg)))); } /** * Renvoie un « faux » nom de champ pour rechercher dans 'extra_data' pour ce champ * * @return string */ protected function get_search_fake_field_name() { return 'xd__'.$this->get_object_name().'__'.$this->get_key_name(); } /** * Fonction principale du module, qui est appelée à chaque déclenchement d'un "hook". * Il revient à cette fonction de savoir à quel moment faire quoi, en filtrant sur le * "hook" passé en paramètre. * * @param string $hook Le nom du "hook" (préfixé par le nom de l'objet) * @param array $data Les données du contexte * * @return void */ public function main(string $hook, array &$data = array()) { $this->log(__METHOD__, "($hook) BEGIN"); $object_name = $this->get_object_name(); // objet associé à ce module $object = $this->object; // object non-vide if (! empty($object)) { $this->log(__METHOD__, "($hook) object: ".get_class($object). "#".$object->getVal($object->clePrimaire)); switch($hook) { // vérifie la valeur du champ case $object_name.'_verifier_post': if (isset($data['val'])) { $field_name = $this->framework->get_extra_data_field_name(); $this->log(__METHOD__, "($hook) 'data['val']: ".print_r($data['val'], true)); $this->log(__METHOD__, "($hook) 'object->valF: ".print_r($object->valF, true)); if (isset($data['val'][$field_name]) && ! empty($data['val'][$field_name])) { $cb = $this->get_field_validator($object); if (is_callable($cb)) { $cb_data = array( 'field_name' => $field_name, 'object_name' => $object_name, 'object' => &$object, 'hook' => $hook, 'data' => &$data); $res = call_user_func_array($cb, $cb_data); if ($res !== true) { $object->correct = false; $err_msg = $res; if (! is_string($res)) { $err_msg = sprintf(__("Le champ '%s' est invalide"), $field_name); } $object->addToMessage($err_msg); } } else { /*$this->log(__METHOD__, "($hook) 'data['val'][$field_name]' is not defined or empty"); $object->correct = false; $object->addToMessage(sprintf(__("Le champ '%s' ne peut pas être vide"), $field_name));*/ } } } else { $this->log(__METHOD__, "($hook) 'val' is not defined"); } break; // déclare le champ de fusion case $object_name.'_modify_edition_merge_fields_values_post': $field_name = $this->framework->get_extra_data_field_name(); $value = $this->cast_field_value( $this->get_field_value($field_name, $object_name, $object)); $field_alias = $this->get_field_alias(); $data['values'][$field_alias] = $value; break; // action par défaut default: break; } } // listing (objet vide) else { $this->log(__METHOD__, "($hook) object: ".var_export($object, true)); switch($hook) { // ajoute le champ au listing // et à la recherche case $object_name.'_table_init_pre': if (isset($data['table']) && ! empty($data['table'])) { $field_name = $this->framework->get_extra_data_field_name(); $key = $this->get_key_name(); // cas particulier du DI if ($object_name == 'dossier_instruction') { $object_name = 'dossier'; } // le champ est ajouté en tant que colonne du listing de l'objet if ($show_in_listing = ($this->params['show_in_listing'] ?? false)) { $new_champAffiche = "$object_name.$field_name->'$key'->>'value' as ".'"'.$key.'"'; // si le champ dynamique n'est pas déjà affiché if (! in_array($new_champAffiche, $data['table']->champAffiche)) { $data['table']->champAffiche[] = $new_champAffiche; } } // le champ est « recherchable » ou utilisé dans le listing if ($searchable = ($this->params['searchable'] ?? false) || $show_in_listing) { $new_champRecherche = "$object_name.$field_name->'$key'->>'value' as ".'"'.$key.'"'; // si le champ dynamique n'est pas déjà recherché if (! in_array($new_champRecherche, $data['table']->champRecherche)) { $data['table']->champRecherche[] = $new_champRecherche; $obj = $this->framework->get_submitted_get_value('obj'); if ($obj === 'dossier_instruction') { $obj = 'dossier'; } $object_table = $obj; $object_cle_primaire = $object_table; $object_join_equality_on = "$object_table.$object_cle_primaire::varchar"; $this->log(__METHOD__, "object_join_equality_on: $object_join_equality_on"); // recherche une option de type 'search' $search_option_index = null; $search_option = null; foreach($data['table']->options as $index => $option) { if (! isset($option['type']) || $option['type'] != 'search') continue; $search_option = $data['table']->options[$index]; $search_option_index = $index; break; } // recherche avancée if (! empty($search_option)) { $this->log(__METHOD__, "($hook) mode recherche avancée"); if (! isset($search_option['absolute_object']) || empty($search_option['absolute_object'])) { throw new RuntimeException("Mode recherche avancée et 'absolute_object' non spécifié"); } $object_table = $search_option['absolute_object']; $object_cle_primaire = $object_table; $object_join_equality_on = "$object_table.$object_cle_primaire::varchar"; $this->log(__METHOD__, "object_join_equality_on: $object_join_equality_on"); $field_desc = $this->get_field_description(); $field_type = $this->get_field_type(); $field_length = $this->get_field_length(); $sql_cast = ''; switch($this->get_value_type()) { case 'int': $sql_cast = '::varchar'; break; } $table_option_to_add = array( 'libelle' => $field_desc, 'type' => ($field_type == 'textarea' ? 'text' : $field_type), 'table' => $object_name, 'colonne' => "$field_name->'$key'->>'value'", 'taille' => 30, 'max' => ($field_length >= 50 ? 50 : $field_length), ); $option_field = $this->get_search_fake_field_name(); $data['table']->options[$search_option_index]['advanced'][$option_field] = $table_option_to_add; $this->log(__METHOD__, "added: options[$search_option_index]". "['advanced'][$option_field] = ". var_export($table_option_to_add, true)); } } } } break; } } $this->log(__METHOD__, 'END'); } /** * Renvoie la valeur du champ dynamique de l'objet. * * @param string $name Le nom du champ dynamique * @param string $object_name Le type d'objet auquel est rattaché le champ dynamique * @param om_dbform $object L'objet métier auquel est rattaché le champ dynamique * * @return mixed La valeur du champ dynamique (non "castée" vers son type) */ protected function get_field_value_from_object(string $name, string $object_name, om_dbform $object) { $field_name = $this->framework->get_extra_data_field_name(); if (! empty($xd_json = $object->getVal($field_name))) { $xd_array = json_decode($xd_json, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new RuntimeException(sprintf( __("Le champ '%s' n'est pas décodable (formattage JSON invalide)"), $field_name)); } $key = $this->get_key_name(); if (isset($key)) { return $key; } } return null; } /** * Renvoie soit la valeur postée (formulaire) du champ dynamique, soit la valeur en BDD. * * @param string $name Le nom du champ dynamique * @param string $object_name Le type d'objet auquel est rattaché le champ dynamique * @param om_dbform $object L'objet métier auquel est rattaché le champ dynamique * * @return mixed La valeur récupérée en BDD (non "castée" vers son type) */ protected function get_field_value(string $name, string $object_name, om_dbform $object) { $posted_value = $this->framework->get_submitted_post_value($name); if (! is_null($posted_value)) { return $posted_value; } return $this->get_field_value_from_object($name, $object_name, $object); } /** * Renvoie le nom de l'objet auquel le champ dynamique est rattaché. * Si ce nom est défini dans les paramètres du module, alors il a la précédence. * Ensuite si un objet métier (om_dbform) a été associé à ce module, alors celui-ci est revoyé. * Enfin, si un objet "lien" est associé à ce module, alors c'est le nom de l'objet défini * dans l'objet "lien" qui est utilisé. * * @return string */ protected function get_object_name() { // le nom renseigné dans le paramétrage a précédence $object_name = $this->params['object_name'] ?? null; $this->log(__METHOD__, "object_name[params]: $object_name"); if (! empty($object_name)) return $object_name; // puis le nom de la classe de l'objet métier rattaché à ce module if (! empty($this->object)) { $object_name = get_class($this->object); $this->log(__METHOD__, "object_name[object]: $object_name"); if (! empty($object_name)) return $object_name; } // enfin le nom de l'objet défini dans l'object "lien" du module if (! empty($this->object_link_instance)) { $object_name = $this->object_link_instance->getVal('object_name'); $this->log(__METHOD__, "object_name[object_link]: $object_name"); if (! empty($object_name)) return $object_name; } return $object_name; } /** * Renvoie le nom de la clé utilisée au sein du champ dynamique de l'objet pour stocker la valeur * * @return string */ public function get_key_name() { return $this->params['key'] ?? $this->get_short_name(); } /** * Renvoie le type de donnée du champ dynamique, tel que défini dans les paramètres ou * avec une valeur par défaut. * * @return string */ protected function get_value_type() { return $this->params['type'] ?? 'int'; } /** * Renvoie le type du champ dynamique, tel que défini dans les paramètres ou avec une * valeur par défaut. * * @return string */ protected function get_field_type() { return $this->params['field_type'] ?? 'text'; } /** * Renvoie le nom du champ de fusion du champ dynamique, tel que défini dans les paramètres * ou avec une valeur par défaut. * * @return string */ protected function get_field_alias() { return $this->params['alias_champ_fusion'] ?? 'extra_data'; } /** * Renvoie la longueur maximale du champ dynamique, telle que défini dans les paramètres * ou avec une valeur par défaut. * * @return int */ protected function get_field_length() { $len = $this->params['max_length'] ?? null; if (empty($len)) { $len = 100; $value_type = $this->get_value_type(); if ($value_type == 'int') { $len = 5; } } return intval($len); } /** * Renvoie la description du champ dynamique, telle que défini dans les paramètres ou avec * une valeur par défaut. * * @return string */ protected function get_field_description() { return $this->params['description'] ?? 'extra_data'; } /** * Renvoie une fonction de validation du champ dynamique, telle que défini dans les * paramètres ou avec une valeur par défaut. * * @param om_dbform $object L'objet métier auquel est rattaché le champ dynamique * * @return array|callable */ protected function get_field_validator(?om_dbform $object = null) { $cb = $this->params['validator_cb'] ?? null; if (is_array($cb) && count($cb) == 2) { if($cb[0] == 'module') { $cb[0] = $this; $this->log(__METHOD__, "replacing 'module' by the actual module instance"); } elseif($cb[0] == 'object') { $cb[0] = $object; $this->log(__METHOD__, "replacing 'object' by the actual object instance"); } } return $cb; } /** * Renvoie la valeur après conversion de son type (cast). * * @param mixed $value La valeur dans un type quelconque (généralement 'string') * * @return mixed La valeur après conversion de son type (cast). */ protected function cast_field_value($value) { $value_type = $this->get_value_type(); if (is_null($value)) { return null; } switch($value_type) { case 'int': $value = intval($value); break; } return $value; } /** * Valide la valeur du champ dynamique. * * Renvoie un message si la valeur n'est pas valide, sinon 'true' (bool). * * @return bool|string */ public function test_valid( string $field_name, string $object_name, om_dbform $object, string $hook, array &$data) { $this->log(__METHOD__, "($field_name, $object_name, $hook) BEGIN"); $field_data = json_decode($data['val'][$field_name], true); if (json_last_error() !== JSON_ERROR_NONE) { return sprintf( __("Le champ '%s' n'est pas décodable (formattage JSON invalide)"), $field_name); } $key = $this->get_key_name(); if (isset($field_data[$key]) && intval($field_data[$key]) > 20) { return sprintf( __("Le champ '%s->%s' ne peut avoir une valeur supérieure à 20 (exemple d'erreur)"), $field_name, $key); } return true; } }