Skip to content

Commit

Permalink
Merge pull request #5538 from Laravel-Backpack/belongs-to-many-save-d…
Browse files Browse the repository at this point in the history
…uplicate-pivots

Allow BelongsToMany/MorphToMany to store duplicate pivots
  • Loading branch information
pxpm authored Jun 12, 2024
2 parents cca35da + c42c48e commit 01270cd
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 15 deletions.
69 changes: 55 additions & 14 deletions src/app/Library/CrudPanel/Traits/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Backpack\CRUD\app\Library\CrudPanel\Traits;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;

Expand All @@ -18,7 +20,7 @@ trait Create
* Insert a row in the database.
*
* @param array $input All input values to be inserted.
* @return \Illuminate\Database\Eloquent\Model
* @return Model
*/
public function create($input)
{
Expand Down Expand Up @@ -95,7 +97,7 @@ public function getRelationFields($fields = [])
/**
* Create relations for the provided model.
*
* @param \Illuminate\Database\Eloquent\Model $item The current CRUD model.
* @param Model $item The current CRUD model.
* @param array $formattedRelations The form data.
* @return bool|null
*/
Expand Down Expand Up @@ -131,34 +133,73 @@ private function createRelationsForItem($item, $formattedRelations)
case 'BelongsToMany':
case 'MorphToMany':
$values = $relationDetails['values'][$relationMethod] ?? [];
$values = is_string($values) ? json_decode($values, true) : $values;
$values = is_string($values) ? (json_decode($values, true) ?? []) : $values;
$field = $relationDetails['crudFields'][0] ?? [];

// disabling ConvertEmptyStringsToNull middleware may return null from json_decode() if an empty string is used.
// we need to make sure no null value can go foward so we reassure that values is not null after json_decode()
$values = $values ?? [];
// if the values are multidimensional, we have additional pivot data.
if (is_array($values) && is_multidimensional_array($values)) {
// if the field allow duplicated pivots, we can't use sync or attach from laravel, we need to manually handle the pivot data.
if ($field['allow_duplicate_pivots'] ?? false) {
$keyName = $field['pivot_key_name'] ?? 'id';
$sentIds = array_filter(array_column($values, $keyName));
$dbValues = $relation->newPivotQuery()->pluck($keyName)->toArray();

$toDelete = array_diff($dbValues, $sentIds);

if (! empty($toDelete)) {
foreach ($toDelete as $id) {
$relation->newPivot()->where($keyName, $id)->delete();
}
}
foreach ($values as $value) {
// if it's an existing pivot, update it
$attributes = $this->preparePivotAttributesForSave($value, $relation, $item->getKey(), $keyName);
if (isset($value[$keyName])) {
$relation->newPivot()->where($keyName, $value[$keyName])->update($attributes);
} else {
$relation->newPivot()->create($attributes);
}
}
break;
}

$relationValues = [];
$relationToManyValues = [];

if (is_array($values) && is_multidimensional_array($values)) {
foreach ($values as $value) {
if (isset($value[$relationMethod])) {
$relationValues[$value[$relationMethod]] = Arr::except($value, $relationMethod);
$relationToManyValues[$value[$relationMethod]] = Arr::except($value, $relationMethod);
}
}

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

// 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($relationValues)) {
$relationValues = array_values($values);
// an array of keys with no additional pivot data. sync those.
if (empty($relationToManyValues)) {
$relationToManyValues = array_values($values);
}

$item->{$relationMethod}()->sync($relationValues);
$item->{$relationMethod}()->sync($relationToManyValues);
unset($relationToManyValues);
break;
}
}
}

private function preparePivotAttributesForSave(array $attributes, BelongsToMany|MorphToMany $relation, string|int $relatedItemKey, $pivotKeyName)
{
$attributes[$relation->getForeignPivotKeyName()] = $relatedItemKey;
$attributes[$relation->getRelatedPivotKeyName()] = $attributes[$relation->getRelationName()];

if ($relation instanceof MorphToMany) {
$attributes[$relation->getMorphType()] = $relation->getMorphClass();
}

return Arr::except($attributes, [$relation->getRelationName(), $pivotKeyName]);
}

/**
* Save the attributes of a given HasOne or MorphOne relationship on the
* related entry, create or delete it, depending on what was sent in the form.
Expand Down
11 changes: 11 additions & 0 deletions src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,17 @@ protected function makeSureSubfieldsHaveNecessaryAttributes($field)
break;
}

if ($field['allow_duplicate_pivots'] ?? false) {
$pivotSelectorField['allow_duplicate_pivots'] = true;
$field['subfields'] = Arr::prepend($field['subfields'], [
'name' => $field['pivot_key_name'] ?? 'id',
'type' => 'hidden',
'wrapper' => [
'class' => 'd-none',
],
]);
}

$this->setupFieldValidation($pivotSelectorField, $field['name']);
$field['subfields'] = Arr::prepend($field['subfields'], $pivotSelectorField);

Expand Down
Loading

0 comments on commit 01270cd

Please sign in to comment.