diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c0111d --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea/ +vendor/ +node_modules/ +.DS_Store +.composer.lock +composer.lock +.phpunit.result.cache +src/public/packages/ +/.phpunit.cache \ No newline at end of file diff --git a/composer.json b/composer.json index 659a5dc..2cd84cd 100644 --- a/composer.json +++ b/composer.json @@ -14,18 +14,24 @@ "Laravel", "Backpack", "Backpack for Laravel", "Backpack Addon", "spatie medialibrary uploaders" ], "require": { - "backpack/crud": "^6.0", + "backpack/crud": "^6.0|dev-next", "spatie/laravel-medialibrary": "^10.7|^11.3" }, "require-dev": { - "phpunit/phpunit": "^9.0|^10.0", - "orchestra/testbench": "~6|^8.0" + "phpunit/phpunit": "^10.0|^11.0", + "orchestra/testbench": "^8.0|^9.0|^10.0" }, "autoload": { "psr-4": { "Backpack\\MediaLibraryUploaders\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Backpack\\MediaLibraryUploaders\\Tests\\": "tests", + "Backpack\\CRUD\\Tests\\": "vendor/backpack/crud/tests" + } + }, "scripts": { "test": "vendor/bin/phpunit --testdox" }, diff --git a/phpunit.xml b/phpunit.xml index ce34605..1d55086 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,22 +1,25 @@ - - - - ./tests/ - - - - - src/ - - + + + + ./tests/Feature + + + + + + + + + + + + + + + + + src/ + + diff --git a/readme.md b/readme.md index 4d471dd..3673045 100644 --- a/readme.md +++ b/readme.md @@ -12,11 +12,11 @@ More exactly, it provides the `->withMedia()` helper, that will handle the file ## Requirements -**Install and use `spatie/laravel-medialibrary` v10**. If you haven't already, please make sure you've installed `spatie/laravel-medialibrary` and followed all installation steps in [their docs](https://spatie.be/docs/laravel-medialibrary/v10/installation-setup): +**Install and use `spatie/laravel-medialibrary` v10|v11**. If you haven't already, please make sure you've installed `spatie/laravel-medialibrary` and followed all installation steps in [their docs](https://spatie.be/docs/laravel-medialibrary/v11/installation-setup): ``` bash # require the package -composer require "spatie/laravel-medialibrary:^10.0.0" +composer require "spatie/laravel-medialibrary:^11.0" # prepare the database # NOTE: Spatie migration does not come with a `down()` method by default, add one now if you need it @@ -33,7 +33,7 @@ php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServicePr ``` -Then prepare your Models to use `spatie/laravel-medialibrary`, by adding the `InteractsWithMedia` trait to your model and implement the `HasMedia` interface like explained on [Media Library Documentation](https://spatie.be/docs/laravel-medialibrary/v10/basic-usage/preparing-your-model). +Then prepare your Models to use `spatie/laravel-medialibrary`, by adding the `InteractsWithMedia` trait to your model and implement the `HasMedia` interface like explained on [Media Library Documentation](https://spatie.be/docs/laravel-medialibrary/v11/basic-usage/preparing-your-model). ## Installation @@ -100,11 +100,11 @@ CRUD::field('main_image') ]); ``` -**NOTE:** Some methods will be called automatically by Backpack; You shoudn't call them inside the closure used for configuration: `toMediaCollection()`, `setName()`, `usingName()`, `setOrder()`, `toMediaCollectionFromRemote()` and `toMediaLibrary()`. They will throw an error if you manually try to call them in the closure. +**NOTE:** Some methods will be called automatically by Backpack; You shouldn't call them inside the closure used for configuration: `toMediaCollection()`, `setName()`, `usingName()`, `setOrder()`, `toMediaCollectionFromRemote()` and `toMediaLibrary()`. They will throw an error if you manually try to call them in the closure. ### Defining media collection in the model -You can also have the collection configured in your model as explained in [Spatie Documentation](https://spatie.be/docs/laravel-medialibrary/v10/working-with-media-collections/defining-media-collections), in that case, you just need to pass the `collection` configuration key. But you are still able to configure all the other options including the `whenSaving` callback. +You can also have the collection configured in your model as explained in [Spatie Documentation](https://spatie.be/docs/laravel-medialibrary/v11/working-with-media-collections/defining-media-collections), in that case, you just need to pass the `collection` configuration key. But you are still able to configure all the other options including the `whenSaving` callback. ```php // In your Model.php @@ -151,7 +151,7 @@ CRUD::field('main_image') 'displayConversions' => 'thumb' ]); -// you can also configure aditional manipulations in the `whenSaving` callback +// you can also configure additional manipulations in the `whenSaving` callback ->withMedia([ 'displayConversions' => 'thumb', 'whenSaving' => function($media) { @@ -183,7 +183,7 @@ You can normally assign custom properties to your media with `->withCustomProper ## Change log -Changes are documented here on Github. Please see the [Releases tab](https://github.com/backpack/media-library-connector/releases). +Changes are documented here on Github. Please see the [Releases tab](https://github.com/Laravel-Backpack/medialibrary-uploaders/releases). ## Testing diff --git a/src/AddonServiceProvider.php b/src/AddonServiceProvider.php index f372134..2dc14b0 100644 --- a/src/AddonServiceProvider.php +++ b/src/AddonServiceProvider.php @@ -5,7 +5,7 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudColumn; use Backpack\CRUD\app\Library\CrudPanel\CrudField; use Backpack\CRUD\app\Library\Uploaders\Support\RegisterUploadEvents; -use Backpack\MediaLibraryUploaders\Uploaders\MediaAjaxUploader; +use Backpack\MediaLibraryUploaders\Uploaders\MediaDropzoneUploader; use Backpack\MediaLibraryUploaders\Uploaders\MediaMultipleFiles; use Backpack\MediaLibraryUploaders\Uploaders\MediaSingleBase64Image; use Backpack\MediaLibraryUploaders\Uploaders\MediaSingleFile; @@ -30,14 +30,21 @@ public function boot() 'image' => MediaSingleBase64Image::class, 'upload' => MediaSingleFile::class, 'upload_multiple' => MediaMultipleFiles::class, - 'dropzone' => MediaAjaxUploader::class, ], 'withMedia'); + if (class_exists(\Backpack\Pro\Uploads\BackpackAjaxUploader::class)) { + app('UploadersRepository')->addUploaderClasses([ + 'dropzone' => MediaDropzoneUploader::class, + ], 'withMedia'); + } + // register media upload macros on crud fields and columns. if (! CrudField::hasMacro('withMedia')) { CrudField::macro('withMedia', function ($uploadDefinition = [], $subfield = null, $registerEvents = true) { - $uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : []; /** @var CrudField $this */ + $uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : []; + $this->setAttributeValue('withMedia', $uploadDefinition); + $this->save(); RegisterUploadEvents::handle($this, $uploadDefinition, 'withMedia', $subfield, $registerEvents); return $this; @@ -46,8 +53,10 @@ public function boot() if (! CrudColumn::hasMacro('withMedia')) { CrudColumn::macro('withMedia', function ($uploadDefinition = [], $subfield = null, $registerEvents = true) { - $uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : []; /** @var CrudColumn $this */ + $uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : []; + $this->setAttributeValue('withMedia', $uploadDefinition); + $this->save(); RegisterUploadEvents::handle($this, $uploadDefinition, 'withMedia', $subfield, $registerEvents); return $this; diff --git a/src/Uploaders/MediaAjaxUploader.php b/src/Uploaders/MediaAjaxUploader.php index ec247a7..b5ffd5c 100644 --- a/src/Uploaders/MediaAjaxUploader.php +++ b/src/Uploaders/MediaAjaxUploader.php @@ -3,48 +3,28 @@ namespace Backpack\MediaLibraryUploaders\Uploaders; use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD; -use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface; -use Illuminate\Database\Eloquent\Model; +use Backpack\Pro\Uploads\BackpackAjaxUploader; use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\File\File; -class MediaAjaxUploader extends MediaUploader +class MediaAjaxUploader extends BackpackAjaxUploader { - public static function for(array $field, $configuration): UploaderInterface + use Traits\IdentifiesMedia; + use Traits\AddMediaToModels; + use Traits\HasConstrainedFileAdder; + use Traits\HasCustomProperties; + use Traits\HasSavingCallback; + use Traits\HasCollections; + use Traits\RetrievesUploadedFiles; + use Traits\HandleRepeatableUploads; + use Traits\DeletesUploadedFiles; + + public function __construct(array $crudObject, array $configuration) { - return (new self($field, $configuration))->multiple(); - } - - public function uploadFiles(Model $entry, $value = null) - { - $temporaryDisk = CRUD::get('dropzone.temporary_disk'); - $temporaryFolder = CRUD::get('dropzone.temporary_folder'); - - $uploads = $value ?? CRUD::getRequest()->input($this->getName()) ?? []; - - $uploads = is_array($uploads) ? $uploads : (json_decode($uploads, true) ?? []); - - $uploadedFiles = array_filter($uploads, function ($value) use ($temporaryFolder, $temporaryDisk) { - return strpos($value, $temporaryFolder) !== false && Storage::disk($temporaryDisk)->exists($value); - }); - - $previousSentFiles = array_filter($uploads, function ($value) use ($temporaryFolder) { - return strpos($value, $temporaryFolder) === false; - }); - - $previousFiles = $this->get($entry); - - foreach ($previousFiles as $previousFile) { - if (! in_array($this->getMediaIdentifier($previousFile, $entry), $previousSentFiles)) { - $previousFile->delete(); - } - } - - foreach ($uploadedFiles as $key => $value) { - $file = new File(Storage::disk($temporaryDisk)->path($value)); - - $this->addMediaFile($entry, $file); - } + parent::__construct($crudObject, $configuration); + $this->mediaName = $configuration['mediaName'] ?? $crudObject['name']; + $this->savingEventCallback = $configuration['whenSaving'] ?? null; + $this->collection = $configuration['collection'] ?? 'default'; } public function uploadRepeatableFiles($values, $previousValues, $entry = null) @@ -83,15 +63,16 @@ public function uploadRepeatableFiles($values, $previousValues, $entry = null) $fileIdentifier = $this->getMediaIdentifier($previousFile, $entry); if (empty($sentFiles)) { $previousFile->delete(); + continue; } - + $foundInSentFiles = false; - foreach($sentFiles as $row => $sentFilesInRow) { + foreach ($sentFiles as $row => $sentFilesInRow) { $fileWasSent = array_search($fileIdentifier, $sentFilesInRow, true); - if($fileWasSent !== false) { + if ($fileWasSent !== false) { $foundInSentFiles = true; - if($row !== $previousFile->getCustomProperty('repeatableRow')) { + if ($row !== $previousFile->getCustomProperty('repeatableRow')) { $previousFile->setCustomProperty('repeatableRow', $row); $previousFile->save(); // avoid checking the same file twice. This is a performance improvement. @@ -102,7 +83,7 @@ public function uploadRepeatableFiles($values, $previousValues, $entry = null) } if ($foundInSentFiles === false) { - $previousFile->delete(); + $previousFile->delete(); } } } diff --git a/src/Uploaders/MediaDropzoneUploader.php b/src/Uploaders/MediaDropzoneUploader.php new file mode 100644 index 0000000..cc18796 --- /dev/null +++ b/src/Uploaders/MediaDropzoneUploader.php @@ -0,0 +1,139 @@ +multiple(); + } + + public function uploadFiles(Model $entry, $value = null) + { + $uploads = $value ?? CRUD::getRequest()->input($this->getName()) ?? []; + + $uploads = is_array($uploads) ? $uploads : (json_decode($uploads, true) ?? []); + + $uploadedFiles = array_filter($uploads, function ($value) { + return strpos($value, $this->temporaryFolder) !== false; + }); + + $previousSentFiles = array_filter($uploads, function ($value) { + return strpos($value, $this->temporaryFolder) === false; + }); + + $previousDatabaseFiles = $this->getPreviousFiles($entry) ?? []; + + foreach ($previousDatabaseFiles as $previousFile) { + if (! in_array($this->getMediaIdentifier($previousFile, $entry), $previousSentFiles)) { + $previousFile->delete(); + } + } + + foreach ($uploadedFiles as $key => $value) { + $file = new UploadedFile($this->temporaryDisk->path($value), $value); + $this->addMediaFile($entry, $file); + } + } + + public function uploadRepeatableFiles($values, $previousValues, $entry = null) + { + $values = array_map(function ($value) { + return is_array($value) ? $value : (json_decode($value, true) ?? []); + }, $values); + + $sentFiles = []; + + foreach ($values as $row => $files) { + $files = is_array($files) ? $files : (json_decode($files, true) ?? []); + + $uploadedFiles = array_filter($files, function ($value) { + return strpos($value, $this->temporaryFolder) !== false; + }); + + $sentFiles = array_merge($sentFiles, [$row => array_filter($files, function ($value) { + return strpos($value, $this->temporaryFolder) === false; + })]); + + foreach ($uploadedFiles ?? [] as $key => $file) { + try { + $file = new UploadedFile($this->temporaryDisk->path($file), $file); + $this->addMediaFile($entry, $file, $row); + } catch (\Throwable $th) { + Log::error($th->getMessage()); + Alert::error('An error occurred uploading files. Check log files.')->flash(); + } + } + } + + foreach ($previousValues as $previousFile) { + $fileIdentifier = $this->getMediaIdentifier($previousFile, $entry); + if (empty($sentFiles)) { + $previousFile->delete(); + continue; + } + + $foundInSentFiles = false; + foreach($sentFiles as $row => $sentFilesInRow) { + $fileWasSent = array_search($fileIdentifier, $sentFilesInRow, true); + if($fileWasSent !== false) { + $foundInSentFiles = true; + if($row !== $previousFile->getCustomProperty('repeatableRow')) { + $previousFile->setCustomProperty('repeatableRow', $row); + $previousFile->save(); + // avoid checking the same file twice. This is a performance improvement. + unset($sentFiles[$row][$fileWasSent]); + break; + } + } + } + + if ($foundInSentFiles === false) { + $previousFile->delete(); + } + } + } + + protected function ajaxEndpointSuccessResponse($files = null): \Illuminate\Http\JsonResponse + { + return $files ? + response()->json(['files' => $files, 'success' => true]) : + response()->json(['success' => true]); + } + + protected function ajaxEndpointErrorMessage(string $message = 'An error occurred while processing the file.'): \Illuminate\Http\JsonResponse + { + return response()->json([ + 'message' => $message, + 'success' => false, + ], 400); + } + + protected function buildAjaxEndpointValidationFilesArray($validationKey, $uploadedFiles, $requestInputName): array + { + $previousUploadedFiles = json_decode(CRUD::getRequest()->input('previousUploadedFiles'), true) ?? []; + + if (Str::contains($validationKey, '.*.')) { + return [ + 'validate_ajax_endpoint' => true, + Str::before($validationKey, '.*') => [ + 0 => [ + Str::after($validationKey, '*.') => array_merge($uploadedFiles[$requestInputName], $previousUploadedFiles), + ], + ], + ]; + } + + return array_merge($uploadedFiles[$requestInputName], $previousUploadedFiles, ['validate_ajax_endpoint' => true]); + } +} diff --git a/src/Uploaders/MediaMultipleFiles.php b/src/Uploaders/MediaMultipleFiles.php index 3a4b232..19d2133 100644 --- a/src/Uploaders/MediaMultipleFiles.php +++ b/src/Uploaders/MediaMultipleFiles.php @@ -74,4 +74,21 @@ public function uploadRepeatableFiles($value, $previousValues, $entry = null) } } } + + protected function hasDeletedFiles($value): bool + { + return empty($this->getFilesToDeleteFromRequest()) ? false : true; + } + + protected function getEntryAttributeValue(Model $entry) + { + $value = $entry->{$this->getAttributeName()}; + + return isset($entry->getCasts()[$this->getName()]) ? $value : json_encode($value); + } + + private function getFilesToDeleteFromRequest(): array + { + return collect(CRUD::getRequest()->get('clear_'.$this->getNameForRequest()))->flatten()->toArray(); + } } diff --git a/src/Uploaders/MediaSingleBase64Image.php b/src/Uploaders/MediaSingleBase64Image.php index ee66c86..20425e6 100644 --- a/src/Uploaders/MediaSingleBase64Image.php +++ b/src/Uploaders/MediaSingleBase64Image.php @@ -5,14 +5,19 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; +use Illuminate\Support\Collection; class MediaSingleBase64Image extends MediaUploader { - public function uploadFiles(Model $entry, $values = null) + public function uploadFiles(Model $entry, $value = null) { $value = $value ?? CrudPanelFacade::getRequest()->get($this->getName()); - $previousImage = $this->get($entry); + $previousImage = $this->getPreviousFiles($entry); + + if (is_a($previousImage, Collection::class, true)) { + $previousImage = null; + } if (! $value && $previousImage) { $previousImage->delete(); @@ -24,7 +29,6 @@ public function uploadFiles(Model $entry, $values = null) if ($previousImage) { $previousImage->delete(); } - $this->addMediaFile($entry, $value); } } @@ -66,4 +70,14 @@ private function getMediaFromFileUrl($previousImages, $fileUrl, $entry) return is_array($previousImage) ? array_shift($previousImage) : null; } + + protected function shouldUploadFiles($value): bool + { + return $value && is_string($value) && Str::startsWith($value, 'data:image'); + } + + public function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool + { + return $entry->exists && is_string($entryValue) && ! Str::startsWith($entryValue, 'data:image'); + } } diff --git a/src/Uploaders/MediaSingleFile.php b/src/Uploaders/MediaSingleFile.php index 1780bbf..34ad93c 100644 --- a/src/Uploaders/MediaSingleFile.php +++ b/src/Uploaders/MediaSingleFile.php @@ -5,6 +5,7 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Collection; class MediaSingleFile extends MediaUploader { @@ -12,7 +13,11 @@ public function uploadFiles(Model $entry, $value = null) { $value = $value ?? CRUD::getRequest()->file($this->getName()); - $previousFile = $this->get($entry); + $previousFile = $this->getPreviousFiles($entry); + + if (is_a($previousFile, Collection::class, true)) { + $previousFile = null; + } if ($previousFile && ($value && is_a($value, UploadedFile::class) || request()->has($this->getName()))) { $previousFile->delete(); @@ -53,4 +58,22 @@ public function uploadRepeatableFiles($values, $previousFiles, $entry = null) } } } + + /** + * Single file uploaders send no value when they are not dirty. + */ + public function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool + { + return is_string($entryValue); + } + + protected function hasDeletedFiles($entryValue): bool + { + return $entryValue === null; + } + + protected function shouldUploadFiles($value): bool + { + return is_a($value, 'Illuminate\Http\UploadedFile', true); + } } diff --git a/src/Uploaders/MediaUploader.php b/src/Uploaders/MediaUploader.php index a9462da..de77efe 100644 --- a/src/Uploaders/MediaUploader.php +++ b/src/Uploaders/MediaUploader.php @@ -3,28 +3,26 @@ namespace Backpack\MediaLibraryUploaders\Uploaders; use Backpack\CRUD\app\Library\Uploaders\Uploader; -use Backpack\MediaLibraryUploaders\ConstrainedFileAdder; -use Exception; use Illuminate\Database\Eloquent\Model; -use Illuminate\Http\UploadedFile; -use Illuminate\Support\Collection; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\MediaCollections\Models\Media; -use Spatie\MediaLibrary\Support\PathGenerator\PathGeneratorFactory; -use Symfony\Component\HttpFoundation\File\File; abstract class MediaUploader extends Uploader { - public $mediaName; - - public $collection; + use Traits\IdentifiesMedia; + use Traits\AddMediaToModels; + use Traits\HasConstrainedFileAdder; + use Traits\HasCustomProperties; + use Traits\HasSavingCallback; + use Traits\HasCollections; + use Traits\RetrievesUploadedFiles; + use Traits\HandleRepeatableUploads; + use Traits\DeletesUploadedFiles; public $displayConversions; public $order; - public $savingEventCallback = null; - public function __construct(array $crudObject, array $configuration) { $this->collection = $configuration['collection'] ?? 'default'; @@ -40,83 +38,18 @@ public function __construct(array $crudObject, array $configuration) }) ->first(); - $configuration['disk'] = $modelDefinition?->diskName ?? null; + $configuration['disk'] ??= $modelDefinition?->diskName ?? null; - $configuration['disk'] = empty($configuration['disk']) ? $crudObject['disk'] ?? config('media-library.disk_name') : null; + $configuration['disk'] = empty($configuration['disk']) ? ($crudObject['disk'] ?? config('media-library.disk_name')) : $configuration['disk']; - // read https://spatie.be/docs/laravel-medialibrary/v10/advanced-usage/using-a-custom-directory-structure#main + // read https://spatie.be/docs/laravel-medialibrary/v11/advanced-usage/using-a-custom-directory-structure#main // on how to customize file directory $crudObject['prefix'] = $configuration['path'] = ''; parent::__construct($crudObject, $configuration); } - /************************* - * Public methods * - *************************/ - public function storeUploadedFiles(Model $entry): Model - { - if ($this->handleRepeatableFiles) { - return $this->handleRepeatableFiles($entry); - } - - $this->uploadFiles($entry); - - // make sure we remove the attribute from the model in case developer is using it in fillable - // or using guarded in their models. - $entry->offsetUnset($this->getName()); - // setting the raw attributes makes sure the `attributeCastCache` property is cleared, preventing - // uploaded files from beeing re-added to the entry from the cache. - $entry = $entry->setRawAttributes($entry->getAttributes()); - - return $entry; - } - - public function retrieveUploadedFiles(Model $entry): Model - { - $media = $this->get($entry); - - if (! $media) { - return $entry; - } - - if (empty($entry->mediaConversions)) { - $entry->registerAllMediaConversions(); - } - - if ($this->handleRepeatableFiles) { - $values = $entry->{$this->getRepeatableContainerName()} ?? []; - - if (! is_array($values)) { - $values = json_decode($values, true); - } - - $repeatableUploaders = array_merge(app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()), [$this]); - foreach ($repeatableUploaders as $uploader) { - $uploadValues = $uploader->getPreviousRepeatableValues($entry); - - $values = $this->mergeValuesRecursive($values, $uploadValues); - } - - $entry->{$this->getRepeatableContainerName()} = $values; - - return $entry; - } - - if (is_a($media, 'Spatie\MediaLibrary\MediaCollections\Models\Media')) { - $entry->{$this->getName()} = $this->getMediaIdentifier($media, $entry); - } else { - $entry->{$this->getName()} = $media->map(function ($item) use ($entry) { - return $this->getMediaIdentifier($item, $entry); - })->toArray(); - } - - return $entry; - } - - /***************************************************** - * Protected methods - default implementation * - *****************************************************/ + /** @deprecated - use getPreviousFiles() */ protected function get(HasMedia|Model $entry) { $media = $entry->getMedia($this->collection, function ($media) use ($entry) { @@ -131,88 +64,7 @@ protected function get(HasMedia|Model $entry) return $media->first(); } - protected function processRepeatableUploads(Model $entry, Collection $values): Collection - { - foreach (app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { - $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $uploader->getPreviousRepeatableMedia($entry), $entry); - - $values->transform(function ($item) use ($uploader) { - unset($item[$uploader->getName()]); - - return $item; - }); - } - - return $values; - } - - protected function addMediaFile($entry, $file, $order = null) - { - $this->order = $order; - - $fileAdder = $this->initFileAdder($entry, $file); - - $fileAdder = $fileAdder->usingName($this->mediaName) - ->withCustomProperties($this->getCustomProperties()) - ->usingFileName($this->getFileName($file)); - - $constrainedMedia = new ConstrainedFileAdder(); - $constrainedMedia->setFileAdder($fileAdder); - $constrainedMedia->setMediaUploader($this); - - if ($this->savingEventCallback && is_callable($this->savingEventCallback)) { - $constrainedMedia = call_user_func_array($this->savingEventCallback, [$constrainedMedia, $this]); - } - - if (! $constrainedMedia) { - throw new Exception('Please return a valid class from `whenSaving` closure. Field: '.$this->getName()); - } - - $constrainedMedia->getFileAdder()->toMediaCollection($this->collection, $this->getDisk()); - } - - protected function getPreviousRepeatableValues(Model $entry) - { - if ($this->canHandleMultipleFiles()) { - return $this->get($entry) - ->groupBy(function ($item) { - return $item->getCustomProperty('repeatableRow'); - }) - ->transform(function ($media) use ($entry) { - $mediaItems = $media->map(function ($item) use ($entry) { - return $this->getMediaIdentifier($item, $entry); - }) - ->toArray(); - - return [$this->getName() => $mediaItems]; - }) - ->toArray(); - } - - return $this->get($entry) - ->transform(function ($item) use ($entry) { - return [ - $this->getName() => $this->getMediaIdentifier($item, $entry), - 'order_column' => $item->getCustomProperty('repeatableRow'), - ]; - }) - ->sortBy('order_column') - ->keyBy('order_column') - ->toArray(); - } - - protected function getPreviousRepeatableMedia(Model $entry) - { - $orderedMedia = []; - $previousMedia = $this->get($entry)->transform(function ($item) { - return [$this->getName() => $item, 'order_column' => $item->getCustomProperty('repeatableRow')]; - }); - $previousMedia->each(function($item) use (&$orderedMedia) { - $orderedMedia[] = $item[$this->getName()]; - }); - - return $orderedMedia; - } + /************************************************** * Private methods- default implementation * @@ -223,22 +75,6 @@ private function getModelInstance($crudObject): Model return new ($crudObject['baseModel'] ?? get_class(app('crud')->getModel())); } - private function initFileAdder($entry, $file) - { - if (is_a($file, UploadedFile::class, true)) { - return $entry->addMedia($file); - } - - if (is_string($file)) { - return $entry->addMediaFromBase64($file); - } - - if (get_class($file) === File::class) { - return $entry->addMedia($file->getPathName()); - } - - } - private function getConversionToDisplay($item) { foreach ($this->displayConversions as $displayConversion) { @@ -250,34 +86,4 @@ private function getConversionToDisplay($item) return false; } - /************************* - * Helper methods * - *************************/ - public function getCustomProperties() - { - return [ - 'name' => $this->getName(), - 'repeatableContainerName' => $this->repeatableContainerName, - 'repeatableRow' => $this->order, - ]; - } - - public function getMediaIdentifier($media, $entry = null) - { - $path = PathGeneratorFactory::create($media); - - if ($entry && ! empty($entry->mediaConversions)) { - $conversion = array_values(array_filter($entry->mediaConversions, function ($item) use ($media) { - return $item->getName() === $this->getConversionToDisplay($media); - }))[0] ?? null; - - if (! $conversion) { - return $path->getPath($media).$media->file_name; - } - - return $path->getPathForConversions($media).$conversion->getConversionFile($media); - } - - return $path->getPath($media).$media->file_name; - } } diff --git a/src/Uploaders/Traits/AddMediaToModels.php b/src/Uploaders/Traits/AddMediaToModels.php new file mode 100644 index 0000000..cb6a9de --- /dev/null +++ b/src/Uploaders/Traits/AddMediaToModels.php @@ -0,0 +1,53 @@ +order = $order; + + $fileAdder = $this->initFileAdder($entry, $file); + + $fileAdder = $fileAdder->usingName($this->mediaName) + ->withCustomProperties($this->getCustomProperties()) + ->usingFileName($this->getFileName($file)); + + $constrainedMedia = new ConstrainedFileAdder(); + $constrainedMedia->setFileAdder($fileAdder); + $constrainedMedia->setMediaUploader($this); + + if ($this->savingEventCallback && is_callable($this->savingEventCallback)) { + $constrainedMedia = call_user_func_array($this->savingEventCallback, [$constrainedMedia, $this]); + } + + if (! $constrainedMedia) { + throw new Exception('Please return a valid class from `whenSaving` closure. Field: '.$this->getName()); + } + + $constrainedMedia->getFileAdder()->toMediaCollection($this->collection, $this->getDisk()); + } + + public function storeUploadedFiles(Model $entry): Model + { + if ($this->handleRepeatableFiles) { + return $this->handleRepeatableFiles($entry); + } + + $this->uploadFiles($entry); + + // make sure we remove the attribute from the model in case developer is using it in fillable + // or using guarded in their models. + $entry->offsetUnset($this->getName()); + // setting the raw attributes makes sure the `attributeCastCache` property is cleared, preventing + // uploaded files from being re-added to the entry from the cache. + $entry = $entry->setRawAttributes($entry->getAttributes()); + + return $entry; + } +} diff --git a/src/Uploaders/Traits/DeletesUploadedFiles.php b/src/Uploaders/Traits/DeletesUploadedFiles.php new file mode 100644 index 0000000..7009524 --- /dev/null +++ b/src/Uploaders/Traits/DeletesUploadedFiles.php @@ -0,0 +1,48 @@ +shouldDeleteFiles()) { + return; + } + + $files = $entry->media->filter(function($query) { + return $query->custom_properties['name'] == $this->getAttributeName() && + $query->custom_properties['repeatableContainerName'] == $this->getRepeatableContainerName() && + $query->custom_properties['repeatableRow'] == null; + }); + + $files->each->delete(); + + } + + protected function deletePivotFiles(Model|Pivot $model) + { + if (! $this->shouldDeleteFiles()) { + return; + } + + if (! is_a($model, Pivot::class, true)) { + $pivots = $model->{$this->getRepeatableContainerName()}; + foreach ($pivots as $pivot) { + $pivot = $pivot->pivot->loadMissing('media'); + $this->deleteFiles($pivot); + } + + return; + } + + //this is a workaround for Laravel Pivot Models, because they don't bring the primary key when eager loading + // https://github.com/laravel/framework/issues/31658 + $model->refresh(); + $model->loadMissing('media'); + + $this->deleteFiles($model); + } +} \ No newline at end of file diff --git a/src/Uploaders/Traits/HandleRepeatableUploads.php b/src/Uploaders/Traits/HandleRepeatableUploads.php new file mode 100644 index 0000000..a14dd8c --- /dev/null +++ b/src/Uploaders/Traits/HandleRepeatableUploads.php @@ -0,0 +1,97 @@ +getRepeatableUploadersFor($this->getRepeatableContainerName()) as $uploader) { + $uploader->uploadRepeatableFiles($values->pluck($uploader->getName())->toArray(), $uploader->getPreviousRepeatableMedia($entry), $entry); + + $values->transform(function ($item) use ($uploader) { + unset($item[$uploader->getName()]); + + return $item; + }); + } + + return $values->toArray(); + } + + protected function uploadRelationshipFiles(Model $entry): Model + { + $entryValue = $entry->{$this->getAttributeName()}; + + if ($this->handleMultipleFiles && is_string($entryValue)) { + try { + $entryValue = json_decode($entryValue, true); + } catch (\Exception) { + return $entry; + } + } + + if ($this->hasDeletedFiles($entryValue)) { + $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, false); + $this->updatedPreviousFiles = $this->getEntryAttributeValue($entry); + } + + if ($this->shouldKeepPreviousValueUnchanged($entry, $entryValue)) { + $entry->{$this->getAttributeName()} = $this->updatedPreviousFiles ?? $this->getEntryOriginalValue($entry); + + return $entry; + } + + if ($this->shouldUploadFiles($entryValue)) { + $entry->{$this->getAttributeName()} = $this->uploadFiles($entry, $entryValue); + } + + return $entry; + } + + public function getPreviousRepeatableValues(Model $entry) + { + if ($this->canHandleMultipleFiles()) { + return $this->getPreviousFiles($entry) + ->groupBy(function ($item) { + return $item->getCustomProperty('repeatableRow'); + }) + ->transform(function ($media) use ($entry) { + $mediaItems = $media->map(function ($item) use ($entry) { + return $this->getMediaIdentifier($item, $entry); + }) + ->toArray(); + + return [$this->getName() => $mediaItems]; + }) + ->toArray(); + } + + return $this->getPreviousFiles($entry) + ->transform(function ($item) use ($entry) { + return [ + $this->getName() => $this->getMediaIdentifier($item, $entry), + 'order_column' => $item->getCustomProperty('repeatableRow'), + ]; + }) + ->sortBy('order_column') + ->keyBy('order_column') + ->toArray(); + } + + public function getPreviousRepeatableMedia(Model $entry) + { + $orderedMedia = []; + $previousMedia = $this->getPreviousFiles($entry)->transform(function ($item) { + return [$this->getName() => $item, 'order_column' => $item->getCustomProperty('repeatableRow')]; + }); + $previousMedia->each(function ($item) use (&$orderedMedia) { + $orderedMedia[] = $item[$this->getName()]; + }); + + return $orderedMedia; + } +} diff --git a/src/Uploaders/Traits/HasCollections.php b/src/Uploaders/Traits/HasCollections.php new file mode 100644 index 0000000..6f85a59 --- /dev/null +++ b/src/Uploaders/Traits/HasCollections.php @@ -0,0 +1,8 @@ +addMedia($file); + } + + if (is_string($file)) { + return $entry->addMediaFromBase64($file); + } + + if (get_class($file) === File::class) { + return $entry->addMedia($file->getPathName()); + } + } +} \ No newline at end of file diff --git a/src/Uploaders/Traits/HasCustomProperties.php b/src/Uploaders/Traits/HasCustomProperties.php new file mode 100644 index 0000000..09e351a --- /dev/null +++ b/src/Uploaders/Traits/HasCustomProperties.php @@ -0,0 +1,15 @@ + $this->getName(), + 'repeatableContainerName' => $this->repeatableContainerName, + 'repeatableRow' => $this->order, + ]; + } +} \ No newline at end of file diff --git a/src/Uploaders/Traits/HasSavingCallback.php b/src/Uploaders/Traits/HasSavingCallback.php new file mode 100644 index 0000000..597f109 --- /dev/null +++ b/src/Uploaders/Traits/HasSavingCallback.php @@ -0,0 +1,10 @@ +mediaConversions)) { + $conversion = array_filter($entry->mediaConversions, function ($item) use ($media) { + return $item->getName() === $this->getConversionToDisplay($media); + })[0] ?? null; + + if (! $conversion) { + return $path->getPath($media).$media->file_name; + } + + return $path->getPathForConversions($media).$conversion->getConversionFile($media); + } + if (is_null($path)) { + dd($media); + } + + return $path->getPath($media).$media->file_name; + } +} diff --git a/src/Uploaders/Traits/RetrievesUploadedFiles.php b/src/Uploaders/Traits/RetrievesUploadedFiles.php new file mode 100644 index 0000000..6b570f2 --- /dev/null +++ b/src/Uploaders/Traits/RetrievesUploadedFiles.php @@ -0,0 +1,67 @@ +getPreviousFiles($entry); + + if (! $media) { + return $entry; + } + + if (empty($entry->mediaConversions)) { + $entry->registerAllMediaConversions(); + } + + if ($this->handleRepeatableFiles) { + $values = $entry->{$this->getRepeatableContainerName()} ?? []; + + if (! is_array($values)) { + $values = json_decode($values, true); + } + + $repeatableUploaders = array_merge(app('UploadersRepository')->getRepeatableUploadersFor($this->getRepeatableContainerName()), [$this]); + foreach ($repeatableUploaders as $uploader) { + $uploadValues = $uploader->getPreviousRepeatableValues($entry); + + $values = $this->mergeValuesRecursive($values, $uploadValues); + } + + $entry->{$this->getRepeatableContainerName()} = $values; + + return $entry; + } + + if (is_a($media, 'Spatie\MediaLibrary\MediaCollections\Models\Media')) { + $entry->{$this->getName()} = $this->getMediaIdentifier($media, $entry); + } else { + $entry->{$this->getName()} = $media->map(function ($item) use ($entry) { + return $this->getMediaIdentifier($item, $entry); + })->toArray(); + } + + return $entry; + } + + public function getPreviousFiles(Model $entry): mixed + { + $media = $entry->getMedia($this->collection, function ($media) use ($entry) { + /** @var Media $media */ + return $media->getCustomProperty('name') === $this->getName() && + $media->getCustomProperty('repeatableContainerName') === $this->repeatableContainerName && + $entry->{$entry->getKeyName()} === $media->getAttribute('model_id'); + }); + + if ($this->canHandleMultipleFiles() || $this->handleRepeatableFiles) { + return $media; + } + + return $media->first(); + } + +} \ No newline at end of file diff --git a/tests/Config/Controllers/MediaUploaderCrudController.php b/tests/Config/Controllers/MediaUploaderCrudController.php new file mode 100644 index 0000000..ee496f6 --- /dev/null +++ b/tests/Config/Controllers/MediaUploaderCrudController.php @@ -0,0 +1,50 @@ +crud->setRoute(config('backpack.base.route_prefix').'/media-uploader'); + $this->crud->setModel(MediaUploader::class); + } + + protected function setupCreateOperation() + { + CRUD::field('upload')->type('upload')->withMedia([ + 'disk' => 'uploaders', + 'fileNamer' => fn ($file) => $file->getClientOriginalName(), + 'whenSaving' => function($spatieMedia, $backpackMediaObject) { + return $spatieMedia->preservingOriginal(); + } + ]); + CRUD::field('upload_multiple')->type('upload_multiple')->withMedia([ + 'disk' => 'uploaders', + 'fileNamer' => fn ($file) => $file->getClientOriginalName(), + 'whenSaving' => function($spatieMedia, $backpackMediaObject) { + return $spatieMedia->preservingOriginal(); + } + ]); + } + + protected function setupUpdateOperation() + { + $this->setupCreateOperation(); + } + + protected function setupDeleteOperation() + { + $this->setupCreateOperation(); + } +} diff --git a/tests/Config/Database/Migrations/create_media_table.php b/tests/Config/Database/Migrations/create_media_table.php new file mode 100644 index 0000000..ea4bd59 --- /dev/null +++ b/tests/Config/Database/Migrations/create_media_table.php @@ -0,0 +1,36 @@ +id(); + + $table->morphs('model'); + $table->uuid('uuid')->nullable()->unique(); + $table->string('collection_name'); + $table->string('name'); + $table->string('file_name'); + $table->string('mime_type')->nullable(); + $table->string('disk'); + $table->string('conversions_disk')->nullable(); + $table->unsignedBigInteger('size'); + $table->json('manipulations'); + $table->json('custom_properties'); + $table->json('generated_conversions'); + $table->json('responsive_images'); + $table->unsignedInteger('order_column')->nullable()->index(); + + $table->nullableTimestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('media'); + } +}; diff --git a/tests/Config/Database/Migrations/create_media_uploader_table.php b/tests/Config/Database/Migrations/create_media_uploader_table.php new file mode 100644 index 0000000..6fdbdf7 --- /dev/null +++ b/tests/Config/Database/Migrations/create_media_uploader_table.php @@ -0,0 +1,20 @@ +unsignedBigInteger('id')->primary(); + $table->json('repeatable')->nullable(); + }); */ + } + + public function down(): void + { + //Schema::dropIfExists('uploaders'); + } +}; diff --git a/tests/Config/Models/File.php b/tests/Config/Models/File.php new file mode 100644 index 0000000..ce8cdd2 --- /dev/null +++ b/tests/Config/Models/File.php @@ -0,0 +1,29 @@ +belongsToMany(MediaUploader::class, 'uploaders_pivot', 'file_id', 'uploader_id'); + } +} diff --git a/tests/Config/Models/Folder.php b/tests/Config/Models/Folder.php new file mode 100644 index 0000000..81c763e --- /dev/null +++ b/tests/Config/Models/Folder.php @@ -0,0 +1,27 @@ +belongsTo(MediaUploader::class); + } +} diff --git a/tests/Config/Models/MediaUploader.php b/tests/Config/Models/MediaUploader.php new file mode 100644 index 0000000..348f81c --- /dev/null +++ b/tests/Config/Models/MediaUploader.php @@ -0,0 +1,48 @@ + 'json', + ]; + + public function documents() : BelongsToMany + { + return $this->belongsToMany(File::class, 'uploaders_pivot', 'uploader_id', 'file_id') + ->using(UploadersPivot::class) + ->withPivot(['id', 'dropzone', 'easymde', 'upload', 'image', 'upload_multiple']); + } + + public function hasManyRelation() : HasMany + { + return $this->hasMany(Picture::class, 'uploader_id'); + } + + public function hasOneRelation() : HasOne + { + return $this->hasOne(Folder::class, 'uploader_id'); + } +} diff --git a/tests/Config/Models/Picture.php b/tests/Config/Models/Picture.php new file mode 100644 index 0000000..9344024 --- /dev/null +++ b/tests/Config/Models/Picture.php @@ -0,0 +1,27 @@ +belongsTo(MediaUploader::class); + } +} diff --git a/tests/Config/Models/UploadersPivot.php b/tests/Config/Models/UploadersPivot.php new file mode 100644 index 0000000..16e3c9d --- /dev/null +++ b/tests/Config/Models/UploadersPivot.php @@ -0,0 +1,21 @@ +crud(config('backpack.base.route_prefix').'/media-uploader', MediaUploaderCrudController::class); + } + + public function setUp(): void + { + parent::setUp(); + + $this->testBaseUrl = config('backpack.base.route_prefix').'/media-uploader'; + } + + public function test_it_can_access_the_uploaders_create_page() + { + $response = $this->get($this->testBaseUrl.'/create'); + $response->assertStatus(200); + } + + public function test_it_can_upload_a_single_file() + { + $response = $this->post($this->testBaseUrl, [ + 'upload' => $this->getUploadedFile('avatar1.jpg'), + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(1, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + } + + public function test_it_can_upload_multiple_files() + { + $response = $this->post($this->testBaseUrl, [ + 'upload_multiple' => [ + $this->getUploadedFile('avatar1.jpg'), + $this->getUploadedFile('avatar2.jpg'), + ], + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(2, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('2/avatar2.jpg')); + } + + public function test_it_can_upload_files_for_multiple_uploaders() + { + $response = $this->post($this->testBaseUrl, [ + 'upload' => $this->getUploadedFile('avatar1.jpg'), + 'upload_multiple' => [ + $this->getUploadedFile('avatar2.jpg'), + $this->getUploadedFile('avatar3.jpg'), + ], + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(3, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('2/avatar2.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('3/avatar3.jpg')); + } + + public function test_it_display_the_edit_page_without_files() + { + self::initUploader(); + + $response = $this->get($this->testBaseUrl.'/1/edit'); + $response->assertStatus(200); + } + + public function test_it_display_the_upload_page_with_files() + { + self::initUploaderWithFiles(); + $response = $this->get($this->testBaseUrl.'/1/edit'); + + $response->assertStatus(200); + + $response->assertSee('avatar1.jpg'); + $response->assertSee('avatar2.jpg'); + $response->assertSee('avatar3.jpg'); + } + + public function test_it_can_update_files() + { + self::initUploaderWithFiles(); + + $response = $this->put($this->testBaseUrl.'/1', [ + 'upload' => $this->getUploadedFile('avatar4.jpg'), + 'upload_multiple' => [ + $this->getUploadedFile('avatar5.jpg'), + $this->getUploadedFile('avatar6.jpg'), + ], + 'clear_upload_multiple' => ['2/avatar2.jpg', '3/avatar3.jpg'], + 'id' => 1, + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(3, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('4/avatar4.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('5/avatar5.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('6/avatar6.jpg')); + $this->assertFalse(Storage::disk('uploaders')->exists('2/avatar2.jpg')); + $this->assertFalse(Storage::disk('uploaders')->exists('3/avatar3.jpg')); + $this->assertFalse(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + + } + + public function test_it_keeps_previous_values_unchanged_when_not_deleted() + { + self::initUploaderWithFiles(); + + $response = $this->put($this->testBaseUrl.'/1', [ + 'upload_multiple' => ['2/avatar2.jpg', '3/avatar3.jpg'], + 'id' => 1, + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(3, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('2/avatar2.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('3/avatar3.jpg')); + } + + public function test_upload_multiple_can_delete_uploaded_files_and_add_at_the_same_time() + { + self::initUploaderWithFiles(); + + $response = $this->put($this->testBaseUrl.'/1', [ + 'upload_multiple' => $this->getUploadedFiles(['avatar4.jpg', 'avatar5.jpg']), + 'clear_upload_multiple' => ['2/avatar2.jpg'], + 'id' => 1, + ]); + + $response->assertStatus(302); + + $response->assertRedirect($this->testBaseUrl); + + $this->assertDatabaseCount('uploaders', 1); + + $uploader = MediaUploader::first(); + + $this->assertEquals(4, $uploader->getMedia()->count()); + + $this->assertTrue(Storage::disk('uploaders')->exists('1/avatar1.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('3/avatar3.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('4/avatar4.jpg')); + $this->assertTrue(Storage::disk('uploaders')->exists('5/avatar5.jpg')); + + } + + public function test_it_can_delete_uploaded_files() + { + self::initUploaderWithFiles(); + + $response = $this->delete($this->testBaseUrl.'/1'); + + $response->assertStatus(200); + + $this->assertDatabaseCount('uploaders', 0); + + $files = Storage::disk('uploaders')->allFiles(); + + $this->assertEquals(0, count($files)); + } + + private static function initUploader() + { + $uploader = new MediaUploader(); + $uploader->save(); + } + + private function initUploaderWithFiles() + { + $uploader = new MediaUploader(); + $uploader->addMedia($this->getUploadedFile('avatar1.jpg'))->withCustomProperties([ + 'name' => 'upload', + 'repeatableContainerName' => null, + 'repeatableRow' => null, + ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); + $uploader->addMedia($this->getUploadedFile('avatar2.jpg'))->withCustomProperties([ + 'name' => 'upload_multiple', + 'repeatableContainerName' => null, + 'repeatableRow' => null, + ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); + $uploader->addMedia($this->getUploadedFile('avatar3.jpg'))->withCustomProperties([ + 'name' => 'upload_multiple', + 'repeatableContainerName' => null, + 'repeatableRow' => null, + ])->preservingOriginal()->toMediaCollection('default', 'uploaders'); + $uploader->save(); + } +} diff --git a/tests/FeatureTestCase.php b/tests/FeatureTestCase.php new file mode 100644 index 0000000..93aea1e --- /dev/null +++ b/tests/FeatureTestCase.php @@ -0,0 +1,42 @@ +loadMigrationsFrom([ + '--database' => 'testing', + '--path' => realpath(__DIR__.'/Config/Database/Migrations'), + ]); + + config(['filesystems.disks.uploaders' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + ]]); + + + Storage::fake('uploaders'); + + $this->actingAs(User::find(1)); + } +} \ No newline at end of file