Storing multiple filenames at once when uploading multiple files

I'm trying to save multiple attachements at once and store their filenames also, so as docs say I use https://filamentphp.com/docs/3.x/forms/fields/file-upload#storing-original-file-names-independently and on my model protected $casts = [ 'filename' => 'array', ]; However, this results in all filenames being stored under filename column instead each filename for each attachement. I did find a workaround by manually updating filename with after() method on CreateAction, but is there a better more straightforward way of doing this?: CreateAction::make()->after(function ($record, $data) { foreach($data['url'] as $url) { $asset = Asset::where('url', $url)->first(); $asset->filename = $data['filename'][$url]; $asset->save(); } })
No description
No description
21 Replies
lukaveck1
lukaveck1OP5w ago
I'm still looking for help on this one, it's real pain in the ass - I've also tried creating observer for creating Asset and can't get it to work:
Forms\Components\FileUpload::make('url')
->label($label)
->directory($directory)
->minFiles(1)
->multiple()
->storeFileNamesIn('filename')
->columnSpanFull();
Forms\Components\FileUpload::make('url')
->label($label)
->directory($directory)
->minFiles(1)
->multiple()
->storeFileNamesIn('filename')
->columnSpanFull();
Asset Model:
class Asset extends Model
{
use HasFactory, HasAccount;

protected $fillable = [
'url',
'filename',
'account_id',
];

protected $casts = [
'filename' => 'array'
];
class Asset extends Model
{
use HasFactory, HasAccount;

protected $fillable = [
'url',
'filename',
'account_id',
];

protected $casts = [
'filename' => 'array'
];
and I'm getting Illuminate\Database\Grammar::parameterize(): Argument #1 ($values) must be of type array, string given, called in /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Query/Grammars/Grammar.php on line 1060 I don't understand anymore.
-Ni3K-
-Ni3K-5w ago
I think it's your cast mate you have the file_upload as "url", but the cast is filename, you should cast the database field, which I think in your case, you call it "url" protected $casts = [ 'url' => 'array' ]; it should be this Can you explain what is URL and what is filename. Is "filename" a database field too?
lukaveck1
lukaveck1OP5w ago
URL is basically the auto generated name and filename is original filename.
-Ni3K-
-Ni3K-5w ago
okay then the original cast is fine, but the FileUpload field should be FileUpload::make('filename'), not URL if URL is auto generated, and the users dont have to see it, you can use observers. if you want them to see it, use a placeholder
lukaveck1
lukaveck1OP5w ago
No, look at docs:
No description
-Ni3K-
-Ni3K-5w ago
ahh I see. maybe this is a bit out of my depth try putting a cast on both url and filename, see if that helps
lukaveck1
lukaveck1OP5w ago
No that doesn't work
Antea
Antea5w ago
StoreFileNamesIn is only an additional optional column to store the actual filenames in for downloading or showing in UI, you have to have 2 columns if you want that, 1 for the actual random generated filenames and 1 for storeFileNamesIn
lukaveck1
lukaveck1OP5w ago
I have two columns in DB, one column 'url' and one 'filename', but it doesn't work. I did it with a very ugly hack, but this seems like a bug on filament side of things. By default it doesn't work, so I resorted to observer creating() method, which I thought is gonna work fine, just pass in the filename, but I got a very weird bug basically. One of the files would incorrectly save filename always when uploading multiple files (it would save all the filenames of uploaded files, instead of just one). Lastly I just said to update the name after record is created and this works as intended now.
class AssetObserver
{
public function created(Asset $asset) {
if ($this->resourceType($asset) === Profile::class) {
$this->updateFilename($asset);
}
}

/**
* Handle the Asset "creating" event.
*/
public function creating(Asset $asset): void
{
$urls = $asset->getAttribute('url');

if (is_array($urls) && count($urls) > 0) {
$firstUrl = array_shift($urls);
$asset->setAttribute('url', $firstUrl);

foreach ($urls as $url) {
$newAsset = clone $asset;
$newAsset->setAttribute('url', $url);
$newAsset->setAttribute('account_id', null);
$newAsset->save();
}
}
}

//TODO: This is ugly hack to set correct filename on profile assets, because doing it in creating() method didn't work correctly - there's weird behaviour when uploading multiple files. Doing it as docs say doesn't work https://filamentphp.com/docs/3.x/forms/fields/file-upload#storing-original-file-names-independently

protected function updateFilename($asset) {
$asset->update(['filename' => $this->parseFilename($asset->getAttribute('url'), $asset->getAttribute('filename'))]);

}

protected function parseFilename($url, $filenames) {
return array_filter($filenames, function ($key) use ($url) {
return $key === $url;
}, ARRAY_FILTER_USE_KEY);
}

protected function resourceType($asset) {
return $asset->getAttribute('resourceable_type');
}
}
class AssetObserver
{
public function created(Asset $asset) {
if ($this->resourceType($asset) === Profile::class) {
$this->updateFilename($asset);
}
}

/**
* Handle the Asset "creating" event.
*/
public function creating(Asset $asset): void
{
$urls = $asset->getAttribute('url');

if (is_array($urls) && count($urls) > 0) {
$firstUrl = array_shift($urls);
$asset->setAttribute('url', $firstUrl);

foreach ($urls as $url) {
$newAsset = clone $asset;
$newAsset->setAttribute('url', $url);
$newAsset->setAttribute('account_id', null);
$newAsset->save();
}
}
}

//TODO: This is ugly hack to set correct filename on profile assets, because doing it in creating() method didn't work correctly - there's weird behaviour when uploading multiple files. Doing it as docs say doesn't work https://filamentphp.com/docs/3.x/forms/fields/file-upload#storing-original-file-names-independently

protected function updateFilename($asset) {
$asset->update(['filename' => $this->parseFilename($asset->getAttribute('url'), $asset->getAttribute('filename'))]);

}

protected function parseFilename($url, $filenames) {
return array_filter($filenames, function ($key) use ($url) {
return $key === $url;
}, ARRAY_FILTER_USE_KEY);
}

protected function resourceType($asset) {
return $asset->getAttribute('resourceable_type');
}
}
I really tried everything to avoid doing this on created() method, because it should be pretty straight forward, but it isn't. I know that creating() method looks weird, but that's how it was done by someone else for some other resource where we are uploading assets, but I had to do it on some other resource now as well, but with filenames stored separately. I tried to tweak it for my cause, but to no success.
toeknee
toeknee5w ago
why not just use spatie media upload?
lukaveck1
lukaveck1OP5w ago
Well this was already implemented as said and worked good for other resource (but saving filenames there wasn't necessary) and then when I started to implement it for another resource the docs seemed pretty straight forward on how it should work if we have separate column for filenames, but then I just couldn't get it to work. It was a quick 20min added feature at first, but with new demands this got ugly after trying to implement storing multiple files with filenames. What's even weirder is that I can't find a working example online as docs say.
toeknee
toeknee5w ago
Do you need to be using preserveFilenames() ?
lukaveck1
lukaveck1OP5w ago
Yes, I have to save filenames, but I don't need that method for it on FileUpload.
toeknee
toeknee5w ago
it shows in the PR how it works: https://github.com/filamentphp/filament/issues/2904 Fairly straight forward
GitHub
feat request: store uploaded files in another column · Issue #2904...
Package filament/filament Package Version v2 Laravel Version v9 Livewire Version v2 PHP Version 8 Bug description to introduce a method to FileUpload component to store file's original name in ...
lukaveck1
lukaveck1OP5w ago
I'll check
lukaveck1
lukaveck1OP5w ago
I'll check it out now. Here'es recording so we understand what's the problem - again for each record it saves all filenames of uploaded files instead of one by one.
lukaveck1
lukaveck1OP5w ago
I don't have a zip (attachement) of assets, I have separate assets on their own
toeknee
toeknee5w ago
No that's right... how would it save them 1 by 1 against a single db? It wouldn't. That's where spatie media would work instantly, storing it one 1 by 1 record. For that you need to explode on , in the relationship manager.
lukaveck1
lukaveck1OP5w ago
I have relationship manager where I'm returning my AssetUpload class, not sure where to explode these things now:
class ProfileBaseAssetsRelationManager extends BaseAssetsRelationManager
{
public static string $assetDirectory = 'profile-assets';

public string $assetLabel = 'Profile assets';

public function form(Form $form): Form
{
return $form
->schema([
AssetUpload::make($this->assetLabel, self::$assetDirectory),
]);
}
class ProfileBaseAssetsRelationManager extends BaseAssetsRelationManager
{
public static string $assetDirectory = 'profile-assets';

public string $assetLabel = 'Profile assets';

public function form(Form $form): Form
{
return $form
->schema([
AssetUpload::make($this->assetLabel, self::$assetDirectory),
]);
}
But thanks for giving me directions, I think that's my knowledge hole in this whole thing and what I should be doing in the first place.
toeknee
toeknee5w ago
on the assets table in the manager for the name you are showing do: ->seperator(',')
lukaveck1
lukaveck1OP5w ago
That doesn't really solve anything, I'll keep the workaround for now and try with Spatie Media Upload as you suggested when I have time.

Did you find this page helpful?