Skip to content

Commit

Permalink
Merge pull request #5243 from Laravel-Backpack/handle-uploads-inside-…
Browse files Browse the repository at this point in the history
…relationships

Handle uploads inside relationships
  • Loading branch information
pxpm authored Aug 11, 2023
2 parents bc15b25 + b299410 commit 31927e8
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 22 deletions.
19 changes: 19 additions & 0 deletions src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,32 @@ protected function makeSureSubfieldsHaveNecessaryAttributes($field)
case 'MorphToMany':
case 'BelongsToMany':
$pivotSelectorField = static::getPivotFieldStructure($field);

$pivot = Arr::where($field['subfields'], function ($item) use ($pivotSelectorField) {
return $item['name'] === $pivotSelectorField['name'];
});

if (! empty($pivot)) {
break;
}

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

break;
case 'MorphMany':
case 'HasMany':
$entity = isset($field['baseEntity']) ? $field['baseEntity'].'.'.$field['entity'] : $field['entity'];
$relationInstance = $this->getRelationInstance(['entity' => $entity]);

$localKeyField = Arr::where($field['subfields'], function ($item) use ($relationInstance) {
return $item['name'] === $relationInstance->getRelated()->getKeyName();
});

if (! empty($localKeyField)) {
break;
}

$field['subfields'] = Arr::prepend($field['subfields'], [
'name' => $relationInstance->getRelated()->getKeyName(),
'type' => 'hidden',
Expand Down
2 changes: 1 addition & 1 deletion src/app/Library/Uploaders/SingleFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function uploadFiles(Model $entry, $value = null)
return $this->getPath().$fileName;
}

if (! $value && CrudPanelFacade::getRequest()->has($this->getName()) && $previousFile) {
if (! $value && CrudPanelFacade::getRequest()->has($this->getRepeatableContainerName() ?? $this->getName()) && $previousFile) {
Storage::disk($this->getDisk())->delete($previousFile);

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public function relationship(bool $isRelation): self;
*/
public function getName(): string;

public function getAttributeName(): string;

public function getDisk(): string;

public function getPath(): string;
Expand All @@ -53,5 +55,7 @@ public function shouldDeleteFiles(): bool;

public function canHandleMultipleFiles(): bool;

public function isRelationship(): bool;

public function getPreviousFiles(Model $entry): mixed;
}
38 changes: 35 additions & 3 deletions src/app/Library/Uploaders/Support/RegisterUploadEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD;
use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface;
use Exception;
use Illuminate\Database\Eloquent\Relations\Pivot;

final class RegisterUploadEvents
{
Expand Down Expand Up @@ -46,6 +47,10 @@ private function registerEvents(array|null $subfield = [], ?bool $registerModelE
$model = $attributes['model'] ?? get_class($this->crudObject->crud()->getModel());
$uploader = $this->getUploader($attributes, $this->uploaderConfiguration);

if (isset($attributes['relation_type']) && $attributes['entity'] !== false) {
$uploader = $uploader->relationship(true);
}

$this->setupModelEvents($model, $uploader);
$this->setupUploadConfigsInCrudObject($uploader);
}
Expand All @@ -63,12 +68,13 @@ private function registerSubfieldEvent(array $subfield, bool $registerModelEvent
return;
}

$model = $subfield['baseModel'] ?? get_class($this->crudObject->crud()->getModel());

if (isset($crudObject['relation_type']) && $crudObject['entity'] !== false) {
$uploader = $uploader->relationship(true);
$subfield['relation_type'] = $crudObject['relation_type'];
}

$model = $this->getSubfieldModel($subfield, $uploader);

// only the last subfield uploader will setup the model events for the whole group
if ($registerModelEvents) {
$this->setupModelEvents($model, $uploader);
Expand Down Expand Up @@ -120,7 +126,11 @@ private function setupModelEvents(string $model, UploaderInterface $uploader): v
if (app('crud')->entry) {
app('crud')->entry = $uploader->retrieveUploadedFiles(app('crud')->entry);
} else {
$model::retrieved(function ($entry) use ($uploader) {
// the retrieve model may differ from the deleting and saving models because retrieved event
// is not called in pivot models when loading the relations.
$retrieveModel = $this->getModelForRetrieveEvent($model, $uploader);

$retrieveModel::retrieved(function ($entry) use ($uploader) {
if ($entry->translationEnabled()) {
$locale = request('_locale', \App::getLocale());
if (in_array($locale, array_keys($entry->getAvailableLocales()))) {
Expand Down Expand Up @@ -172,4 +182,26 @@ private function setupUploadConfigsInCrudObject(UploaderInterface $uploader): vo
{
$this->crudObject->upload(true)->disk($uploader->getDisk())->prefix($uploader->getPath());
}

private function getSubfieldModel(array $subfield, UploaderInterface $uploader)
{
if (! $uploader->isRelationship()) {
return $subfield['baseModel'] ?? get_class(app('crud')->getModel());
}

if (in_array($subfield['relation_type'], ['BelongsToMany', 'MorphToMany'])) {
return app('crud')->getModel()->{$subfield['baseEntity']}()->getPivotClass();
}

return $subfield['baseModel'];
}

private function getModelForRetrieveEvent(string $model, UploaderInterface $uploader)
{
if (! $uploader->isRelationship()) {
return $model;
}

return is_a($model, Pivot::class, true) ? $this->crudObject->getAttributes()['model'] : $model;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ private function uploadRelationshipFiles(Model $entry, mixed $value): Model
$value = $value->slice($modelCount, 1)->toArray();

foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
if (array_key_exists($modelCount, $value) && isset($value[$modelCount][$uploader->getName()])) {
$entry->{$uploader->getName()} = $uploader->uploadFiles($entry, $value[$modelCount][$uploader->getName()]);
if (array_key_exists($modelCount, $value) && array_key_exists($uploader->getAttributeName(), $value[$modelCount])) {
$entry->{$uploader->getAttributeName()} = $uploader->uploadFiles($entry, $value[$modelCount][$uploader->getAttributeName()]);
}
}

Expand All @@ -74,10 +74,10 @@ private function uploadRelationshipFiles(Model $entry, mixed $value): Model
protected function processRepeatableUploads(Model $entry, Collection $values): Collection
{
foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) {
$uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader));
$uploadedValues = $uploader->uploadRepeatableFiles($values->pluck($uploader->getAttributeName())->toArray(), $this->getPreviousRepeatableValues($entry, $uploader));

$values = $values->map(function ($item, $key) use ($uploadedValues, $uploader) {
$item[$uploader->getName()] = $uploadedValues[$key] ?? null;
$item[$uploader->getAttributeName()] = $uploadedValues[$key] ?? null;

return $item;
});
Expand All @@ -89,7 +89,7 @@ protected function processRepeatableUploads(Model $entry, Collection $values): C
private function retrieveRepeatableFiles(Model $entry): Model
{
if ($this->isRelationship) {
return $this->retrieveFiles($entry);
return $this->retrieveRepeatableRelationFiles($entry);
}

$repeatableUploaders = app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName());
Expand All @@ -98,7 +98,7 @@ private function retrieveRepeatableFiles(Model $entry): Model
$values = is_string($values) ? json_decode($values, true) : $values;
$values = array_map(function ($item) use ($repeatableUploaders) {
foreach ($repeatableUploaders as $upload) {
$item[$upload->getName()] = $this->getValuesWithPathStripped($item, $upload);
$item[$upload->getAttributeName()] = $this->getValuesWithPathStripped($item, $upload);
}

return $item;
Expand All @@ -109,10 +109,48 @@ private function retrieveRepeatableFiles(Model $entry): Model
return $entry;
}

private function retrieveRepeatableRelationFiles(Model $entry)
{
switch($this->getRepeatableRelationType()) {
case 'BelongsToMany':
case 'MorphToMany':
$pivotClass = app('crud')->getModel()->{$this->getUploaderSubfield()['baseEntity']}()->getPivotClass();
$pivotFieldName = 'pivot_'.$this->getAttributeName();
$connectedEntry = new $pivotClass([$this->getAttributeName() => $entry->$pivotFieldName]);
$entry->{$pivotFieldName} = $this->retrieveFiles($connectedEntry)->{$this->getAttributeName()};

break;
default:
$entry = $this->retrieveFiles($entry);
}

return $entry;
}

private function getRepeatableRelationType()
{
return $this->getUploaderField()->getAttributes()['relation_type'];
}

private function getUploaderField()
{
return app('crud')->field($this->getRepeatableContainerName());
}

private function getUploaderSubfield()
{
return collect($this->getUploaderFieldSubfields())->where('name', '===', $this->getName())->first();
}

private function getUploaderFieldSubfields()
{
return $this->getUploaderField()->getAttributes()['subfields'];
}

private function deleteRepeatableFiles(Model $entry): void
{
if ($this->isRelationship) {
$this->deleteFiles($entry);
$this->deleteRelationshipFiles($entry);

return;
}
Expand Down Expand Up @@ -199,4 +237,42 @@ private function getValuesWithPathStripped(array|string|null $item, UploaderInte

return isset($uploadedValues) ? Str::after($uploadedValues, $upload->getPath()) : null;
}

private function deleteRelationshipFiles(Model $entry): void
{
if (in_array($this->getRepeatableRelationType(), ['BelongsToMany', 'MorphToMany'])) {
$pivotAttributes = $entry->getAttributes();
$connectedPivot = $entry->pivotParent->{$this->getRepeatableContainerName()}->where(function ($item) use ($pivotAttributes) {
$itemPivotAttributes = $item->pivot->only(array_keys($pivotAttributes));

return $itemPivotAttributes === $pivotAttributes;
})->first();

if (! $connectedPivot) {
return;
}

$files = $connectedPivot->getOriginal()['pivot_'.$this->getAttributeName()];

if (! $files) {
return;
}

if (is_array($files)) {
foreach ($files as $value) {
$value = Str::start($value, $this->getPath());
Storage::disk($this->getDisk())->delete($value);
}

return;
}

$value = Str::start($files, $this->getPath());
Storage::disk($this->getDisk())->delete($value);

return;
}

$this->deleteFiles($entry);
}
}
32 changes: 21 additions & 11 deletions src/app/Library/Uploaders/Uploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ public function storeUploadedFiles(Model $entry): Model
if ($this->attachedToFakeField) {
$fakeFieldValue = $entry->{$this->attachedToFakeField};
$fakeFieldValue = is_string($fakeFieldValue) ? json_decode($fakeFieldValue, true) : (array) $fakeFieldValue;
$fakeFieldValue[$this->getName()] = $this->uploadFiles($entry);
$fakeFieldValue[$this->getAttributeName()] = $this->uploadFiles($entry);

$entry->{$this->attachedToFakeField} = isset($entry->getCasts()[$this->attachedToFakeField]) ? $fakeFieldValue : json_encode($fakeFieldValue);

return $entry;
}

$entry->{$this->getName()} = $this->uploadFiles($entry);
$entry->{$this->getAttributeName()} = $this->uploadFiles($entry);

return $entry;
}
Expand Down Expand Up @@ -118,6 +118,11 @@ public function getName(): string
return $this->name;
}

public function getAttributeName(): string
{
return Str::afterLast($this->name, '.');
}

public function getDisk(): string
{
return $this->disk;
Expand Down Expand Up @@ -157,6 +162,11 @@ public function canHandleMultipleFiles(): bool
return $this->handleMultipleFiles;
}

public function isRelationship(): bool
{
return $this->isRelationship;
}

public function getPreviousFiles(Model $entry): mixed
{
if (! $this->attachedToFakeField) {
Expand All @@ -166,7 +176,7 @@ public function getPreviousFiles(Model $entry): mixed
$value = $this->getOriginalValue($entry, $this->attachedToFakeField);
$value = is_string($value) ? json_decode($value, true) : (array) $value;

return $value[$this->getName()] ?? null;
return $value[$this->getAttributeName()] ?? null;
}

/*******************************
Expand Down Expand Up @@ -195,11 +205,11 @@ public function uploadFiles(Model $entry, $values = null)

private function retrieveFiles(Model $entry): Model
{
$value = $entry->{$this->name};
$value = $entry->{$this->getAttributeName()};

if ($this->handleMultipleFiles) {
if (! isset($entry->getCasts()[$this->name]) && is_string($value)) {
$entry->{$this->name} = json_decode($value, true);
if (! isset($entry->getCasts()[$this->getName()]) && is_string($value)) {
$entry->{$this->getAttributeName()} = json_decode($value, true);
}

return $entry;
Expand All @@ -209,13 +219,13 @@ private function retrieveFiles(Model $entry): Model
$values = $entry->{$this->attachedToFakeField};
$values = is_string($values) ? json_decode($values, true) : (array) $values;

$values[$this->name] = isset($values[$this->name]) ? Str::after($values[$this->name], $this->path) : null;
$values[$this->getAttributeName()] = isset($values[$this->getAttributeName()]) ? Str::after($values[$this->getAttributeName()], $this->path) : null;
$entry->{$this->attachedToFakeField} = json_encode($values);

return $entry;
}

$entry->{$this->name} = Str::after($value, $this->path);
$entry->{$this->getAttributeName()} = Str::after($value, $this->path);

return $entry;
}
Expand All @@ -242,7 +252,7 @@ private function deleteFiles(Model $entry)

private function performFileDeletion(Model $entry)
{
if ($this->isRelationship || ! $this->handleRepeatableFiles) {
if (! $this->handleRepeatableFiles) {
$this->deleteFiles($entry);

return;
Expand All @@ -263,13 +273,13 @@ private function getPathFromConfiguration(array $crudObject, array $configuratio

private function getOriginalValue(Model $entry, $field = null)
{
$previousValue = $entry->getOriginal($field ?? $this->getName());
$previousValue = $entry->getOriginal($field ?? $this->getAttributeName());

if (! $previousValue) {
return $previousValue;
}

if ($entry->translationEnabled()) {
if (method_exists($entry, 'translationEnabled') && $entry->translationEnabled()) {
return $previousValue[$entry->getLocale()] ?? null;
}

Expand Down

0 comments on commit 31927e8

Please sign in to comment.