/** * Compare single row * * @param array $data_row * @param array $original_row * @param array $collection * @param array $options * @param array $parent_pk * @param array $parent_row * @return array */ public final function compare_one_row($data_row, $original_row, $collection, $options, $parent_pk = null, $parent_row = []) { $result = ['success' => false, 'error' => [], 'data' => ['history' => [], 'audit' => [], 'total' => 0, 'updated' => false, 'deleted' => false, 'inserted' => false], 'new_serials' => []]; $model = $collection['model_object']; // important to reset cache $model->reset_cache(); // step 1, clenup data $data_row_final = $data_row; // we need to manualy inject parents keys if (!empty($parent_pk)) { foreach ($collection['map'] as $k => $v) { // if we are dealing with relations if (strpos($k, 'relation_id') !== false) { $data_row_final[$v] = $parent_row[$k]; } else { $data_row_final[$v] = $parent_pk[$k]; } } } $model->process_columns($data_row_final, ['ignore_not_set_fields' => true, 'skip_type_validation' => true]); // step 2 process row $delete = $update = $audit = $audit_details = $pk = []; $action = null; if (!empty($options['flag_delete_row']) || empty($data_row)) { // if we delete // if we have data if (!empty($original_row)) { $pk = extract_keys($collection['pk'], $original_row); $delete = ['table' => $model->name, 'pk' => $pk]; // audit $action = 'delete'; $audit = $original_row; } } else { if (empty($original_row)) { // if we insert // process who columns $model->process_who_columns(['inserted', 'optimistic_lock'], $data_row_final, $this->timestamp); // handle serial types foreach ($model->columns as $k => $v) { if (strpos($v['type'], 'serial') !== false && empty($v['null'])) { $temp = $this->primary_model->db_object->sequence($model->name . '_' . $k . '_seq'); $result['new_serials'][$k] = $data_row_final[$k] = $temp['rows'][0]['counter']; } } $temp = $this->primary_model->db_object->insert($model->name, [$data_row_final], null); if (!$temp['success']) { $result['error'] = $temp['error']; return $result; } $result['data']['total']++; // flag for main record if (!empty($options['flag_main_record'])) { $result['data']['inserted'] = true; } // pk $pk = extract_keys($collection['pk'], $data_row_final); // audit $action = 'insert'; $audit = $data_row_final; } else { // if we update foreach ($data_row_final as $k => $v) { // hard comparison if ($v !== $original_row[$k]) { $update[$k] = $v; } if (in_array($k, $collection['pk'])) { $pk[$k] = $v; } } // audit $action = 'update'; } } // step 3 process details if (!empty($collection['details'])) { foreach ($collection['details'] as $k => $v) { // create new object $v['model_object'] = factory::model($k, true); if ($v['type'] == '11') { $details_result = $this->compare_one_row($data_row[$k] ?? [], $original_row[$k] ?? [], $v, ['flag_delete_row' => !empty($delete)], $pk, $data_row_final); if (!empty($details_result['error'])) { $result['error'] = $details_result['error']; return $result; } else { $result['data']['total'] += $details_result['data']['total']; } // audit if (!empty($details_result['data']['audit'])) { $audit_details[$k] = $details_result['data']['audit']; } } else { if ($v['type'] == '1M') { $keys = []; if (isset($original_row[$k]) && is_array($original_row[$k])) { $keys = array_keys($original_row[$k]); } if (isset($data_row[$k]) && is_array($data_row[$k])) { $keys = array_merge($keys, array_keys($data_row[$k])); } $keys = array_unique($keys); if (!empty($keys)) { foreach ($keys as $v2) { $details_result = $this->compare_one_row($data_row[$k][$v2] ?? [], $original_row[$k][$v2] ?? [], $v, ['flag_delete_row' => !empty($delete)], $pk, $data_row_final); if (!empty($details_result['error'])) { $result['error'] = $details_result['error']; return $result; } else { $result['data']['total'] += $details_result['data']['total']; } // audit if (!empty($details_result['data']['audit'])) { $audit_details[$k][$v2] = $details_result['data']['audit']; } } } } } } } // step 4 update record if (!empty($update) || $action == 'update' && $result['data']['total'] > 0) { // process who columns $model->process_who_columns(['updated', 'optimistic_lock'], $update, $this->timestamp); if (!empty($update)) { // update record $temp = $this->primary_model->db_object->update($model->name, $update, [], ['where' => $pk]); if (!$temp['success']) { $result['error'] = $temp['error']; return $result; } $result['data']['total']++; } // flag for main record if (!empty($options['flag_main_record'])) { $result['data']['updated'] = true; } // audit $audit = $update; } // step 5 delete record after we deleted all childrens if (!empty($delete)) { $temp = $this->primary_model->db_object->delete($delete['table'], [], [], ['where' => $delete['pk']]); if (!$temp['success']) { $result['error'] = $temp['error']; return $result; } $result['data']['total']++; // flag for main record if (!empty($options['flag_main_record'])) { $result['data']['deleted'] = true; } } // step 6 history only if we updated or deleted if ($model->history && (!empty($delete) || !empty($update))) { $temp = $original_row; $model->process_who_columns(['updated'], $temp, $this->timestamp); $result['data']['history'][$model->history_name][] = $temp; } // step 7 audit if ($this->primary_model->audit && !empty($audit)) { $result['data']['audit'] = ['action' => $action, 'pk' => $pk, 'columns' => []]; foreach ($audit as $k => $v) { $old = $original_row[$k] ?? null; if ($v !== $old) { if (($model->columns[$k]['domain'] ?? '') == 'password') { $v = '*** *** ***'; } $result['data']['audit']['columns'][$k] = [$v, $old]; } } // details if (!empty($audit_details)) { $result['data']['audit']['details'] = $audit_details; } } // success if (!empty($result['data']['total'])) { $result['success'] = true; } return $result; }
/** * test extract_keys */ public function testExtractKeys() { $this->assertEquals([10, 20], extract_keys(['foo' => 20, 'bar' => 10, 'qux' => 99], ['bar', 'foo'])); $this->assertEquals([null, 20, 300], extract_keys(['foo' => 20, 'qux' => 99], ['bar', 'foo' => 0, 'jazz' => 300])); }
/** * Data default renderer * * @return string */ private final function render_data_default() { $result = ''; // if we have no rows we display a messsage if ($this->num_rows == 0) { return html::message(['type' => 'warning', 'options' => [i18n(null, object_content_messages::no_rows_found)]]); } $counter = 1; $table = ['header' => [], 'options' => []]; // action flags $actions = []; if (object_controller::can('record_view')) { $actions['view'] = true; } // generate columns foreach ($this->columns as $k => $v) { // if we can not view we skip action column if (empty($actions) && $k == 'action') { continue; } $table['header'][$k] = ['value' => i18n(null, $v['name']), 'nowrap' => true, 'width' => $v['width'] ?? null]; } // generate rows foreach ($this->rows as $k => $v) { // process all columns first $row = []; foreach ($this->columns as $k2 => $v2) { // if we can not view we skip action column if (empty($actions) && $k2 == 'action') { continue; } $value = []; // create cell properties foreach (['width', 'align'] as $v3) { if (isset($v2[$v3])) { $value[$v3] = $v2[$v3]; } } // process rows if ($k2 == 'action') { $value['value'] = []; if (!empty($actions['view'])) { $mvc = application::get('mvc'); $pk = extract_keys($this->model_object->pk, $v); $url = $mvc['controller'] . '/_edit?' . http_build_query2($pk); $value['value'][] = html::a(['value' => i18n(null, 'View'), 'href' => $url]); } $value['value'] = implode(' ', $value['value']); } else { if ($k2 == 'row_number') { $value['value'] = format::id($counter) . '.'; } else { if ($k2 == 'offset_number') { $value['value'] = format::id($this->offset + $counter) . '.'; } else { if (!empty($v2['options_model'])) { if (strpos($v2['options_model'], '::') === false) { $v2['options_model'] .= '::options'; } $params = $v2['options_params'] ?? []; if (!empty($v2['options_depends'])) { foreach ($v2['options_depends'] as $k0 => $v0) { $params[$k0] = $v[$v0]; } } $crypt_object = new crypt(); $hash = $crypt_object->hash($v2['options_model'] . serialize($params)); if (!isset($this->cached_options[$hash])) { $method = factory::method($v2['options_model'], null, true); $this->cached_options[$hash] = call_user_func_array($method, [['where' => $params]]); } if (isset($this->cached_options[$hash][$v[$k2]])) { $value['value'] = $this->cached_options[$hash][$v[$k2]]['name']; } else { $value['value'] = null; } } else { if (!empty($v2['options']) && !is_array($v[$k2])) { if (isset($v2['options'][$v[$k2]])) { $value['value'] = $v2['options'][$v[$k2]]['name']; } else { $value['value'] = null; } } else { if (isset($v[$k2])) { $value['value'] = $v[$k2]; } else { $value['value'] = null; } } } } } } // put value into row if (!empty($v2['format'])) { $format_options = $v2['format_options'] ?? []; if (!empty($v2['format_depends'])) { $format_depends = $v2['format_depends']; $this->process_params_and_depends($format_depends, $v); $format_options = array_merge_hard($format_options, $format_depends); } $method = factory::method($v2['format'], 'format'); $value['value'] = call_user_func_array([$method[0], $method[1]], [$value['value'], $format_options]); } $row[$k2] = $value; } // put processed columns though user defined function if (method_exists($this, 'render_data_rows')) { $table['options'][$counter] = $this->render_data_rows($row, $v); } else { $table['options'][$counter] = $row; } $counter++; } return html::table($table); }