/** * Save the current objects attributes * * @param boolean $runValidation * should validations be executed on all models before allowing saveAll() * @param boolean $hasParentModel * whether this method was called from the top level or by a parent * If false, it means the method was called at the top level * @param boolean $fromSaveAll * has the save() call come from saveAll() or not * @return boolean * did save() successfully process */ public function save($runValidation = true, $hasParentModel = false, $push = false, $fromSaveAll = false) { if ($this->getReadOnly() && !$hasParentModel) { // return failure if we are at the top of the tree and should not be asking to saveAll // not allowed to amend or delete $message = 'Attempting to save on ' . Tools::getClassName($this) . ' readOnly model'; //$this->addActionError($message); throw new Exception($message); } elseif ($this->getReadOnly() && $hasParentModel) { $message = 'Skipping save on ' . Tools::getClassName($this) . ' readOnly model'; $this->addActionWarning($message); return true; } $allOk = true; if (($this->loaded || $this->isNewRecord && $this->isNewPrepared) && $this->changedData) { if ($this->entityId === false) { throw new Exception('No entity id available for ' . __METHOD__ . '()'); } if (!$this->objectId) { throw new Exception('No object id available for ' . __METHOD__ . '()'); } $thisTime = time(); $attributeDefs = $this->getEntityAttributeList(); // we do not record modified, modifiedBy, created or createdBy against individual attributes but we will support // automatically updating them if these attributeNames have been setup as their own attributes for this entity if (\Yii::$app->has('user')) { try { if (\Yii::$app->user->isGuest) { $userId = 0; } else { $userId = \Yii::$app->user->getId(); } } catch (InvalidConfigException $e) { if ($e->getMessage() == 'User::identityClass must be set.') { $userId = 0; } else { throw $e; } } } $extraChangeFields = array(); if (array_key_exists('modifiedAt', $attributeDefs)) { if (!array_key_exists('modifiedAt', $this->changedData)) { $exists = array_key_exists('modifiedAt', $this->data); $this->changedData['modifiedAt'] = array_key_exists('modifiedAt', $this->data) ? $this->data['modifiedAt'] : Tools::DATE_TIME_DB_EMPTY; $this->data['modifiedAt'] = date(Tools::DATETIME_DATABASE, $thisTime); if ($this->lazyAttributes && array_key_exists('modifiedAt', $this->lazyAttributes)) { unset($this->lazyAttributes['modifiedAt']); } } } if (array_key_exists('modifiedBy', $attributeDefs)) { if (!array_key_exists('modifiedBy', $this->changedData)) { if (!isset($this->data['modifiedBy']) || $this->data['modifiedBy'] != $userId) { $this->changedData['modifiedBy'] = array_key_exists('modifiedBy', $this->data) ? $this->data['modifiedBy'] : 0; $this->data['modifiedBy'] = $userId; if ($this->lazyAttributes && array_key_exists('modifiedBy', $this->lazyAttributes)) { unset($this->lazyAttributes['modifiedBy']); } } } } if (array_key_exists('createdAt', $attributeDefs)) { if (!array_key_exists('createdAt', $this->changedData)) { $exists = array_key_exists('createdAt', $this->data); if (!$exists || $exists && $this->data['createdAt'] == Tools::DATE_TIME_DB_EMPTY) { $this->changedData['createdAt'] = array_key_exists('createdAt', $this->data) ? $this->data['createdAt'] : Tools::DATE_TIME_DB_EMPTY; $this->data['createdAt'] = date(Tools::DATETIME_DATABASE, $thisTime); if ($this->lazyAttributes && array_key_exists('created', $this->lazyAttributes)) { unset($this->lazyAttributes['createdAt']); } } } } if (array_key_exists('createdBy', $attributeDefs)) { if (!array_key_exists('createdBy', $this->changedData)) { $exists = array_key_exists('createdBy', $this->data); if (!$exists || $exists && $this->data['createdBy'] != $userId) { $this->changedData['createdBy'] = array_key_exists('createdBy', $this->data) ? $this->data['createdBy'] : 0; $this->data['createdBy'] = $userId; if ($this->lazyAttributes && array_key_exists('createdBy', $this->lazyAttributes)) { unset($this->lazyAttributes['createdBy']); } } } } if (!$this->changedData) { $updateColumns = $this->data; } else { $updateColumns = array(); foreach ($this->changedData as $field => $value) { $updateColumns[$field] = $this->data[$field]; } } foreach ($updateColumns as $attributeName => $attributeValue) { $attributeId = 0; $attributeDef = isset($attributeDefs[$attributeName]) ? $attributeDefs[$attributeName] : false; if ($attributeDef) { $attributeId = $attributeDef['id']; } $ok = false; if ($attributeId) { $attributeValue = Tools::formatAttributeValue($attributeValue, $attributeDef); if ($attributeDef['deleteOnDefault'] && $attributeValue === Tools::formatAttributeValue($attributeDef['defaultValue'], $attributeDef)) { // value is default so we will remove it from the attribtue table as not required $ok = true; if (array_key_exists($attributeName, $this->attributeValues)) { $ok = $this->attributeValues[$attributeName]->deleteFull(true); if ($ok) { $this->setChildOldValues($attributeName, true, 'deleted'); } else { if ($this->attributeValues[$attributeName]->hasActionErrors()) { $this->mergeActionErrors($this->attributeValues[$attributeName]->getActionErrors()); } else { $this->addActionError('Failed to delete attribute', 0, $attributeName); } } } } else { switch (strtolower($attributeDef['dataType'])) { case 'boolean': $attributeValue = $attributeValue ? '1' : '0'; break; default: break; } if (is_null($attributeValue)) { // typically where null is permitted it will be the default value with deleteOnDefault set, so should have been caught in the deleteOnDefault if ($attributeDef['isNullable']) { $attributeValue = '__NULL__'; // we do not want to allow null in the attribute database so use this string to denote null when it is permitted } else { $attributeValue = '__NULL__'; // needs to be caught elsewhere } } if (!array_key_exists($attributeName, $this->attributeValues)) { $this->attributeValues[$attributeName] = new $this->attributeValuesClass(); $this->attributeValues[$attributeName]->entityId = $this->entityId; $this->attributeValues[$attributeName]->attributeId = $attributeId; // this is a new entry that has not been included in the childHasChanges array yet $this->setChildHasChanges($attributeName); $this->setChildOldValues($attributeName, $this->attributeValues[$attributeName]->getResetDataForFailedSave()); } if ($this->newObjectId) { $this->attributeValues[$attributeName]->objectId = $this->newObjectId; } else { $this->attributeValues[$attributeName]->objectId = $this->objectId; } $this->attributeValues[$attributeName]->value = $attributeValue; $ok = $this->attributeValues[$attributeName]->save(false, null, true, true); if (!$ok) { if ($this->attributeValues[$attributeName]->hasActionErrors()) { $this->mergeActionErrors($this->attributeValues[$attributeName]->getActionErrors()); } else { $this->addActionError('Failed to save attribute', 0, $attributeName); } } } } if (!$ok) { $allOk = false; } } if ($allOk) { $this->changedData = array(); $this->loaded = true; $this->isNewRecord = false; } } if ($allOk && $this->loaded && $this->newObjectId) { // we need to update the objectId for all attributes belonging to // the current object to a new value taking into account that not // all attributes might have been loaded yet, if any. foreach ($this->attributeValues as $attributeName => $attributeValue) { $this->attributeValues[$attributeName]->objectId = $this->newObjectId; $this->attributeValues[$attributeName]->setOldAttribute('objectId', $this->newObjectId); } $attributeValuesClass = $this->attributeValuesClass; $ok = $attributeValuesClass::updateAll(array('objectId' => $this->newObjectId), array('objectId' => $this->objectId)); $this->objectId = $this->newObjectId; $this->newObjectId = false; } return $allOk; }
/** * Returns the attribute values that have been modified in this new record since loading the default values * Only really gets used by self::hasChanges() when _oldAttributes == [] (typically when no default values are set * because they are all set to null in the table schema) * @param string[]|null $names the names of the attributes whose values may be returned if they are * changed recently. If null, [[attributes()]] will be used. * @return array the changed attribute values (name-value pairs) */ public function getIsNewDirtyAttributes($names = null) { if (!$this->getIsNewRecord() || !$this->defaultsApplied) { return $this->getDirtyAttributes($names); } $attributes = []; $theAttributes = $this->getAttributes($names); if ($theAttributes) { $stru = self::getTableSchema(); $columns = $stru->columns; foreach ($theAttributes as $name => $value) { $spec = ArrayHelper::getValue($columns, $name, []); if ($spec) { if ($spec->isPrimaryKey && $spec->autoIncrement) { if ($value !== null) { $attributes[$name] = $value; } } else { $defaultValue = Tools::formatAttributeValue('__DEFAULT__', $spec); if ($defaultValue === null && $value !== null) { $attributes[$name] = $value; } elseif ($defaultValue != $value) { $attributes[$name] = $value; } } } } } return $attributes; }