hnhdigital-os/laravel-model-change-tracking

View on GitHub
src/LogChangeTrait.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Bluora\LaravelModelChangeTracking;

use App\Models\LogModelChange;
use Auth;
use Config;
use Diff\Differ\MapDiffer;

trait LogChangeTrait
{
    /**
     * No logging of the standard date columns.
     *
     * @param array
     */
    protected static $do_not_log_fields = ['created_at', 'updated_at', 'archived_at', 'deleted_at'];

    /**
     * Calculate the difference.
     *
     * @param string $column_name
     * @param array  &$log_change
     * @param mixed  $old_text
     * @param mixed  $new_text
     *
     * @return void
     */
    private static function getModelChangeDiff($column_name, &$log_change, $old_text, $new_text)
    {
        if (is_array($old_text) && is_array($new_text)) {
            $difference = (new MapDiffer())->doDiff($old_text, $new_text);

            foreach ($difference as $key => $value) {
                $value = $value->toArray();

                if (!array_key_exists('oldText', $value)) {
                    $value['oldText'] = '';
                }

                if (!array_key_exists('newValue', $value)) {
                    $value['newValue'] = '';
                }

                if (is_array($value['oldText']) && is_array($value['newValue'])) {
                    static::getModelChangeDiff($column_name.'.'.$key, $log_change, $value['oldText'], $value['newValue']);

                    return;
                }

                $log_change[] = [
                    'column_name' => $column_name.'.'.$key,
                    'old_text'    => $value['oldText'],
                    'new_text'    => $value['newValue'],
                ];
            }

            return;
        }

        $log_change[] = [
            'column_name' => $column_name,
            'old_text'    => $old_text,
            'new_text'    => $new_text,
        ];
    }

    /**
     * Log the change.
     *
     * @param string $model
     * @param string $table_name
     * @param string $column_name
     * @param string $old_text
     * @param string $new_text
     * @param array  $add_value
     * @param array  $remove_value
     *
     * @return void
     */
    public static function addModelChange(
        $model_id,
        $table_name,
        $column_name,
        $old_text,
        $new_text,
        $add_value,
        $remove_value
    ) {
        $models_path = Config::get('model_change_tracking.ModelsPath');
        $log_model_name = Config::get('model_change_tracking.LogModelChange');
        $model_id_name = config('model_change_tracking.log-model-change.model-id', 'model_id');

        $add_value = (is_null($add_value)) ? '' : $add_value;
        $remove_value = (is_null($remove_value)) ? '' : $remove_value;

        if ($old_text !== $new_text) {
            $log = new $log_model_name();
            $log->model = str_replace($models_path, '', static::class);
            $log->$model_id_name = $model_id;
            $log->table_name = $table_name;
            $log->column_name = $column_name;
            $log->old_text = $old_text;
            $log->new_text = $new_text;
            $log->add_value = json_encode($add_value);
            $log->remove_value = json_encode($remove_value);
            if (Auth::check()) {
                $log->log_by = Auth::user()->getKey();
            }
            $log->ip_address = request()->ip();
            $log->save();
        }
    }

