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