Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor relationship creation #4095

Merged
merged 16 commits into from
Jan 23, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 74 additions & 96 deletions src/app/Library/CrudPanel/Traits/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ public function create($data)
{
$data = $this->decodeJsonCastedAttributes($data);
$data = $this->compactFakeFields($data);

$data = $this->changeBelongsToNamesFromRelationshipToForeignKey($data);

// omit the n-n relationships when updating the eloquent item
$nn_relationships = Arr::pluck($this->getRelationFieldsWithPivot(), 'name');
$field_names_to_exclude = $this->getFieldNamesArray($this->relationFieldsWithoutRelationType('BelongsTo', true));

$item = $this->model->create(Arr::except($data, $field_names_to_exclude));

$item = $this->model->create(Arr::except($data, $nn_relationships));
$relation_data = $this->getRelationDetailsFromInput($data);

// if there are any relationships available, also sync those
$this->createRelations($item, $data);
// handle the creation of the model relations after the main entity is created.
$this->createRelationsForItem($item, $relation_data);

return $item;
}
Expand Down Expand Up @@ -85,75 +87,6 @@ public function getRelationFields()
return $relationFields;
}

/**
* Create the relations for the current model.
*
* @param \Illuminate\Database\Eloquent\Model $item The current CRUD model.
* @param array $data The form data.
*/
public function createRelations($item, $data)
{
$this->syncPivot($item, $data);
$this->createOneToOneRelations($item, $data);
}

/**
* Sync the declared many-to-many associations through the pivot field.
*
* @param \Illuminate\Database\Eloquent\Model $model The current CRUD model.
* @param array $input The form input.
*/
public function syncPivot($model, $input)
{
$fields_with_pivot = $this->getRelationFieldsWithPivot();

//remove fields that are not in the submitted form input
$fields_with_pivot = array_filter($fields_with_pivot, function ($item) use ($input) {
return Arr::has($input, $item['name']);
});

foreach ($fields_with_pivot as $key => $field) {
$values = $input[$field['name']] ?? [];

// if a JSON was passed instead of an array, turn it into an array
if (is_string($values)) {
$values = json_decode($values, true);
}

$relation_data = [];
if (isset($field['subfields'])) {
foreach ($values as $pivot_row) {
$relation_data[$pivot_row[$field['name']]] = Arr::except($pivot_row, $field['name']);
}
}

// if there is no relation data, and the values array is single dimensional we have
// an array of keys with no aditional pivot data. sync those.
if (empty($relation_data) && count($values) == count($values, COUNT_RECURSIVE)) {
$relation_data = array_values($values);
}

$model->{$field['name']}()->sync($relation_data);

if (isset($field['morph']) && $field['morph'] && isset($input[$field['name']])) {
$values = $input[$field['name']];
$model->{$field['name']}()->sync($values);
}
}
}

/**
* Create any existing one to one relations for the current model from the form data.
*
* @param \Illuminate\Database\Eloquent\Model $item The current CRUD model.
* @param array $input The form data.
*/
private function createOneToOneRelations($item, $input)
{
$relationDetails = $this->getRelationDetailsFromInput($input);
$this->createRelationsForItem($item, $relationDetails);
}

