SolvedCRUD Bug image field, store in fake field (store as base64, not path)
โ๏ธAccepted Answer
Hi everyone. Allow me to post this to this closed topic. It could be useful to someone.
Looking for a solution to @Jimmylet post I landed here. I finally managed to store image paths into a fake field. It's not the best solution, as I didn't rewrite the backpack code to add this functionality, but it works. I'm not sure it's a backpack bug, but it's just something the fake fields are not thought for. Nothing mentioned in the documentation though (whether it should work or shouldn't).
As far as I could guess, the moment in the store call stack where actual values are written into the fake fields is inside some of the backpack structure (Model.php). To modify that behavior I should override quite a lot of files, and I didn't want to mess them up. Not now. So, why previous comments didn't work for me:
The getters and setters like getSomeFieldAttribute don't work as we would expect for fake fields. They do work for actual fields in the table. If you try to do setExtrasAttribute and do there the image processing, it also doesn't work, as the extras field is at the same time a "protected $fake" field and a "protected $appends" field.
So, what I did is to surface to the controller itself and insert the following code into the update and create methods. The code basically checks for fields ending with _image, then stores images to disk, gets their filename, and replaces the fake field data (data:image) with the recently created filename. Voliร ! Don't pay attention to the specific code that you won't need. This code is mostly the part of the setImageAttribute function where images are processed for actual DB fields. The model keeps the getImageAttribute (second part of the code) with the getAttributeValueExtended trick to parse the value to store and read it json_decoded from the fake field.
In the controller:
public function update(UpdateRequest $request)
{
$this->addDefaultPageFields(\Request::input('template'));
$this->useTemplate(\Request::input('template'));
$fields = array_keys(request()->all());
foreach ($fields as $field) {
if (Str::endsWith($field, 'image'))
$request[$field] = $this->processImage($request, $field, 'extras');
}
return parent::updateCrud($request);
}
/**
* Stores the actual image to disk and returns the stored filename. Deletes previous file from disk, if any.
*
* @param $request
* @param $attribute_name
* @param null $store_in If not null, image is stored in a fake field
* @return null|string
*/
private function processImage($request, $attribute_name, $store_in = null)
{
$value = $request[$attribute_name];
$prev_value = $this->crud->getCurrentEntry()->{$attribute_name};
$type = 'pages';
$destination_path = env('IMAGES_STORAGE_WRITE_PATH') . $type;
// if the image was erased
if ($value == null) {
$this->deleteImage($prev_value);
$value = null;
}
// if a new image was loaded
if (Str::startsWith($value, 'data:image')) {
$this->deleteImage($prev_value);
$image = \Image::make($value);
switch ($image->mime()) {
case 'image/png':
$extension = '.png';
break;
default:
$extension = '.jpg';
}
$filename = $type . '_' . $attribute_name . '_' . md5($value.time()) . $extension;
\Storage::disk(env('IMAGES_STORAGE_DISK'))->put($destination_path . '/' . $filename, (string)$image->stream());
$value = $filename;
}
return basename($value);
}
private function deleteImage($value)
{
$type = 'pages';
if (!empty($value)) {
$file = basename($value);
$file_path = env('IMAGES_STORAGE_WRITE_PATH') . Str::plural($type) . '/' . $file;
\Storage::disk(env('IMAGES_STORAGE_DISK'))->delete($file_path);
}
}
In the model:
public function getHeader1ImageAttribute()
{
return $this->retrieveImage('header_1_image', 'extras');
}
public function getHeader2ImageAttribute()
{
return $this->retrieveImage('header_2_image', 'extras');
}
public function getHomeCoverImageAttribute()
{
return $this->retrieveImage('home_cover_image', 'extras');
}
private function retrieveImage($attribute_name, $stored_in = null)
{
$type = Str::snake(class_basename($this));
$value = $this->getAttributeValueExtended($attribute_name, $stored_in);
if ($value && Str::contains($value, '_default.')) {
return env('IMAGES_STORAGE_READ_PATH'). '/' . $value;
}
if ($value) {
return env('IMAGES_STORAGE_READ_PATH') . Str::plural($type) . '/' . $value;
}
}
private function getAttributeValueExtended($attribute_name, $stored_in = null)
{
if ($stored_in) {
if (isset(json_decode($this->{$stored_in})->{$attribute_name}))
return json_decode($this->{$stored_in})->{$attribute_name};
return null;
}
return $this->{$attribute_name};
}
Hope this helps someone!
Hello,
I try to store the path of an image in a fake field. And it converts it to "base64", it's a problem.
It works when I specify a field other than "extras" and I create my mutator.
As I have several images on my pages (I use PageManager for simple pages). I need to store a different number on each page, which is why I would like a "fake field" solution, it would keep it flexible.
I try to follow the solution given here by @tabacitu Laravel-Backpack/CRUD#400.
Whatever I do, if I try to store an image in "extras", it converts it to base64 and that bug and even with the following code
I try to have something dynamic, otherwise I could post only one entry per column in the DB.
@tabacitu has given information to try to solve this problem. But I can not create it correctly.
So, is there a way to store multiple images from different fields on the same page in a fake-field?
Thank you
PS : Sorry for my english.