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

reorganize uploader docs #615

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions 7.x-dev/crud-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,9 @@ CRUD::field([

> NOTE: Summernote does NOT sanitize the input. If you do not trust the users of this field, you should sanitize the input or output using something like HTML Purifier. Personally we like to use install [mewebstudio/Purifier](https://github.com/mewebstudio/Purifier) and add an [accessor or mutator](https://laravel.com/docs/8.x/eloquent-mutators#accessors-and-mutators) on the Model, so that wherever the model is created from (admin panel or app), the output will always be clean. [Example here](https://github.com/Laravel-Backpack/demo/commit/7342cffb418bb568b9e4ee279859685ddc0456c1).

#### Uploading files with summernote

Summernote saves images as base64 encoded strings in the database. If you want to save them as files on the server, you can use the [Summernote Uploader](https://backpackforlaravel.com/docs/7.x/crud-uploaders). Please note that the Summernote Uploader is part of the `backpack/pro` package.
Input preview:

![CRUD Field - summernote](https://backpackforlaravel.com/uploads/docs-4-2/fields/summernote.png)
Expand Down
199 changes: 193 additions & 6 deletions 7.x-dev/crud-uploaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
<a name="upload-about"></a>
## About

Uploading and managing files is a common task in Admin Panels. Starting with Backpack v6, you can fully setup your upload fields in your field definition, using purpose-built classes we call Uploaders. No more need to create mutators, manual validation of input or custom code to handle the files - though you can still do that, if you want.
Uploading and managing files is a common task in Admin Panels. In Backpack v7, your field definition can include uploading logic, thanks to some classes we call Uploaders. You don't need to create mutators, manual validation of input or custom code to handle file upload - though you can still do that, if you want.

<a name="upload-how-it-works"></a>
## How it works
<a name="how-to-use-uploaders"></a>
## How to Use Uploaders

When adding an upload field (`upload`, `upload_multiple`, `image` or `dropzone`) to your operation, tell Backpack that you want to use the appropriate Uploader, by using `withFiles()`:
When adding an upload field (`upload`, `upload_multiple`, `image`, `dropzone`, `easymde`, `summernote`) to your operation, tell Backpack that you want to use the appropriate Uploader, by using `withFiles()`:

```php
CRUD::field('avatar')->type('upload')->withFiles();
Expand All @@ -23,8 +23,8 @@ That's it. Backpack will now handle the upload, storage and deletion of the file
> - (*) If you want your files to be deleted when the entry is deleted, please [Configure File Deletion](#deleting-files-when-entry-is-deleted)


<a name="upload-configuration"></a>
## Configuring the Uploaders
<a name="how-to-configure-uploaders"></a>
## How to Configure Uploaders

The `withFiles()` method accepts an array of options that you can use to customize the upload.

Expand Down Expand Up @@ -56,6 +56,177 @@ This allows you to overwrite or set the uploader class for this field. You can u
- **`fileNamer`** - default: **null**
It accepts a `FileNameGeneratorInterface` instance or a closure. As the name implies, this will be used to generate the file name. Read more about in the [Naming uploaded files](#upload-name-files) section.

<a name="available-uploaders"></a>
## Available Uploaders

We've already created Uploaders for the most common scenarios:
- CRUD comes with `SingleFile`, `MultipleFiles`, `SingleBas64Image`
- PRO comes with `DropzoneUploader`, `EasyMDEUploader`, `SummernoteUploader`
- if you want to use spatie/medialibrary you can just install [medialibrary-uploaders](https://github.com/Laravel-Backpack/medialibrary-uploaders) to get `MediaAjaxUploader`, `MediaMultipleFiles`, `MediaSingleBase64Image`, `MediaSingleFile`


<a name="how-to-create-uploaders"></a>
## How to Create Uploaders

Do you want to create your own Uploader class, for your custom field? Here's how you can do that, and how Uploader classes work behind the scenes.

First thing you need to decide if you are creating a _non-ajax_ or _ajax_ uploader:
- _non-ajax_ uploaders process the file upload when you submit your form;
- _ajax_ uploaders process the file upload before the form is submitted, by submitting an AJAX request using Javascript;

<a name="how-to-create-a-custom-non-ajax-uploader"></a>
### How to Create a Custom Non-Ajax Uploader

tabacitu marked this conversation as resolved.
Show resolved Hide resolved
First let's see how to create a non-ajax uploader, for that we will create a `CustomUploader` class that extends the abstract class `Uploader`.

```php
namespace App\Uploaders\CustomUploader;

use Backpack\CRUD\app\Library\Uploaders\Uploader;

class CustomUploader extends Uploader
{
// the function we need to implement
public function uploadFiles(Model $entry, $values)
{
// $entry is the model instance we are working with
// $values is the sent files from request.

// do your upload logic here

return $valueToBeStoredInTheDatabaseEntry;
}

// this is called when your uploader field is a subfield of a repeatable field. In here you receive
// the sent values in the current request and the previous repeatable values (only the uploads values).
protected function uploadRepeatableFiles($values, $previousValues)
{
// you should return an array of arrays (each sub array is a repeatable row) where the array key is the field name.
// backpack will merge this values along the other repeatable fields and save them in the database.
return [
[
'custom_upload' => 'path/file.jpg'
],
[
'custom_upload' => 'path/file.jpg'
]
];
}
}
```

You can now use this uploader in your field definition:

```php
CRUD::field('avatar')->type('upload')->withFiles([
'uploader' => \App\Uploaders\CustomUploader::class,
]);
```

If you custom uploader was created to work for a custom field (say it's called `custom_upload`), you can tell Backpack to always use this uploader for that field type - that way you don't have to specify it every time you use the field. You can do that in your Service Provider `boot()` method, by adding it to the `UploadersRepository`:

```php
// in your App\Providers\AppServiceProvider.php

protected function boot()
{
app('UploadersRepository')->addUploaderClasses(['custom_upload' => \App\Uploaders\CustomUploader::class], 'withFiles');
}
```

You can now use `CRUD::field('avatar')->type('custom_upload')->withFiles();` and it will use your custom uploader. What happens behind the scenes is that Backpack will register your uploader to run on 3 different model events: `saving`, `retrieved` and `deleting`.

The `Uploader` class has 3 "entry points" for the mentioned events: **`storeUploadedFiles()`**, **`retrieveUploadedFiles()`** and **`deleteUploadedFiles()`**. You can override these methods in your custom uploader, but typically you will not need to do that. The methods already delegate what will happen to the relevant methods (eg. if it's not a repeatable, call ```uploadFiles()```, othewise call ```uploadRepeatableFiles()```).

Notice this custom class you're creating is extending `Backpack\CRUD\app\Library\Uploaders\Uploader`. That base uploader class has most of the functionality implemented and uses **"strategy methods"** to configure the underlying behavior.

**`shouldUploadFiles`** - a method that returns a boolean to determine if the files should be uploaded. By default it returns true, but you can overwrite it to add your custom logic.

**`shouldKeepPreviousValuesUnchanged`** - a method that returns a boolean to determine if the previous values should be kept unchanged and not perform the upload.

**`hasDeletedFiles`** - a method that returns a boolean to determine if the files were deleted from the field.

**`getUploadedFilesFromRequest`** - this is the method that will be called to get the values sent in the request. Some uploaders require you get the `->files()` others the `->input()`. By default it returns the `->files()`.

This is the implementation of those methods in `SingleFile` uploader:
```php
protected function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool
{
// if a string is sent as the value, it means the file was not changed so we should keep
// previous value unchanged
return is_string($entryValue);
}

protected function hasDeletedFiles($entryValue): bool
{
// if the value is null, it means the file was deleted from the field
return $entryValue === null;
}

protected function shouldUploadFiles($value): bool
{
// when the value is an instance of UploadedFile, it means the file was uploaded and we should upload it
return is_a($value, 'Illuminate\Http\UploadedFile', true);
}

<a name="how-to-create-a-custom-ajax-uploader"></a>
### How to Create a Custom Ajax Uploader

For the ajax uploaders, the process is similar, but your custom uploader class should extend `BackpackAjaxUploader` instead of `Uploader` (**note that this requires backpack/pro**).

```php

namespace App\Uploaders\CustomUploader;

use Backpack\Pro\Uploaders\BackpackAjaxUploader;

class CustomUploader extends BackpackAjaxUploader
{
// this is called on `saving` event of the main entry, at this point you already performed the upload
// of the files in the ajax endpoint. By default they are in a temp folder, so here is the place
// where you should move them to the final disk and path and setup what will be saved in the database.
public function uploadFiles(Model $entry, $values)
{
return $valueToBeStoredInTheDatabaseEntry;
}

// this is called when your uploader field is a subfield of a repeatable field. In here you receive
// the sent values in the current request and the previous repeatable values (only the uploads values).
protected function uploadRepeatableFiles($values, $previousValues)
{
// you should return an array of arrays (each sub array is a repeatable row) where the array key is the field name.
// backpack will merge this values along the other repeatable fields and save them in the database.
return [
[
'custom_upload' => 'path/file.jpg'
],
[
'custom_upload' => 'path/file.jpg'
]
];
}
}
```

The process to register the uploader in the `UploadersRepositoy` is the same as the non-ajax uploader. `app('UploadersRepository')->addUploaderClasses(['custom_upload' => \App\Uploaders\CustomUploader::class], 'withFiles');` in the boot method of your provider.

In addition to the field configuration, ajax uploaders require that you use the `AjaxUploadOperation` trait in your controller. The operation is responsible to register the ajax route where your files will be sent and the upload process will be handled and the delete route from where you can delete **temporary files**.

Similar to model events, there are two "setup" methods for those endpoints: **`processAjaxEndpointUploads()`** and **`deleteAjaxEndpointUpload()`**. You can overwrite them to add your custom logic but most of the time you will not need to do that and just implement the `uploadFiles()` and `uploadRepeatableFiles()` methods.

The ajax uploader also has the same "strategy methods" as the non-ajax uploader (see above), but adds a few more:
- **`ajaxEndpointSuccessResponse($files = null)`** - This should return a `JsonResponse` with the needed information when the upload is successful. By default it returns a json response with the file path.
- **`ajaxEndpointErrorResponse($message)`** - Use this method to change the endpoint response in case the upload failed. Similar to the success it should return a `JsonResponse`.
- **`getAjaxEndpointDisk()`** - By default a `temporaryDisk` is used to store the files before they are moved to the final disk (when uploadFiles() is called). You can overwrite this method to change the disk used.
- **`getAjaxEndpointPath()`** - By default the path is `/temp` but you can override this method to change the path used.
- **`getDefaultAjaxEndpointValidation()`** - Should return the default validation rules (in the format of `BackpackCustomRule`) for the ajax endpoint. By default it returns a `ValidGenericAjaxEndpoint` rule.


For any other customization you would like to perform, please check the source code of the `Uploader` and `BackpackAjaxUploader` classes.

<a name="faq-uploaders"></a>
## FAQ about Uploaders

<a name="handling-uploaders-in-relationship-fields"></a>
### Handling uploads in relationship fields

Expand Down Expand Up @@ -184,6 +355,22 @@ class SomeModel extends Model
}
```

<a name="deleting-temporary-files"></a>
## Deleting temporary files

When using ajax uploaders, the files are uploaded to a temporary disk and path before being moved to the final disk and path. If by some reason the user does not finish the operation, those files may lay around in your server temporary folder.
To delete them, we have created a `backpack:purge-temporary-folder` command that you can schedule to run every day, or in the time frame that better suits your needs.

```php
// in your routes/console
use Illuminate\Console\Scheduling\Schedule;

Schedule::command('backpack:purge-temporary-folder')->daily();

```

For additional configuration check the `config/backpack/operations/ajax-uploads.php` file. Those configurations can also be passed on a "per-command" basis, eg: `backpack:purge-temporary-folder --disk=public --path=temp --older-than=5`.

<a name="custom-upload-fields"></a>
### Configuring uploaders in custom fields

Expand Down