Skip to content

Commit

Permalink
[7.x] Refactor the HasMany fieldtype (#556)
Browse files Browse the repository at this point in the history
Co-authored-by: duncanmcclean <duncanmcclean@users.noreply.github.com>
  • Loading branch information
duncanmcclean and duncanmcclean authored Jul 12, 2024
1 parent e738e51 commit 8c4aa0a
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 334 deletions.
124 changes: 9 additions & 115 deletions src/Fieldtypes/HasManyFieldtype.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
namespace StatamicRadPack\Runway\Fieldtypes;

use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Statamic\Facades\Blink;
use Statamic\Facades\GraphQL;
use StatamicRadPack\Runway\Runway;

Expand Down Expand Up @@ -47,125 +46,20 @@ protected function configFieldItems(): array
return array_merge(parent::configFieldItems(), $config);
}

// Pre-process the data before it gets sent to the publish page
/**
* Pre-process the values before they get sent to the publish form.
*
* @return array
*/
public function preProcess($data)
{
$resource = Runway::findResource($this->config('resource'));

// Determine whether or not this field is on a resource or a collection
$resourceHandle = request()->route('resource');

if (! $resourceHandle) {
return Arr::wrap($data);
}

return collect($data)
->pluck($resource->primaryKey())
->toArray();
}