/**
* Create any existing one to one relations for the current model from the relation data.
*
Expand All @@ -170,30 +103,60 @@ private function createRelationsForItem($item, $formattedRelations)
if (! isset($relationDetails['model'])) {
continue;
}
$model = $relationDetails['model'];
$relation = $item->{$relationMethod}();
$relation_type = $relationDetails['relation_type'];

switch ($relation_type) {
case 'BelongsTo':
$modelInstance = $relationDetails['model']::find($relationDetails['values'])->first();
if ($modelInstance != null) {
$relation->associate($modelInstance)->save();
} else {
$relation->dissociate()->save();
}
break;

case 'HasOne':
case 'MorphOne':
$modelInstance = $this->createUpdateOrDeleteOneToOneRelation($relation, $relationMethod, $relationDetails);
break;
case 'HasMany':
case 'MorphMany':
$relation_values = $relationDetails['values'][$relationMethod];
// if relation values are null we can only attach, also we check if we sent
// - a single dimensional array: [1,2,3]
// - an array of arrays: [[1][2][3]]
// if is as single dimensional array we can only attach.
if ($relation_values === null || count($relation_values) == count($relation_values, COUNT_RECURSIVE)) {
$this->attachManyRelation($item, $relation, $relationDetails, $relation_values);
} else {
$this->createManyEntries($item, $relation, $relationMethod, $relationDetails);
}
break;
case 'BelongsToMany':
case 'MorphToMany':
$values = $relationDetails['values'][$relationMethod] ?? [];
$values = is_string($values) ? json_decode($values, true) : $values;

$relation_data = [];

foreach ($values as $value) {
if (! isset($value[$relationMethod])) {
continue;
}
$relation_data[$value[$relationMethod]] = Arr::except($value, $relationMethod);
}

if ($relation instanceof BelongsTo) {
$modelInstance = $model::find($relationDetails['values'])->first();
if ($modelInstance != null) {
$relation->associate($modelInstance)->save();
} else {
$relation->dissociate()->save();
}
} elseif ($relation instanceof HasOne || $relation instanceof MorphOne) {
$modelInstance = $this->createUpdateOrDeleteOneToOneRelation($relation, $relationMethod, $relationDetails);
} elseif ($relation instanceof HasMany || $relation instanceof MorphMany) {
$relation_values = $relationDetails['values'][$relationMethod];
// if relation values are null we can only attach, also we check if we sent
// - a single dimensional array: [1,2,3]
// - an array of arrays: [[1][2][3]]
// if is as single dimensional array we can only attach.
if ($relation_values === null || count($relation_values) == count($relation_values, COUNT_RECURSIVE)) {
$this->attachManyRelation($item, $relation, $relationDetails, $relation_values);
} else {
$this->createManyEntries($item, $relation, $relationMethod, $relationDetails);
}
// if there is no relation data, and the values array is single dimensional we have
// an array of keys with no aditional pivot data. sync those.
if (empty($relation_data) && count($values) === count($values, COUNT_RECURSIVE)) {
$relation_data = array_values($values);
}

$item->{$relationMethod}()->sync($relation_data);
break;
}

if (isset($relationDetails['relations'])) {
$this->createRelationsForItem($modelInstance, ['relations' => $relationDetails['relations']]);
}
Expand Down Expand Up @@ -363,7 +326,7 @@ private function createManyEntries($entry, $relation, $relationMethod, $relation
*/
private function getRelationDetailsFromInput($input)
{
$relationFields = $this->getRelationFieldsWithoutPivot();
$relationFields = $this->getRelationFields();

//remove fields that are not in the submitted form input
$relationFields = array_filter($relationFields, function ($item) use ($input) {
Expand Down Expand Up @@ -391,6 +354,7 @@ private function getRelationDetailsFromInput($input)
$fieldDetails['parent'] = $fieldDetails['parent'] ?? $this->getRelationModel($field['name'], -1);
$fieldDetails['entity'] = $fieldDetails['entity'] ?? $field['entity'];
$fieldDetails['attribute'] = $fieldDetails['attribute'] ?? $field['attribute'];
$fieldDetails['relation_type'] = $fieldDetails['relation_type'] ?? $field['relation_type'];
$fieldDetails['values'][$attributeName] = Arr::get($input, $field['name']);

if (isset($field['fallback_id'])) {
Expand All @@ -405,4 +369,18 @@ private function getRelationDetailsFromInput($input)

return $relationDetails;
}

protected function getFieldNamesArray($fields)
{
return array_unique(
// we check if any of the field names to be removed contains a dot, if so, we remove all fields from array with same key.
// example: HasOne Address -> address.street, address.country, would remove whole `address` instead of both single fields
array_map(function ($field_name) {
if (Str::contains($field_name, '.')) {
return Str::before($field_name, '.');
}

return $field_name;
}, Arr::pluck($fields, 'name')));
}
}
27 changes: 27 additions & 0 deletions src/app/Library/CrudPanel/Traits/Relationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,33 @@ public function parseRelationFieldNamesFromHtml($fields)
return $fields;
}

/**
* Gets the relation fields that DON'T contain the provided relations.
*
* @param string|array $relations - the relations to exclude
* @param bool $include_nested - if the nested relations of the same relations should be excluded too.
*/
protected function relationFieldsWithoutRelationType($relations, $include_nested = false)
{
if (! is_array($relations)) {
$relations = [$relations];
}

$fields = $this->getRelationFields();

foreach ($relations as $relation) {
$fields = array_filter($fields, function ($field) use ($relation, $include_nested) {
if ($include_nested) {
return $field['relation_type'] !== $relation || ($field['relation_type'] === $relation && Str::contains($field['name'], '.'));
}

return $field['relation_type'] !== $relation;
});
}

return $fields;
}

protected function changeBelongsToNamesFromRelationshipToForeignKey($data)
{
$belongs_to_fields = $this->getFieldsWithRelationType('BelongsTo');
Expand Down
10 changes: 5 additions & 5 deletions src/app/Library/CrudPanel/Traits/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ public function update($id, $data)

$data = $this->changeBelongsToNamesFromRelationshipToForeignKey($data);

$this->createRelations($item, $data);
$relation_data = $this->getRelationDetailsFromInput($data);

// omit the n-n relationships when updating the eloquent item
$nn_relationships = Arr::pluck($this->getRelationFieldsWithPivot(), 'name');
// handle the creation of the model relations.
$this->createRelationsForItem($item, $relation_data);

$data = Arr::except($data, $nn_relationships);
$field_names_to_exclude = $this->getFieldNamesArray($this->relationFieldsWithoutRelationType('BelongsTo', true));

$updated = $item->update($data);
$updated = $item->update(Arr::except($data, $field_names_to_exclude));

return $item;
}
Expand Down
Loading