- Seamless integration
- Flexible configuration
- Good for serverless environments
- One JSON column to hold all files in a record
- Append pre-defined data to file base path
- Handle and validate pre-signed url responses out of the box
- Path obfuscation
1 — This package requires PHP 8.1 and at least Laravel 9.52. To get the latest version, simply require using Composer:
composer require gsmeira/laravel-attachments
2 — Once installed, if you are not using automatic package discovery, then you need to register the GSMeira\LaravelAttachments\AttachmentsServiceProvider
service provider in your config/app.php
.
3 — Publish the configuration file.
php artisan vendor:publish --tag=laravel-attachments
An attachments.php
file will be created in your applications config
folder.
1 — Set the filesystem disk on your .env
file.
FILESYSTEM_DISK=<disk>
2 — Add a json
column named attachments
to your model's table. Also, a jsonb
column could be used if you are using PostgreSQL.
$table->json('attachments')->nullable();
2 — Add the HasAttachments
trait to your model.
class MyModel extends Model
{
use HasAttachments;
}
3 — Pass a valid value for the attachments when creating or updating.
MyModel::create([
// ...
'attachments' => [
'profile_image' => $request->profile_image,
],
]);
$myModel->update([
// ...
'attachments' => [
'profile_image' => $request->profile_image,
],
]);
The file will be store in the attachments
column like this:
{
"profile_image": "b/4/c/Y5EsfRvITY4SEyP3IBwUMurkkpRo69DfEMEIRlp9.jpg"
}
IMPORTANT: There are two accepted values for an attachment. The first one is when a file is sent directly to the server. An UploadedFile
value is expected. The second one is when pre-signed urls are being used. An array
with a valid path
key is expected. We'll talk more about this second type in the section about pre-signed urls.
The main rule you need to be aware of when using this package is that every time the attachments
is set, a merge will be performed between the new value and the previous one already stored (if exist). So, even if you have multiple files stored you'll be able to perform actions individually. All examples below would work even if multiple files were already stored.
To update an attachment just set a new value to the key you want to update. The old file will be permanently removed from the storage disk. Example:
$user->update([
'attachments' => [
'profile_image' => $request->profile_image,
],
]);
Option 1 — Set the attachments with the key you want to remove equals null
(or any invalid value).
$myModel->update([
'attachments' => [
'profile_image' => null,
],
]);
The profile_image
file will be removed from the storage disk and the key will be removed from the attachments column.
Option 2 — The second way is using the deleteAttachment
method.
$myModel->deleteAttachment('profile_image');
If you want to delete more than one file an array can be provided to this method.
$myModel->deleteAttachment(['profile_image', 'cover_image']);
To remove all files you can set the attachments to null
.
$myModel->update([
'attachments' => null,
]);
Also, you can use the deleteAttachments
method.
$myModel->delateAttachments();
Every time a record from model that has HasAttachments
trait is deleted, all the files in the attachments column will be removed from the storage disk. If your model has soft deletion enabled, the files will be removed only on forceDelete
.
Located inside the config/attachments.php
file.
The folder where all attachments will be stored. Default: ''
Transforms the path string stored in the database into an object with extra information. If empty, nothing will be appended to the path. Default: [ AttachmentsAppend::Path, AttachmentsAppend::Url, AttachmentsAppend::Exists ]
Determines if the path obfuscation is active. Default: false
Defines how deep the path will be. Default: 3
If true
, the route to generate pre-signed urls will be registered. Default: false
The temporary folder that will be used to store the files when using pre-signed urls. Default: tmp
The pre-signed urls expiration time in minutes. Default: 5
The pre-signed url route generator configuration.
If you want a more granular configuration you can customize some options directly at your model's file.
public function attachmentsBaseFolder(): string
{
return 'users';
}
public function isAttachmentsWrapperFolderEnabled(): bool
{
return true;
}
public function isAttachmentsPathObfuscationEnabled(): bool
{
return true;
}
public function attachmentsPathObfuscationLevels(): int
{
return 2;
}
public function attachmentsDisk(): string
{
return 'private';
}
When using a pre-signed urls approach, your files will be stored in services like s3 and the files won't be sent through the server anymore. Instead, the files are uploaded directly from the frontend to the cloud service and just a reference for this file will be sent to the server. This package expects an array with a path
that should match with the file location in your storage service.
1 — Configure s3 in your Laravel app and set your filesystem disk to s3
.
FILESYSTEM_DISK=s3
2 — Set a CORS policy for your bucket in order to send files to it.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
IMPORTANT: Be careful, you should probably be more restrictive than the example above in non local environments.
3 — Enable and configure the pre-signed url endpoint in the attachments.php
config file.
4 — Create an endpoint to receive the file from the pre-signed url response. Example:
protected function update(User $user, Request $request) {
$request->validate([
'profile_image' => ['required', new PreSignedAttachmentRule],
]);
$user->update([
// ...
'attachments' => [
'profile_image' => $request->profile_image,
],
]);
// ...
}
Always use the PreSignedAttachmentRule
to check if the signed file is valid. This rule will check if the value is an array with a path
key and check if the path exists on s3
.
5 — On your frontend you should have something like this.
<input id="file" type="file" />
import axios from 'axios'
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
const host = 'http://localhost/'
async function generatePreSignedUrlAnsStore(file) {
const response = await axios.post(`${host}/attachments/signed-storage-url`)
const headers = response.data.headers
if ('Host' in headers)
delete headers.Host
await axios.put(response.data.url, file, { headers })
response.data.extension = file.name.split('.').pop()
return response.data
}
document.querySelector('#file').addEventListener('change', (evt) => {
const file = evt.target.files[0]
generatePreSignedUrlAnsStore(file).then((response) => {
axios.post(`${host}/profile-image`, {
profile_image: {
path: response.path,
name: file.name,
content_type: file.type,
},
})
})
})
This package will be fully tested before 1.0 release.
Laravel Attachments is licensed under The MIT License (MIT).