// Process the data before it gets saved
public function process($data)
{
// Determine whether or not this field is on a resource or a collection
$resource = request()->route('resource');

if (Blink::get('RunwayRouteResource')) {
$resource = Runway::findResource(Blink::get('RunwayRouteResource'));
}

if (! $resource) {
return $data;
}

$model = $resource->model()->firstWhere(
$resource->routeKey(),
request()->route('model') ?? Blink::get('RunwayRouteModel')
);

// If we're adding HasMany relations on a model that doesn't exist yet,
// return a closure that will be run post-save.
if (! $model) {
return function ($resource, $model) use ($data) {
$relatedResource = Runway::findResource($this->config('resource'));
$relatedField = $model->{$this->field()->handle()}();

// Many to many relation
if ($relatedField instanceof BelongsToMany) {
$model->{$this->field()->handle()}()->sync($data);
} else {
// Add anything new
collect($data)
->each(function ($relatedId) use ($model, $relatedResource, $relatedField) {
$relatedModel = $relatedResource->model()->find($relatedId);

$relatedModel->update([
$relatedField->getForeignKeyName() => $model->{$relatedResource->primaryKey()},
]);
});
}
};
}

$deleted = [];
$relatedResource = Runway::findResource($this->config('resource'));
$relatedField = $model->{$this->field()->handle()}();

// Many to many relation
if ($relatedField instanceof BelongsToMany) {
// When Reordering is enabled, we need to change the format of the $data array. The key should
// be the foriegn key and the value should be pivot data (our sort order).
if ($this->config('reorderable') && $orderColumn = $this->config('order_column')) {
$data = collect($data)
->mapWithKeys(function ($relatedId, $index) use ($orderColumn) {
return [$relatedId => [$orderColumn => $index]];
})
->toArray();
}

$model->{$this->field()->handle()}()->sync($data);

return null;
}

// Delete any deleted models
collect($relatedField->get())
->reject(fn ($relatedModel) => in_array($relatedModel->id, $data))
->each(function ($relatedModel) use ($relatedResource, &$deleted) {
$deleted[] = $relatedModel->{$relatedResource->primaryKey()};

$relatedModel->delete();
});

// Add anything new
collect($data)
->reject(fn ($relatedId) => $relatedField->get()->pluck($relatedResource->primaryKey())->contains($relatedId))
->reject(fn ($relatedId) => in_array($relatedId, $deleted))
->each(function ($relatedId) use ($model, $relatedResource, $relatedField) {
$relatedModel = $relatedResource->model()->find($relatedId);

$relatedModel->update([
$relatedField->getForeignKeyName() => $model->{$relatedResource->primaryKey()},
]);
});

// If reordering is enabled, update all models with their new sort order.
if ($this->config('reorderable') && $orderColumn = $this->config('order_column')) {
collect($data)
->each(function ($relatedId, $index) use ($relatedResource, $orderColumn) {
$relatedModel = $relatedResource->model()->find($relatedId);

if ($relatedModel->{$orderColumn} === $index) {
return;
}

$relatedModel->update([
$orderColumn => $index,
]);
});
if (collect($data)->every(fn ($item) => $item instanceof Model)) {
return collect($data)->pluck($resource->primaryKey())->all();
}

return null;
return Arr::wrap($data);
}

public function preload()
Expand Down
31 changes: 17 additions & 14 deletions src/Http/Controllers/CP/ResourceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

namespace StatamicRadPack\Runway\Http\Controllers\CP;

use Illuminate\Support\Facades\DB;
use Statamic\CP\Breadcrumbs;
use Statamic\CP\Column;
use Statamic\Exceptions\NotFoundHttpException;
use Statamic\Facades\Action;
use Statamic\Facades\Scope;
use Statamic\Facades\User;
use Statamic\Fields\Field;
use Statamic\Http\Controllers\CP\CpController;
use StatamicRadPack\Runway\Fieldtypes\HasManyFieldtype;
use StatamicRadPack\Runway\Http\Requests\CP\CreateRequest;
use StatamicRadPack\Runway\Http\Requests\CP\EditRequest;
use StatamicRadPack\Runway\Http\Requests\CP\IndexRequest;
use StatamicRadPack\Runway\Http\Requests\CP\StoreRequest;
use StatamicRadPack\Runway\Http\Requests\CP\UpdateRequest;
use StatamicRadPack\Runway\Http\Resources\CP\Model as ModelResource;
use StatamicRadPack\Runway\Relationships;
use StatamicRadPack\Runway\Resource;

class ResourceController extends CpController
Expand Down Expand Up @@ -102,11 +102,6 @@ public function store(StoreRequest $request, Resource $resource)

$model = $resource->model();

$postCreatedHooks = $resource->blueprint()->fields()->all()
->filter(fn (Field $field) => $field->fieldtype() instanceof HasManyFieldtype)
->map(fn (Field $field) => $field->fieldtype()->process($request->get($field->handle())))
->values();

$this->prepareModelForSaving($resource, $model, $request);

if ($resource->revisionsEnabled()) {
Expand All @@ -115,12 +110,13 @@ public function store(StoreRequest $request, Resource $resource)
'user' => User::current(),
]);
} else {
$saved = $model->save();
}
$saved = DB::transaction(function () use ($model, $request) {
$model->save();
Relationships::for($model)->with($request->all())->save();

// Runs anything in the $postCreatedHooks array. See HasManyFieldtype@process for an example
// of where this is used.
$postCreatedHooks->each(fn ($postCreatedHook) => $postCreatedHook($resource, $model));
return true;
});
}

return [
'data' => (new ModelResource($model->fresh()))->resolve()['data'],
Expand Down Expand Up @@ -204,13 +200,20 @@ public function update(UpdateRequest $request, Resource $resource, $model)

$model = $model->fromWorkingCopy();
} else {
$saved = $model->save();
$saved = DB::transaction(function () use ($model, $request) {
$model->save();
Relationships::for($model)->with($request->all())->save();

return true;
});

$model->refresh();
}

[$values] = $this->extractFromFields($model, $resource, $resource->blueprint());