    /**
     * Process relationship changes and log.
     *
     * @param string $relation_method_name
     * @param array  $new_value
     *
     * @return void
     */
    public function processManyToManyChange($relation_method_name, $new_value, $override_relationship_method = null)
    {
        // The join between the model and the other model
        $relation = $this->$relation_method_name();

        // This model
        $model_id = $this->getKey();

        if (!method_exists($relation, $relation_fk_method = 'getQualifiedForeignKeyName')) {
            $relation_fk_method = 'getQualifiedForeignPivotKeyName';
        }

        $model_key_name = $relation->$relation_fk_method();
        $model_qualified_id_column = $this->getQualifiedKeyName();

        // The other model
        $related_model = $relation->getRelated();

        if (!method_exists($relation, $relation_rk_method = 'getQualifiedRelatedKeyName')) {
            $relation_rk_method = 'getQualifiedRelatedPivotKeyName';
        }

        $related_model_key_name = $relation->$relation_rk_method();
        $related_model_id_column = $related_model->getKeyName();
        $related_model_class = get_class($related_model);

        // Check singular and plural
        if (!is_null($override_relationship_method)) {
            $relationship_method = $override_relationship_method;
        } elseif (is_null($override_relationship_method)) {
            $relationship_method = camel_case($this->getTable());
            if (!method_exists($related_model_class, $relationship_method)) {
                $relationship_method = str_plural($relationship_method);
            }
        }

        // Old items before we make change
        $old_data = $related_model_class::whereHas($relationship_method, function ($sub_query) use ($model_qualified_id_column, $model_id) {
            $sub_query->where($model_qualified_id_column, $model_id);
        })->get();

        // Run sync
        $relation->sync($new_value);

        // New items after we made the change
        $new_data = $related_model_class::whereHas($relationship_method, function ($sub_query) use ($model_qualified_id_column, $model_id) {
            $sub_query->where($model_qualified_id_column, $model_id);
        })->get();

        // Filter this
        $old_data_list = $old_data;
        $new_data_list = $new_data;
        $old_data_id_list = array_unique($old_data_list->pluck($related_model_id_column)->all());
        $new_data_id_list = array_unique($new_data_list->pluck($related_model_id_column)->all());

        // Calculate the change
        $add_value = array_diff($new_data_id_list, $old_data_id_list);
        $remove_value = array_diff($old_data_id_list, $new_data_id_list);
        $change_values = $add_value + $remove_value;

        // Old text and values
        $old_text = $old_data->whereIn($related_model_id_column, $change_values)->implode('log_name', ', ');

        // New text and values
        $new_text = $new_data->whereIn($related_model_id_column, $change_values)->implode('log_name', ', ');

        // Log against the model
        self::addModelChange(
            $this->getKey(),
            $relation->getTable(),
            $related_model_key_name,
            $old_text,
            $new_text,
            $add_value,
            $remove_value
        );

        // Log against each added related model
        foreach ($add_value as $relation_id) {
            self::addModelChange(
                $relation_id,
                $relation->getTable(),
                $model_key_name,
                '',
                $this->log_name,
                [$this->getKey()],
                []
            );
        }

        // Log against each removed related model
        foreach ($remove_value as $relation_id) {
            self::addModelChange(
                $relation_id,
                $relation->getTable(),
                $model_key_name,
                $this->log_name,
                '',
                [],
                [$this->getKey()]
            );
        }
    }

    /**
     * Get change log for this model.
     *
     * @return
     */
    public function changeLog()
    {
        $model_id_name = config('model_change_tracking.log-model-change.model-id', 'model_id');
        $model_other_id_name = config('model_change_tracking.log-model-change.model-other-id', 'id');

        return $this->hasMany(LogModelChange::class, $model_id_name, $model_other_id_name)
            ->where('table_name', $this->getTable());
    }

    /**
     * Boot the log changes trait for a model.
     *
     * @return void
     *
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    public static function bootLogChangeTrait()
    {
        // Saved model event
        static::saved(function ($model) {

            // Track all changes to every column
            $casts = $model->getCasts();
            foreach ($model->getDirty(true) as $column_name => $value) {

                // Ignore columns found in the do_not_log variable, and ignore non-json columns
                if ((empty($model->do_not_log) || !in_array($column_name, $model->do_not_log))
                    && !in_array($column_name, static::$do_not_log_fields)
                    && (empty($casts[$column_name]) || $casts[$column_name] != 'json')) {
                    $old_text = $model->getOriginal($column_name);
                    $log_change = [];
                    static::getModelChangeDiff($column_name, $log_change, $old_text, $value);
                    foreach ($log_change as $change) {
                        if (is_array($change['old_text'])) {
                            $change['old_text'] = json_encode($change['old_text']);
                        }

                        if (is_array($change['new_text'])) {
                            $change['new_text'] = json_encode($change['new_text']);
                        }

                        self::addModelChange(
                            $model->getKey(),
                            $model->getTable(),
                            $change['column_name'],
                            (string) $change['old_text'],
                            (string) $change['new_text'],
                            [],
                            []
                        );
                    }
                }
            }
        });
    }
}