Skip to content
Snippets Groups Projects
AuditTrailBehavior.php 10.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • Marcello Pivanti's avatar
    Marcello Pivanti committed
    <?php
    /**
    
     * Copyright 2020 Art-ER S. Cons. P.A.
     * EROI - Emilia Romagna Open Innovation is based on:
     * https://www.open2.0.regione.lombardia.it
    
    Marcello Pivanti's avatar
    Marcello Pivanti committed
     *
    
     * @see https://repo.art-er.it Developers' community
    
     * @license GPLv3
     * @license https://opensource.org/licenses/gpl-3.0.html GNU General Public License version 3
     *
    
    Marcello Pivanti's avatar
    Marcello Pivanti committed
     * @package    arter\amos\audit
    
    Marcello Pivanti's avatar
    Marcello Pivanti committed
     * @category   CategoryName
     * @author     Elite Division S.r.l.
     */
    
    waltermazza's avatar
    waltermazza committed
    namespace arter\amos\audit;
    
    Marcello Pivanti's avatar
    Marcello Pivanti committed
    
    
    waltermazza's avatar
    waltermazza committed
    use arter\amos\audit\models\AuditTrail;
    
    Marcello Pivanti's avatar
    Marcello Pivanti committed
    
    use Yii;
    use yii\base\Behavior;
    use yii\db\ActiveRecord;
    use yii\helpers\Json;
    use yii\db\Query;
    
    /**
     * Class AuditTrailBehavior
    
    waltermazza's avatar
    waltermazza committed
     * @package arter\amos\audit
    
    Marcello Pivanti's avatar
    Marcello Pivanti committed
     *
     * @property \yii\db\ActiveRecord $owner
     */
    class AuditTrailBehavior extends Behavior
    {
    
        /**
         * Array with fields to save
         * You don't need to configure both `allowed` and `ignored`
         * @var array
         */
        public $allowed = [];
    
        /**
         * Array with fields to ignore
         * You don't need to configure both `allowed` and `ignored`
         * @var array
         */
        public $ignored = [];
    
        /**
         * Array with classes to ignore
         * @var array
         */
        public $ignoredClasses = [];
    
        /**
         * Timestamp attributes should, in most cases, be ignored. If both AudittrailBehavior and
         * TimestampBehavior logs the created_at and updated_at fields, the data is saved twice.
         * In case you want to log them, you can unset the column from this timestamp column name suggestions.
         * Set to null to disable this filter and log all columns.
         * @var null|array
         */
        public $timestamp_fields = ['created', 'updated', 'created_at', 'updated_at', 'timestamp'];
    
        /**
         * Is the behavior is active or not
         * @var boolean
         */
        public $active = true;
    
        /**
         * Date format to use in stamp - set to "Y-m-d H:i:s" for datetime or "U" for timestamp
         * @var string
         */
        public $dateFormat = 'Y-m-d H:i:s';
    
        /**
         * @var array
         */
        private $_oldAttributes = [];
    
        /**
         * Array with fields you want to override before saving the row into audit_trail table
         * @var array
         */
        public $override = [];
    
        /**
         * @inheritdoc
         */
        public function events()
        {
            return [
                ActiveRecord::EVENT_AFTER_FIND => 'afterFind',
                ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert',
                ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate',
                ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
            ];
        }
    
        /**
         *
         */
        public function afterFind()
        {
            $this->setOldAttributes($this->owner->getAttributes());
        }
    
        /**
         *
         */
        public function afterInsert()
        {
            $this->audit('CREATE');
            $this->setOldAttributes($this->owner->getAttributes());
        }
    
        /**
         *
         */
        public function afterUpdate()
        {
            $this->audit('UPDATE');
            $this->setOldAttributes($this->owner->getAttributes());
        }
    
        /**
         *
         */
        public function afterDelete()
        {
            $this->audit('DELETE');
            $this->setOldAttributes([]);
        }
    
        /**
         * @param $action
         * @throws \yii\db\Exception
         */
        public function audit($action)
        {
            // Not active? get out of here
            if (!$this->active) {
                return;
            }
            
            // Lets check if the whole class should be ignored
            if (sizeof($this->ignoredClasses) > 0 && array_search(get_class($this->owner), $this->ignoredClasses) !== false) {
                return;
            }
            
            // If this is a delete then just write one row and get out of here
            if ($action == 'DELETE') {
                $this->saveAuditTrailDelete();
                return;
            }
            
            // Now lets actually write the attributes
            $this->auditAttributes($action);
        }
    
        /**
         * Clean attributes of fields that are not allowed or ignored.
         *
         * @param $attributes
         * @return mixed
         */
        protected function cleanAttributes($attributes)
        {
            $attributes = $this->cleanAttributesAllowed($attributes);
            $attributes = $this->cleanAttributesIgnored($attributes);
            $attributes = $this->cleanAttributesOverride($attributes);
            return $attributes;
        }
    
        /**
         * Unset attributes which are not allowed
         *
         * @param $attributes
         * @return mixed
         */
        protected function cleanAttributesAllowed($attributes)
        {
            if (sizeof($this->allowed) > 0) {
                foreach ($attributes as $f => $v) {
                    if (array_search($f, $this->allowed) === false) {
                        unset($attributes[$f]);
                    }
                }
            }
            return $attributes;
        }
    
        /**
         * Unset attributes which are ignored
         *
         * @param $attributes
         * @return mixed
         */
        protected function cleanAttributesIgnored($attributes)
        {
            if(is_array($this->timestamp_fields) && count($this->timestamp_fields) > 0) {
                $this->ignored = array_merge($this->ignored, $this->timestamp_fields);
            }
    
            if (count($this->ignored) > 0) {
                foreach ($attributes as $f => $v) {
                    if (array_search($f, $this->ignored) !== false) {
                        unset($attributes[$f]);
                    }
                }
            }
            return $attributes;
        }
    
        /**
         * attributes which need to get override with a new value
         *
         * @param $attributes
         * @return mixed
         */
        protected function cleanAttributesOverride($attributes)
        {
            if (sizeof($this->override) > 0 && sizeof($attributes) >0) {
                foreach ($this->override as $field => $queryParams) {
                    $newOverrideValues = $this->getNewOverrideValues($attributes[$field], $queryParams);
                    $saveField = \yii\helpers\ArrayHelper::getValue($queryParams, 'saveField', $field);
    
                    if (count($newOverrideValues) >1) {
                        $attributes[$saveField] = implode(', ',
                                            \yii\helpers\ArrayHelper::map($newOverrideValues, $queryParams['returnField'], $queryParams['returnField'])
                        );
                    } elseif (count($newOverrideValues) == 1) {
                        $attributes[$saveField] = $newOverrideValues[0][$queryParams['returnField']];
                    }
                }
            }
            return $attributes;
        }
    
        /**
         * @param string $searchFieldValue
         * @param string $queryParams
         * @return mixed
         */
        private function getNewOverrideValues($searchFieldValue, $queryParams)
        {
            $query = new Query;
    
            $query
                ->select($queryParams['returnField'])
                ->from($queryParams['tableName'])
                ->where([$queryParams['searchField'] => $searchFieldValue]);
    
            $rows = $query->all();
    
            return $rows;
        }
    
    
        /**
         * @param string $action
         * @throws \yii\db\Exception
         */
        protected function auditAttributes($action)
        {
            // Get the new and old attributes
            $newAttributes = $this->cleanAttributes($this->owner->getAttributes());
            $oldAttributes = $this->cleanAttributes($this->getOldAttributes());
    
            // ensure to handle serialized attributes properly
            foreach($newAttributes as $key => $value) {
                if(is_array($newAttributes[$key])) {
                    $newAttributes[$key] = Json::encode($newAttributes[$key]);
                }
            }
    
            foreach($oldAttributes as $key => $value) {
                if(is_array($oldAttributes[$key])) {
                    $oldAttributes[$key] = Json::encode($oldAttributes[$key]);
                }
            }
    
            // If no difference then get out of here
            if (count(array_diff_assoc($newAttributes, $oldAttributes)) <= 0) {
                return;
            }
            
            // Get the trail data
            $entry_id = $this->getAuditEntryId();
            $user_id = $this->getUserId();
            $model = $this->owner->className();
            $model_id = $this->getNormalizedPk();
            $created = date($this->dateFormat);
    
            $this->saveAuditTrail($action, $newAttributes, $oldAttributes, $entry_id, $user_id, $model, $model_id, $created);
        }
    
        /**
         * Save the audit trails for a create or update action
         *
         * @param $action
         * @param $newAttributes
         * @param $oldAttributes
         * @param $entry_id
         * @param $user_id
         * @param $model
         * @param $model_id
         * @param $created
         * @throws \yii\db\Exception
         */
        protected function saveAuditTrail($action, $newAttributes, $oldAttributes, $entry_id, $user_id, $model, $model_id, $created)
        {
            // Build a list of fields to log
            $rows = array();
            foreach ($newAttributes as $field => $new) {
                $old = isset($oldAttributes[$field]) ? $oldAttributes[$field] : '';
                // If they are not the same lets write an audit log
                if ($new != $old) {
                    $rows[] = [$entry_id, $user_id, $old, $new, $action, $model, $model_id, $field, $created];
                }
            }
            // Record the field changes with a batch insert
            if (!empty($rows)) {
                $columns = ['entry_id', 'user_id', 'old_value', 'new_value', 'action', 'model', 'model_id', 'field', 'created'];
                $audit = Audit::getInstance();
                $audit->getDb()->createCommand()->batchInsert(AuditTrail::tableName(), $columns, $rows)->execute();
            }
        }
    
        /**
         * Save the audit trails for a delete action
         */
        protected function saveAuditTrailDelete()
        {
            $audit = Audit::getInstance();
            $audit->getDb()->createCommand()->insert(AuditTrail::tableName(), [
                'action' => 'DELETE',
                'entry_id' => $this->getAuditEntryId(),
                'user_id' => $this->getUserId(),
                'model' => $this->owner->className(),
                'model_id' => $this->getNormalizedPk(),
                'created' => date($this->dateFormat),
            ])->execute();
        }
    
        /**
         * @return array
         */
        public function getOldAttributes()
        {
            return $this->_oldAttributes;
        }
    
        /**
         * @param $value
         */
        public function setOldAttributes($value)
        {
            $this->_oldAttributes = $value;
        }
    
        /**
         * @return string
         */
        protected function getNormalizedPk()
        {
            $pk = $this->owner->getPrimaryKey();
            return is_array($pk) ? json_encode($pk) : $pk;
        }
    
        /**
         * @return int|null|string
         */
        protected function getUserId()
        {
            return Audit::getInstance()->getUserId();
        }
    
        /**
         * @return models\AuditEntry|null|static
         * @throws \Exception
         */
        protected function getAuditEntryId()
        {
            $module = Audit::getInstance();
            
            if (!$module) {
                $module = \Yii::$app->getModule(Audit::findModuleIdentifier());
            }
            
            if (!$module) {
                throw new \Exception('Audit module cannot be loaded');
            }
            
            $entry = Audit::getInstance()->getEntry(true);
            
            return (empty($entry)) ? null : $entry->id;
        }
    
    }