return [
'data' => array_merge((new ModelResource($model->fresh()))->resolve()['data'], [
'data' => array_merge((new ModelResource($model))->resolve()['data'], [
'values' => $values,
]),
'saved' => $saved,
Expand Down
23 changes: 16 additions & 7 deletions src/Http/Controllers/CP/Traits/PreparesModels.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Statamic\Fields\Field;
use Statamic\Fieldtypes\Section;
use Statamic\Support\Arr;
use StatamicRadPack\Runway\Fieldtypes\BelongsToFieldtype;
use StatamicRadPack\Runway\Fieldtypes\HasManyFieldtype;
use StatamicRadPack\Runway\Resource;
Expand Down Expand Up @@ -62,13 +62,20 @@ protected function prepareModelForPublishForm(Resource $resource, Model $model):
}
}

// HasMany fieldtype: when reordering is enabled, we need to ensure the models are returned in the correct order.
if ($field->fieldtype() instanceof HasManyFieldtype && $field->get('reorderable', false)) {
$orderColumn = $field->get('order_column');
if ($field->fieldtype() instanceof HasManyFieldtype) {
// Use IDs from the model's $runwayRelationships property, if there are any.
if (array_key_exists($field->handle(), $model->runwayRelationships)) {
$value = Arr::get($model->runwayRelationships, $field->handle());
}

// When re-ordering is enabled, ensure the models are returned in the correct order.
if ($field->get('reorderable', false)) {
$orderColumn = $field->get('order_column');

$value = $model->{$field->handle()}()
->reorder($orderColumn, 'ASC')
->get();
$value = $model->{$field->handle()}()
->reorder($orderColumn, 'ASC')
->get();
}
}

return [$field->handle() => $value];
Expand All @@ -91,6 +98,8 @@ protected function prepareModelForSaving(Resource $resource, Model &$model, Requ
$processedValue = $field->fieldtype()->process($request->get($field->handle()));

if ($field->fieldtype() instanceof HasManyFieldtype) {
$model->runwayRelationships[$field->handle()] = $processedValue;

return;
}

Expand Down
77 changes: 77 additions & 0 deletions src/Relationships.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace StatamicRadPack\Runway;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Statamic\Fields\Field;
use StatamicRadPack\Runway\Fieldtypes\HasManyFieldtype;

class Relationships
{
public function __construct(protected Model $model, protected array $values = []) {}

public static function for(Model $model): self
{
return new static($model);
}

public function with(array $values): self
{
$this->values = $values;

return $this;
}

public function save(): void
{
$this->model->runwayResource()->blueprint()->fields()->all()
->filter(fn (Field $field) => $field->fieldtype() instanceof HasManyFieldtype)
->each(function (Field $field): void {
$relationshipName = $this->model->runwayResource()->eloquentRelationships()->get($field->handle());

match (get_class($this->model->{$relationshipName}())) {
HasMany::class => $this->saveHasManyRelationship($field, $this->values[$field->handle()] ?? []),
BelongsToMany::class => $this->saveBelongsToManyRelationship($field, $this->values[$field->handle()] ?? []),
};
});
}

protected function saveHasManyRelationship(Field $field, array $values): void
{
/** @var HasMany $relationship */
$relationship = $this->model->{$field->handle()}();
$relatedResource = Runway::findResource($field->fieldtype()->config('resource'));

$deleted = $relationship->whereNotIn($relatedResource->primaryKey(), $values)->get()
->each->delete()
->map->getKey()
->all();

$models = $relationship->get();

collect($values)
->reject(fn ($id) => $models->pluck($relatedResource->primaryKey())->contains($id))
->reject(fn ($id) => in_array($id, $deleted))
->each(fn ($id) => $relatedResource->model()->find($id)->update([
$relationship->getForeignKeyName() => $this->model->getKey(),
]));

if ($field->fieldtype()->config('reorderable') && $orderColumn = $field->fieldtype()->config('order_column')) {
collect($values)
->map(fn ($id) => $relatedResource->model()->find($id))
->reject(fn (Model $model, int $index) => $model->getAttribute($orderColumn) === $index)
->each(fn (Model $model, int $index) => $model->update([$orderColumn => $index]));
}
}

protected function saveBelongsToManyRelationship(Field $field, array $values): void
{
if ($field->fieldtype()->config('reorderable') && $orderColumn = $field->fieldtype()->config('order_column')) {
$values = collect($values)->mapWithKeys(fn ($id, $index) => [$id => [$orderColumn => $index]])->all();
}

$this->model->{$field->handle()}()->sync($values);
}
}
Loading

0 comments on commit 8c4aa0a

Please sign in to comment.