Open tabacitu opened 1 year ago
What we need to do here is create a list of things/problems/quirks that happen when using Backpack v6 on Laravel Forge... then see what we can do about each one.
Problem 1. Basset needs to use the S3 bucket on Vapor. And for cache maps to be disabled. Thanks to @malek77z we've documented that process in the Basset docs.
Problem 2. Uploads need to happen on the S3 bucket as well. This is pretty standard I think, but we do need to
Anything else I've missed @malek77z ? Note that I haven't deployed v6 on Vapor yet so I'm working blind here 😅 We're hard at work to finish up the v6 launch so the priority is that, at the moment. But as soon as we're done I'd like us to take a closer look at this.
@tabacitu
Very happy t help out an retrace my steps. I now have 3 projects on Backpack (now 6!) and Vapor, so confirmed it works well. Also Basset came just in time as the tweaks i made before to upload so many files stopped working just as basset was released!
I'm going to assume that a Vapor project from https://vapor.laravel.com/ has been set up and there is an environment ready. Also to not, im working on windows, and not deploying directly, but instead though github actions.
So once the Vapor AWS is set up and connected to vapor, domain if you want one, database (im using db.t3.micros for now), you will need to log into Vapor on you CLI to connect the project.
composer require laravel/vapor-core
composer global require laravel/vapor-cli
then
vapor login
You should end up with a yml file like the following in root called vapor.yml
id: VAPORPROJECTID
name: PROJECTNAME
environments:
production:
timeout: 30
domain: PROJECTDOMAIN
memory: 1024
cli-memory: 512
runtime: 'php-8.2:al2'
database: DATABASENAME
queues:
- QUEUENAME-production # only if needed
queue-timeout: 300
storage: S3BUCKETNAME # Vapor will make this for you
build:
- 'COMPOSER_MIRROR_PATH_REPOS=1 composer install --no-dev'
- 'php artisan event:cache'
- 'npm ci && npm run prod && rm -rf node_modules'
deploy:
- 'php artisan migrate --force'
- 'php artisan queue:restart'
staging:
...
This is my github action yml file in .github/workflows/deply.yml
name: Deploy
on:
push:
branches: [ production, staging ]
jobs:
deploy:
runs-on: ubuntu-22.04
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
tools: composer:v2
coverage: none
- name: Require Vapor CLI
run: composer global require laravel/vapor-cli
- name: Configure Backpack authentication
run: composer config http-basic.backpackforlaravel.com IDHERE KEYHERE
- name: Install Project Dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Set Environment Variables for Production
if: github.ref == 'refs/heads/production'
run: |
echo "MIX_PUSHER_APP_KEY=${{ secrets.MIX_PUSHER_APP_KEY_PROD }}" >> $GITHUB_ENV
echo "MIX_PUSHER_APP_CLUSTER=${{ secrets.MIX_PUSHER_APP_CLUSTER_PROD }}" >> $GITHUB_ENV
- name: Set Environment Variables for Staging
if: github.ref == 'refs/heads/staging'
run: |
echo "MIX_PUSHER_APP_KEY=${{ secrets.MIX_PUSHER_APP_KEY_STAGE }}" >> $GITHUB_ENV
echo "MIX_PUSHER_APP_CLUSTER=${{ secrets.MIX_PUSHER_APP_CLUSTER_STAGE }}" >> $GITHUB_ENV
- name: Deploy Environment
run: vapor deploy ${{ github.ref_name }} --commit="${{ github.event.head_commit.id }}" --message="${{ github.event.head_commit.message }}"
env:
VAPOR_API_TOKEN: ${{ secrets.VAPOR_API_TOKEN }}
MIX_PUSHER_APP_KEY: ${{ env.MIX_PUSHER_APP_KEY }}
MIX_PUSHER_APP_CLUSTER: ${{ env.MIX_PUSHER_APP_CLUSTER }}
Note the secrets set. If you are using pusher or similar, important to set here as they will get written into the js asset file, I might have gone a bit overkill with the env as Vapor basically writes its own, but this got me to a working action.
So this should get git hub to put to Vapor now, and mostly work, but there are a few more things you have to be aware of. As none of the assets are stored in on the server, but S3, you need to make sure you wrap all the assets in public with asset(), this will pit up the ASSET_URL variable in env.
Next, for logos and so on in the ui.php config, when you want to pass them in you will also need to make sure your pointing to the correct place, this is an example I used in my config to make sure it was using the s3 bucket...
'project_logo' => '<img src="'.env('ASSET_URL').'/assets/log.svg" class="logo_main" />',
as you cant use asset() in the config, this works the same way
Next is image uploading... This comes in 2 parts, 1 being able to write the image to S3 bucket, however you also need to think about the tmp upload folder, as although this may work uploading ot the tmp folder of a lambda, its not guarantied to work as you could be dealing with a different machine.
this is my modified image.blade.php I use to do just that:
@php
// Image with tmp disk option. Set to s3_vapor if you need to store the uploads in s3
// This will then return the s3 temp url to save, which will then be moved to the real location in the s3 bucket
// There is potetnially an issue if the form fails to submit, due to error, and we are left with the location of the file
// being in the temp s3 bucket. This would mean we can read it in... This will need to be cleaned up at some point.
$field['prefix'] = $field['prefix'] ?? '';
$field['disk'] = $field['disk'] ?? null;
$field['tmp_disk'] = $field['tmp_disk'] ?? null;
$value = old_empty_or_null($field['name'], '') ?? $field['value'] ?? $field['default'] ?? '';
$imageDisplayUrl = '';
if (! function_exists('getDiskUrl')) {
function getDiskUrl($disk, $path) {
try {
// make sure the value don't have disk base path on it, this is the same as `Storage::disk($disk)->url($prefix);`,
// we need this solution to deal with `S3` not supporting getting empty urls
// that could happen when there is no $prefix set.
$origin = substr(Storage::disk($disk)->url('/'), 0, -1);
$path = str_replace($origin, '', $path);
return Storage::disk($disk)->url($path);
}
catch (Exception $e) {
// the driver does not support retrieving URLs (eg. SFTP)
return url($path);
}
}
}
if (! function_exists('maximumServerUploadSizeInBytes')) {
function maximumServerUploadSizeInBytes() {
$val = trim(ini_get('upload_max_filesize'));
$last = strtolower($val[strlen($val)-1]);
switch($last) {
// The 'G' modifier is available since PHP 5.1.0
case 'g':
$val = (int)$val * 1073741824;
break;
case 'm':
$val = (int)$val * 1048576;
break;
case 'k':
$val = (int)$val * 1024;
break;
}
return $val;
}
}
// if value isn't a base 64 image, generate URL
if($value && !preg_match('/^data\:image\//', $value)) {
// make sure to append prefix once to value
$imageDisplayUrl = Str::start($value, $field['prefix']);
// generate URL
$imageDisplayUrl = $field['disk']
? getDiskUrl($field['disk'], $imageDisplayUrl)
: url($imageDisplayUrl);
}
$max_image_size_in_bytes = $field['max_file_size'] ?? (int)maximumServerUploadSizeInBytes();
$field['wrapper'] = $field['wrapper'] ?? $field['wrapperAttributes'] ?? [];
$field['wrapper']['class'] = $field['wrapper']['class'] ?? "form-group col-sm-12";
$field['wrapper']['class'] = $field['wrapper']['class'].' cropperImage';
$field['wrapper']['data-aspectRatio'] = $field['aspect_ratio'] ?? 0;
$field['wrapper']['data-crop'] = $field['crop'] ?? false;
$field['wrapper']['data-field-name'] = $field['wrapper']['data-field-name'] ?? $field['name'];
$field['wrapper']['data-init-function'] = $field['wrapper']['data-init-function'] ?? 'bpFieldInitCropperImageElement';
@endphp
@include('crud::fields.inc.wrapper_start')
<label>{!! $field['label'] !!}</label>
@include('crud::fields.inc.translatable_icon')
{{-- Wrap the image or canvas element with a block element (container) --}}
<div class="row">
<div class="col-sm-6" data-handle="previewArea" style="margin-bottom: 20px;">
<img data-handle="mainImage" src="">
</div>
@if(isset($field['crop']) && $field['crop'])
<div class="col-sm-3" data-handle="previewArea">
<div class="docs-preview clearfix">
<div class="img-preview preview-lg">
<img src="" style="display: block; min-width: 0px !important; min-height: 0px !important; max-width: none !important; max-height: none !important; margin-left: -32.875px; margin-top: -18.4922px; transform: none;">
</div>
</div>
</div>
@endif
</div>
<div class="btn-group">
<div class="btn btn-light btn-sm btn-file">
{{ trans('backpack::crud.choose_file') }} <input type="file" accept="image/*" data-handle="uploadImage" @include('crud::fields.inc.attributes')>
<input type="hidden" data-handle="hiddenImage" name="{{ $field['name'] }}"
@if($field['tmp_disk'] && Auth::check())
@can('uploadFiles', App\Models\User::class)
tmp_disk="{{ $field['tmp_disk'] }}"
@endcan
@endif
data-value-prefix="{{ $field['prefix'] }}" data-value-url="{{$imageDisplayUrl}}" value="{{ $value }}">
</div>
@if(isset($field['crop']) && $field['crop'])
<button class="btn btn-light btn-sm" data-handle="rotateLeft" type="button" style="display: none;"><i class="la la-rotate-left"></i></button>
<button class="btn btn-light btn-sm" data-handle="rotateRight" type="button" style="display: none;"><i class="la la-rotate-right"></i></button>
<button class="btn btn-light btn-sm" data-handle="zoomIn" type="button" style="display: none;"><i class="la la-search-plus"></i></button>
<button class="btn btn-light btn-sm" data-handle="zoomOut" type="button" style="display: none;"><i class="la la-search-minus"></i></button>
<button class="btn btn-light btn-sm" data-handle="reset" type="button" style="display: none;"><i class="la la-times"></i></button>
@endif
<button class="btn btn-light btn-sm" data-handle="remove" type="button"><i class="la la-trash"></i></button>
</div>
@if ($field['tmp_disk'])
@if (Auth::check())
@can('uploadFiles', App\Models\User::class)
<span class="badge badge-pill bg-success" data-upload-status><span class="la la-check"></span></span></span>
@else
<span class="badge badge-pill bg-warning"><span class="la la-warning"></span></span></span>
@endcan
@else
<span class="badge badge-pill bg-danger"><span class="la la-warning"></span></span></span>
@endif
@endif
{{-- HINT --}}
@if (isset($field['hint']))
<p class="help-block">{!! $field['hint'] !!}</p>
@endif
@include('crud::fields.inc.wrapper_end')
{{-- ########################################## --}}
{{-- Extra CSS and JS for this particular field --}}
{{-- If a field type is shown multiple times on a form, the CSS and JS will only be loaded once --}}
{{-- FIELD CSS - will be loaded in the after_styles section --}}
@push('crud_fields_styles')
@loadOnce('packages/cropperjs/dist/cropper.min.css')
@loadOnce('image_field_style')
<style>
.image .btn-group {
margin-top: 10px;
}
img {
max-width: 100%; /* This rule is very important, please do not ignore this! */
}
.img-container, .img-preview {
width: 100%;
text-align: center;
}
.img-preview {
float: left;
margin-right: 10px;
margin-bottom: 10px;
overflow: hidden;
}
.preview-lg {
width: 263px;
height: 148px;
}
.btn-file {
position: relative;
overflow: hidden;
}
.btn-file input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 100px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
outline: none;
background: white;
cursor: inherit;
display: block;
}
</style>
@endLoadOnce
@endpush
{{-- FIELD JS - will be loaded in the after_scripts section --}}
@push('crud_fields_scripts')
@loadOnce('packages/cropperjs/dist/cropper.min.js')
@loadOnce('packages/jquery-cropper/dist/jquery-cropper.min.js')
@loadOnce('bpFieldInitCropperImageElement')
<script>
function bpFieldInitCropperImageElement(element) {
// Find DOM elements under this form-group element
var $mainImage = element.find('[data-handle=mainImage]');
var $uploadImage = element.find("[data-handle=uploadImage]");
var $hiddenImage = element.find("[data-handle=hiddenImage]");
var $rotateLeft = element.find("[data-handle=rotateLeft]");
var $rotateRight = element.find("[data-handle=rotateRight]");
var $zoomIn = element.find("[data-handle=zoomIn]");
var $zoomOut = element.find("[data-handle=zoomOut]");
var $reset = element.find("[data-handle=reset]");
var $remove = element.find("[data-handle=remove]");
var $previews = element.find("[data-handle=previewArea]");
// Options either global for all image type fields, or use 'data-*' elements for options passed in via the CRUD controller
var options = {
viewMode: 2,
checkOrientation: false,
autoCropArea: 1,
responsive: true,
preview : element.find('.img-preview'),
aspectRatio : element.attr('data-aspectRatio')
};
var crop = element.attr('data-crop');
// Hide 'Remove' button if there is no image saved
if (!$hiddenImage.val()){
$previews.hide();
$remove.hide();
}
// Make the main image show the image in the hidden input url (image loaded from database) or show the preview data
$mainImage.attr('src', $hiddenImage.data('value-url').length > 0 ? $hiddenImage.data('value-url') : $hiddenImage.val());
// Only initialize cropper plugin if crop is set to true
if(crop){
$remove.click(function() {
$mainImage.cropper("destroy");
$mainImage.attr('src','');
$hiddenImage.val('');
$rotateLeft.hide();
$rotateRight.hide();
$zoomIn.hide();
$zoomOut.hide();
$reset.hide();
$remove.hide();
$previews.hide();
});
} else {
$remove.click(function() {
$mainImage.attr('src','');
$hiddenImage.val('');
$remove.hide();
$previews.hide();
});
}
$uploadImage.change(function() {
var fileReader = new FileReader(),
files = this.files,
file;
if (!files.length) {
return;
}
file = files[0];
const maxImageSize = {{ $max_image_size_in_bytes }};
if(maxImageSize > 0 && file.size > maxImageSize) {
alert('Please pick an image smaller than '+maxImageSize+' bytes.');
} else if (/^image\/\w+$/.test(file.type)) {
fileReader.readAsDataURL(file);
fileReader.onload = function () {
$uploadImage.val("");
$previews.show();
if(crop){
$mainImage.cropper(options).cropper("reset", true).cropper("replace", this.result);
// update the hidden input after selecting a new item or cropping
$mainImage.on('ready cropstart cropend', function() {
var imageURL = $mainImage.cropper('getCroppedCanvas').toDataURL(file.type);
$hiddenImage.val(imageURL);
$hiddenImage.attr('data-update',1);
return true;
});
$rotateLeft.show();
$rotateRight.show();
$zoomIn.show();
$zoomOut.show();
$reset.show();
$remove.show();
} else {
$mainImage.attr('src',this.result);
$hiddenImage.val(this.result);
$hiddenImage.attr('data-update',1);
$remove.show();
}
};
} else {
new Noty({
type: "error",
text: "<strong>Please choose an image file</strong><br>The file you've chosen does not look like an image."
}).show();
}
});
//moved the click binds outside change event, or we would register as many click events for the same amout of times
//we triggered the image change
if(crop) {
$rotateLeft.click(function() {
$mainImage.cropper("rotate", 90);
$mainImage.trigger('cropend');
});
$rotateRight.click(function() {
$mainImage.cropper("rotate", -90);
$mainImage.trigger('cropend');
});
$zoomIn.click(function() {
$mainImage.cropper("zoom", 0.1);
$mainImage.trigger('cropend');
});
$zoomOut.click(function() {
$mainImage.cropper("zoom", -0.1);
$mainImage.trigger('cropend');
});
$reset.click(function() {
$mainImage.cropper("reset");
$mainImage.trigger('cropend');
});
}
element.on('CrudField:disable', function(e) {
element.children('.btn-group').children('button[data-handle=remove]').attr('disabled','disabled');
element.children('.btn-group').children('.btn-file').children('input[data-handle=uploadImage]').attr('disabled','disabled');
});
element.on('CrudField:enable', function(e) {
element.children('.btn-group').children('button[data-handle=remove]').removeAttr('disabled');
element.children('.btn-group').children('.btn-file').children('input[data-handle=uploadImage]').removeAttr('disabled');
});
}
</script>
<script>
// Check if any forms on the page contain an input with tmp_disk="s3_vapor"
var inputs = document.querySelectorAll('input[type=hidden][tmp_disk=s3_vapor]');
if (inputs.length > 0) { // Only run the code if such inputs exist
var forms = document.querySelectorAll('form'); // select all forms on the page
for (var i = 0; i < forms.length; i++) {
forms[i].addEventListener('submit', function(event) {
var form = event.target; // the form that triggered the submit event
// check if the form contains an input with tmp_disk="s3_vapor"
var hiddenImage = form.querySelector('input[type=hidden][tmp_disk=s3_vapor]');
// also check if attr data-update is set to 1
if (!hiddenImage || hiddenImage.getAttribute('data-update') != 1) {
// this form doesn't contain an input with tmp_disk="s3_vapor", so we don't need to do anything
return;
}
event.preventDefault(); // prevent the form from submitting immediately
// Find DOM elements under this form-group element for the hiddenImage we have
// ie the a parent will have the class form group lets get that element and work from there
var element = $(hiddenImage).closest('.form-group');
var mainImage = element.find('[data-handle=mainImage]');
var uploadImage = element.find("[data-handle=uploadImage]");
var uploadBadgeStatus = element.find("[data-upload-status]");
// Get the base64 data from the hidden image input
var fileData = hiddenImage.value;
// Convert the base64 data to a Blob
var blob = dataURLToBlob(fileData);
// Now use the Vapor's store method to upload the blob to S3
var file = new File([blob], "filename.png", {type: blob.type});
Vapor.store(file, {
visibility: 'public-read',
progress: progress => {
console.log('Upload progress:', Math.round(progress * 100), '%');
// update the data-upload-status to have the progress status
uploadBadgeStatus.html(Math.round(progress * 100)+'%');
}
}).then(response => {
var uploadedFileUrl = response.url.split('?')[0];
var uploadedFileKey = response.key;
mainImage.src = uploadedFileUrl;
hiddenImage.value = uploadedFileKey;
hiddenImage.attributes.tmp_disk.value = 'done'; // its uploaded, no need to do again just in case
// now, submit the form
form.submit();
}).catch(error => {
console.error('Upload failed:', error);
// fall back to normal upload
// remove the tmp_disk attribute so we don't get into a loop
hiddenImage.removeAttribute('tmp_disk');
// remove the data-update attribute so we don't get into a loop
hiddenImage.removeAttribute('data-update');
// submit the form again, this should now upload the normal way
form.submit();
});
});
}
}
// This function converts a base64 string to a Blob
function dataURLToBlob(dataURL) {
var BASE64_MARKER = ';base64,';
if (dataURL.indexOf(BASE64_MARKER) == -1) {
var parts = dataURL.split(',');
var contentType = parts[0].split(':')[1];
var raw = parts[1];
return new Blob([raw], {type: contentType});
}
var parts = dataURL.split(BASE64_MARKER);
var contentType = parts[0].split(':')[1];
var raw = window.atob(parts[1]);
var rawLength = raw.length;
var uInt8Array = new Uint8Array(rawLength);
for (var i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], {type: contentType});
}
</script>
@endLoadOnce
@endpush
{{-- End of Extra CSS and JS --}}
{{-- ########################################## --}}
And this is me calling it
CRUD::addField([
'name' => 'image_url',
'label'=> 'Image',
'type' => 'plus.image',
'crop' => true,
'tmp_disk' => 's3_vapor',
]);
(note the tmp_disk attribute)
We are still not there yet as we need to use laravel-vapor to do the client side uploading... this is my app.js in rescources/app.js which gets created with npm magic
import './bootstrap';
window.Vapor = require('laravel-vapor');
for NPM you will make sure you have
"laravel-vapor": "^0.6.0"
also in the ui.php make sure your loading whereever the script is i.e.
'scripts' => [
'js/app.js',
],
This is an example, not the green checkmark to show its connected as expected.
This is the other side, its my image trait I made to upload to s3 and so on
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\ImageManagerStatic as Image;
trait ImageTrait
{
public function setImageUrlAttribute($value, $attributeName="image_url")
{
$attribute_name = $attributeName;
$disk = config('features.use_s3_images') ? 's3' : 'public';
// remove if now none
if (empty($value)) {
if (isset($this->{$attribute_name}) && !empty($this->{$attribute_name})) {
$this->trashImage($this->{$attribute_name});
}
$this->attributes[$attribute_name] = null;
}
if (Str::startsWith($value, 'data:image')) {
$image = Image::make($value)->encode('jpg', 90);
$destination_path = $this->getImageDestinationPath($attribute_name);
$filename = $this->getImageFilename($value, $attribute_name);
Storage::disk($disk)->put($destination_path.'/'.$filename, $image->stream(), 'public');
// remove if different from saves
if (isset($this->{$attribute_name}) && !empty($this->{$attribute_name})) {
$this->trashImage($this->{$attribute_name});
}
$this->attributes[$attribute_name] = Storage::disk($disk)->url($destination_path.'/'.$filename);
} elseif (!empty($value)) {
// This can either be a URL that we want to store, or a tmp S3 KEY we want to move permanently and store
// The temp S3 KEY will look like this: tmp/a866dc90-b2e2-494b-8fcc-bc729b047685
// First we want to check for that
// check for temp S3 KEY
if(Str::startsWith($value, 'tmp/')) {
// we have a temp S3 URL, lets move it to the right place
$destination_path = $this->getImageDestinationPath($attribute_name);
$filename = $this->getImageFilename($value, $attribute_name);
$new_path = $destination_path.'/'.$filename;
Storage::disk($disk)->copy($value, $new_path);
Storage::disk($disk)->setVisibility($new_path, 'public');
// delete the tmp file option, not needed really as we will do clean up
config('features.delete_tmp_s3_files') ? $this->trashImage($value) : null;
// remove if different from saves
if (isset($this->{$attribute_name}) && !empty($this->{$attribute_name})) {
$this->trashImage($this->{$attribute_name});
}
$this->attributes[$attribute_name] = Storage::disk($disk)->url($new_path);
} else {
// we have a URL, lets just store it
$this->attributes[$attribute_name] = $value;
}
}
}
// trash a S3 image
public function trashImage($image_location)
{
// image location will be a whole URL, so we need to parse it and get the S3 true path
$disk = config('features.use_s3_images') ? 's3' : 'public';
$image_location = parse_url($image_location, PHP_URL_PATH);
// Break down the image path into directory, filename, and extension
$image_pathinfo = pathinfo($image_location);
// Construct the new image location by adding the model's ID to the start of the filename
$new_image_location = 'trash/'. $image_pathinfo['dirname'] . '/' . $this->id . '_' . $image_pathinfo['basename'];
// Move the file to the new location within the "trash" directory
Storage::disk($disk)->move($image_location, $new_image_location);
// Set the visibility of the trashed file to 'private'
Storage::disk($disk)->setVisibility($new_image_location, 'private');
}
protected function getImageDestinationPath($attribute_name)
{
// Define the specific destination path logic for each model
$year = date('Y');
$month = date('m');
$day = date('d');
// get the model name
$model_name = strtolower(class_basename($this));
return "/$model_name/$year/$month/$day/$attribute_name";
}
protected function getImageFilename($value, $attribute_name)
{
// Define the specific filename logic for each model
$timestamp = time();
return $attribute_name . '_' . "$timestamp" . '_' . md5($value.$timestamp) . '.jpg';
}
}
and this is me using it in my user model to upload avatars
...
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use CrudTrait, HasRoles;
...
use ImageTrait;
...
public function setAvatarAttribute($value)
{
$this->setImageUrlAttribute($value, 'avatar');
}
Also, this was another important step, I needed to make sure i had a UserPolicy created like so:
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class UserPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
//
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, User $model): bool
{
//
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
//
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, User $model): bool
{
//
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, User $model): bool
{
//
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, User $model): bool
{
//
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, User $model): bool
{
//
}
public function uploadFiles(User $user): bool
{
return true;
}
}
to give vapor the permission to upload in the client side
with this provider
<?php
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Policies\UserPolicy;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
User::class => UserPolicy::class,
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
//
}
}
which is put in app.php in the providers
'providers' => [
...
App\Providers\AuthServiceProvider::class,
Finally, I was having trouble getting larvel-vapor client upload understanding the backpack auth, due the guard being different.
Setting the guard to null in base.php works i.e.
'guard' => null,
However I has a work around to run this in the crud which was a hack but worked:
$user = backpack_auth()->user();
Auth::login($user, $remember = true);
CRUD::addField([ ...
This was a hack that worked, and sure a better way, but thats what i landed on before i wiped the guard.
So thats about it. Im sure i have done some things in a round aout way, so very happy to hear improvements and so on! I especially think my lack of totally understanding on the AuthServiceProvider means im doing something the hard way around.
Anyway, once all of this is done, i simply push production or staging and 5 min later up on vapor :-)
One more note, make sure you have supplied VAPOR_API_TOKEN in github secrets, just ran into that on a new project
Thank you SO MUCH @malek77z . Seems like we can do quite a bit to improve the DX for Vapor users. That's GREAT. It'll take us a while to process this and come up with appropriate, more general solutions. Seems like a Size: Medium
(1 week) and we don't have the bandwidth for that right now. But we will soon, after the dust settles with the v6 launch 😉
Thanks again, we super appreciate it!
Feature Request
What's the feature you think Backpack should have?
Backpack should be able to work out-of-the-box on Laravel Vapor and/or provide clear steps on how to deploy it there.
Have you already implemented a prototype solution, for your own project?
No but @malek77z has 👀
Do you see this as a core feature or an add-on?
Core, most likely.