/** * Create root node if multiple-root tree mode. Update node if it's not new * * @param boolean $runValidation * should validations be executed on all models before allowing save() * @param array $attributes * which attributes should be saved (default null means all changed attributes) * @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 */ private function save($runValidation = true, $attributes = null, $hasParentModel = false, $fromSaveAll = false) { if ($this->owner->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->owner) . ' readOnly model'; //$this->addActionError($message); throw new \fangface\db\Exception($message); } elseif ($this->owner->getReadOnly() && $hasParentModel) { $message = 'Skipping save on ' . Tools::getClassName($this->owner) . ' readOnly model'; $this->addActionWarning($message); return true; } else { if ($runValidation && !$this->owner->validate($attributes)) { return false; } if ($this->owner->getIsNewRecord()) { return $this->makeRoot($attributes); } $updateChildPaths = false; if ($this->hasPaths && !$this->owner->getIsNewRecord()) { if ($this->owner->hasAttribute($this->pathAttribute)) { if ($this->owner->hasChanged($this->pathAttribute)) { $updateChildPaths = true; if ($this->_previousPath == '') { $this->_previousPath = $this->owner->getOldAttribute($this->pathAttribute); } } } if (!$updateChildPaths && $this->owner->hasAttribute($this->nameAttribute)) { if ($this->owner->hasChanged($this->nameAttribute)) { $this->_previousPath = $this->owner->getAttribute($this->pathAttribute); $this->checkAndSetPath($this->owner); if ($this->_previousPath != $this->owner->getAttribute($this->pathAttribute)) { $updateChildPaths = true; } } } } $nameChanged = false; if ($this->owner->hasAttribute($this->nameAttribute) && $this->owner->hasChanged($this->nameAttribute)) { $nameChanged = true; if (!$this->beforeRenameNode($this->_previousPath)) { return false; } } $result = false; $db = $this->owner->getDb(); if ($db->getTransaction() === null) { $transaction = $db->beginTransaction(); } try { $this->_ignoreEvent = true; //$result = $this->owner->update(false, $attributes); if (false && method_exists($this->owner, 'saveAll')) { $result = $this->owner->saveAll(false, $hasParentModel, false, $attributes); } else { $result = $this->owner->save(false, $attributes, $hasParentModel, $fromSaveAll); } $this->_ignoreEvent = false; if ($result && $updateChildPaths) { // only if we have children if ($this->owner->getAttribute($this->rightAttribute) > $this->owner->getAttribute($this->leftAttribute) + 1) { $condition = $db->quoteColumnName($this->leftAttribute) . '>' . $this->owner->getAttribute($this->leftAttribute) . ' AND ' . $db->quoteColumnName($this->rightAttribute) . '<' . $this->owner->getAttribute($this->rightAttribute); $params = []; if ($this->hasManyRoots) { $condition .= ' AND ' . $db->quoteColumnName($this->rootAttribute) . '=:' . $this->rootAttribute; $params[':' . $this->rootAttribute] = $this->owner->getAttribute($this->rootAttribute); } $updateColumns = []; $pathLength = Tools::strlen($this->_previousPath) + 1; // SQL Server: SUBSTRING() rather than SUBSTR // SQL Server: + instead of CONCAT if ($db->getDriverName() == 'mssql') { $updateColumns[$this->pathAttribute] = new Expression($db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ' + SUBSTRING(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))'); } else { $updateColumns[$this->pathAttribute] = new Expression('CONCAT(' . $db->quoteValue($this->owner->getAttribute($this->pathAttribute)) . ', SUBSTR(' . $db->quoteColumnName($this->pathAttribute) . ', ' . $pathLength . '))'); } $result = $this->owner->updateAll($updateColumns, $condition, $params); } } if ($result && $nameChanged) { $result = $this->afterRenameNode($this->_previousPath); } } catch (\Exception $e) { if (isset($transaction)) { $transaction->rollback(); } throw $e; } if (isset($transaction)) { if (!$result) { $transaction->rollback(); } else { $transaction->commit(); } } $this->_previousPath = ''; } return $result; }
/** * This method is called at the beginning of a deleteFull() request on a record array * * @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 * @return boolean whether the deleteFull() method call should continue * If false, deleteFull() will be cancelled. */ public function beforeDeleteFullInternal($hasParentModel = false) { $this->clearActionErrors(); $this->resetChildHasChanges(); $canDeleteFull = true; if (!$hasParentModel) { //$event = new ModelEvent; //$this->trigger(self::EVENT_BEFORE_SAVE_ALL, $event); //$canSaveAll = $event->isValid; } if ($this->getReadOnly()) { // will be ignored during deleteFull() } elseif (!$this->getCanDelete()) { // will be ignored during deleteFull() } else { if ($canDeleteFull) { if ($this->count()) { $iterator = $this->getIterator(); while ($iterator->valid()) { $isReadOnly = false; $canDelete = true; if ($iterator->current() instanceof ActiveRecordReadOnlyInterface) { $isReadOnly = $iterator->current()->getReadOnly(); $canDelete = $iterator->current()->getCanDelete(); } if (!$isReadOnly && $canDelete) { $this->setChildHasChanges($iterator->key()); if ($iterator->current() instanceof ActiveRecordSaveAllInterface) { $this->setChildOldValues($iterator->key(), $iterator->current()->getResetDataForFailedSave()); } else { $this->setChildOldValues($iterator->key(), array('new' => $iterator->current()->getIsNewRecord(), 'oldValues' => $iterator->current()->getOldAttributes(), 'current' => $iterator->current()->getAttributes())); } $canDeleteThis = true; if ($iterator->current() instanceof ActiveRecordSaveAllInterface) { $canDeleteThis = $iterator->current()->beforeDeleteFullInternal(true); if (!$canDeleteThis) { if (method_exists($iterator->current(), 'hasActionErrors')) { if ($iterator->current()->hasActionErrors()) { $this->mergeActionErrors($iterator->current()->getActionErrors()); } } } } elseif (method_exists($iterator->current(), 'beforeDeleteFull')) { $canDeleteThis = $iterator->current()->beforeDeleteFull(); if (!$canDeleteThis) { $errors = $iterator->current()->getErrors(); foreach ($errors as $errorField => $errorDescription) { $this->addActionError($errorDescription, 0, $errorField, Tools::getClassName($iterator->current())); } } } if (!$canDeleteThis) { $canDeleteFull = false; } } $iterator->next(); } } } } if ($this->hasActionErrors()) { $canDeleteFull = false; } elseif (!$canDeleteFull) { $this->addActionError('beforeDeleteFullInternal checks failed'); } if (!$canDeleteFull) { $this->resetChildHasChanges(); } return $canDeleteFull; }
/** * 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; }
/** * Adds a new action warning * @param string $message new warning message * @param integer $code new warning code * @param string $attribute attribute to which the error applies * @param string $modelName model to which the error applies */ public function addActionWarning($message, $code = 0, $attribute = '', $modelName = null) { $message = is_array($message) ? $message : array($message); $this->actionWarnings[] = array('message' => $message, 'code' => $code, 'attribute' => $attribute, 'model' => $modelName !== null ? $modelName : (true ? Tools::getClassName($this) : get_called_class())); }
/** * PHP setter magic method. * This method is overridden so that AR attributes can be accessed like properties, * but only if the current model is not read only * @param string $name property name * @param mixed $value property value * @throws Exception if the current record is read only */ public function __set($name, $value) { if ($this->getReadOnly()) { throw new Exception('Attempting to set attribute `' . $name . '` on a read only ' . Tools::getClassName($this) . ' model'); } parent::__set($name, $value); }