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

[7.x] Refactor the HasMany fieldtype #556

Merged
merged 17 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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