/** * Handles a nested update, link or create for a single model, returning * the result. * * @param mixed $data * @param RelationInfo $info * @param string $attribute * @param null|int $index optional, for to-many list indexes to append after attribute * @return UpdateResult|false false if no model available * @throws DisallowedNestedActionException */ protected function handleNestedSingleUpdateOrCreate($data, RelationInfo $info, $attribute, $index = null) { // handle model before, use results to save foreign key on the model later $nestedKey = $this->appendNestedKey($attribute, $index); $data = $this->normalizeNestedSingularData($data, $info->model()->getKeyName(), $nestedKey); // if this data set has a temporary id, create the model and convert to link operation instead // note that we cannot assume that it is being created explicitly for this relation, it // may simply be the first time it is referenced while processing the data tree. if ($this->isHandlingTemporaryIds() && $this->hasTemporaryIds() && array_key_exists($this->getTemporaryIdAttributeKey(), $data)) { $temporaryId = $data[$this->getTemporaryIdAttributeKey()]; if (!$this->temporaryIds->hasId($temporaryId)) { return $this->makeUpdateResult(); } $model = $this->temporaryIds->getModelForId($temporaryId); if (!$model) { // if it has not been created, attempt to create it $data = $this->temporaryIds->getDataForId($temporaryId); // safeguard, this should never happen if (null === $data) { return $this->makeUpdateResult(); } $updater = $this->makeNestedParser($info->updater(), [get_class($info->model()), $attribute, $nestedKey, $this->model, $this->config]); $updateResult = $updater->create($data); // if for some reason the update or create was not succesful or // did not return a model, dissociate the relationship if (!$updateResult->model()) { return $this->makeUpdateResult(); } $this->temporaryIds->setModelForId($temporaryId, $updateResult->model()); return $updateResult; } // it has been created, so can reference it here, converting the data to link-only $data = [$model->getKeyName() => $model->getKey()]; } $updateId = Arr::get($data, $info->model()->getKeyName()); // if the key is present, but the data is empty, the relation should be dissociated if (empty($data)) { return $this->makeUpdateResult(); } // if we're not allowed to perform creates or updates, only handle the link // -- and this is not possible, stop the process or make sure it is handled right if (!$info->isUpdateAllowed()) { // if we cannot create it, we cannot proceed if (empty($updateId)) { throw (new DisallowedNestedActionException("Not allowed to create new for link-only nested relation"))->setNestedKey($nestedKey); } // strip everything but the key, so it is treated as a link-only operation $data = [$info->model()->getKeyName() => $updateId]; } // get the existing model, if we have an update ID, or null if no match exists if (!empty($updateId)) { $existingModel = $this->getModelByLookupAtribute($updateId, $info->model()->getKeyName(), get_class($info->model()), $nestedKey, false); } else { $existingModel = null; } // if a model for a given 'updateId' does not exist yet, and the model's key is // not an incrementing key, this should be treated as an attempt to create a record $creatingWithKey = !$info->model()->getIncrementing() && !empty($updateId) && !$existingModel; // if this is a link-only operation, mark it $onlyLinking = count($data) == 1 && !empty($updateId) && !$creatingWithKey; // if we are allowed to update, but only the key is provided, treat this as a link-only operation // throw an exception if we couldn't find the model if (!$info->isUpdateAllowed() || $onlyLinking) { if (!$existingModel) { throw (new NestedModelNotFoundException())->setModel(get_class($info->model()))->setNestedKey($nestedKey); } return $this->makeUpdateResult($existingModel); } // otherwise, create or update, depending on whether the primary key is present in the data // if it is a create operation, make sure we're allowed to if ((empty($updateId) || $creatingWithKey) && !$info->isCreateAllowed()) { throw (new DisallowedNestedActionException("Not allowed to create new for update-only nested relation"))->setNestedKey($nestedKey); } $updater = $this->makeNestedParser($info->updater(), [get_class($info->model()), $attribute, $nestedKey, $this->model, $this->config]); $updateResult = empty($updateId) || $creatingWithKey ? $updater->create($data) : $updater->update($data, $updateId, $info->model()->getKeyName()); // if for some reason the update or create was not succesful or // did not return a model, dissociate the relationship if (!$updateResult->model()) { return $this->makeUpdateResult(); } return $updateResult; }
/** * @param RelationInfo $info * @param string $attribute key of attribute * @param null|int $index if data is plural for this attribute, the index for it * @return array */ protected function getNestedRelationValidationRulesForSingleItem(RelationInfo $info, $attribute, $index = null) { $rules = []; $dotKey = $attribute . (null !== $index ? '.' . (int) $index : ''); $data = array_get($this->data, $dotKey); // if the data is scalar, it is treated as the primary key in a link-only operation, which should be allowed // if the relation is allowed in nesting at all -- if the data is null, it should be considered a detach // operation, which is allowed aswell. if (is_scalar($data) || null === $data) { // add rule if we know that the primary key should be an integer if ($info->model()->getIncrementing()) { $rules[$this->getNestedKeyPrefix() . $dotKey] = 'integer'; } return $rules; } // if not a scalar or null, the only other value allowed is an array $rules[$this->getNestedKeyPrefix() . $dotKey] = 'array'; $keyName = $info->model()->getKeyName(); $keyIsRequired = false; $keyMustExist = false; // if it is a link-only or update-only nested relation, require a primary key field // it also helps to check whether the key actually exists, to prevent problems with // a non-existant non-incrementing keys, which would be interpreted as a create action if (!$info->isCreateAllowed()) { $keyIsRequired = true; $keyMustExist = true; } elseif (!$info->model()->getIncrementing()) { // if create is allowed, then the primary key is only required for non-incrementing key models, // for which it should always be present $keyIsRequired = true; } // if the primary key is not present, this is a create operation, so we must apply the model's create rules // otherwise, it's an update operation -- if the model is non-incrementing, however, the create/update // distinction depends on whether the given key exists if ($info->model()->getIncrementing()) { $creating = !array_has($data, $keyName); } else { $key = array_get($data, $keyName); $creating = !$key || !$this->checkModelExistsByLookupAtribute($key, $keyName, get_class($info->model())); } if (!$creating) { $keyMustExist = true; } // build up rules for primary key $keyRules = []; if ($info->model()->getIncrementing()) { $keyRules[] = 'integer'; } if ($keyIsRequired) { $keyRules[] = 'required'; } if ($keyMustExist) { $keyRules[] = 'exists:' . $info->model()->getTable() . ',' . $keyName; } if (count($keyRules)) { $rules[$this->getNestedKeyPrefix() . $dotKey . '.' . $keyName] = $keyRules; } // get and merge rules for model fields by deferring to a nested validator /** @var NestedValidatorInterface $validator */ $validator = $this->makeNestedParser($info->validator(), [get_class($info->model()), $attribute, $this->appendNestedKey($attribute, $index), $this->model, $this->config, $this->modelClass]); $rules = $this->mergeInherentRulesWithCustomModelRules($rules, $validator->validationRules($data, $creating)); return $rules; }