key-value and json arrays: {closure}(): Argument #1 ($value) must be of type ?string, array given

Hey all, I'm working on an admin section for my portfolio page. The data is from a bunch of Markdown files with some frontmatter; this frontmatter has been converted and stored as json in a sqlite database. When editing the Project, I chose to display the frontmatter with a KeyValue form component. After trying to save an instance without changing the values (meaning they should be valid), I get the following error:
Filament\Forms\Components\KeyValue::Filament\Forms\Components\{closure}(): Argument #1 ($value) must be of type ?string, array given
Filament\Forms\Components\KeyValue::Filament\Forms\Components\{closure}(): Argument #1 ($value) must be of type ?string, array given
Could it be that an array value is incompatible with the KeyValue component? Or am I doing something wrong ?
Solution:
Looking at it, you are storing as an array but we load that magically and it renders ok. But it should always be stored as a string in a keyValue.... you could look to cast it to a string with attributes. If you are insistent on storing it as an Array you can use this model class for your project using the get/set attributes ```php...
Jump to solution
26 Replies
toeknee
toeknee3mo ago
Yes 'KeyValue' needs to be a string as it's the value
Matteo G
Matteo G3mo ago
ah, and is there no way to convert it to and from an array on save ? I've been trying to use mutateFormDataBeforeSave, but it doesn't look to be triggering ?
toeknee
toeknee3mo ago
Is it the key value pair field or the keyValue?
Matteo G
Matteo G3mo ago
the value can be an array (I know which ones are arrayis in advance)
toeknee
toeknee3mo ago
So the field should be cast as an Array on the model
Matteo G
Matteo G3mo ago
done
toeknee
toeknee3mo ago
If that's done it should be ok then...
Matteo G
Matteo G3mo ago
an example of data is this:
{
"title": "example title"
"stack":["typescript","another tech"]
}
{
"title": "example title"
"stack":["typescript","another tech"]
}
Shows up fine, with the stack element separated by , but then the error happens when I try to save so could it be that it gets interpreted as an array when it gets sent back to be saved ? maybe because of the cast ?
toeknee
toeknee3mo ago
It will be failing at "stack":["typescript","another tech"] surely? because stack is an array
Matteo G
Matteo G3mo ago
yes, but only on save I think (it displays fine). Is there a way to handle saving this field manually ? I thought maybe mutateFormDataBeforeSaving would work, but it does not even trigger, it seems
toeknee
toeknee3mo ago
Can you provide the field code
Matteo G
Matteo G2mo ago
Forms\Components\KeyValue::make('fm'),
Forms\Components\KeyValue::make('fm'),
upping this since I'm still trying to make this work; I tried using mutateFormDataBeforeSave and handleRecordUpdate, but neither gets called before the error is thrown 😦
toeknee
toeknee2mo ago
Can you provide an example repo and we can test it
Matteo G
Matteo G2mo ago
GitHub
GitHub - matfire/filament-kv-repro
Contribute to matfire/filament-kv-repro development by creating an account on GitHub.
toeknee
toeknee2mo ago
Thanks, why are you saving it as an array to the DB?
Solution
toeknee
toeknee2mo ago
Looking at it, you are storing as an array but we load that magically and it renders ok. But it should always be stored as a string in a keyValue.... you could look to cast it to a string with attributes. If you are insistent on storing it as an Array you can use this model class for your project using the get/set attributes
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
use HasFactory;

protected $fillable = ["fm", "content"];
protected $casts = [
"fm" => "array"
];


public function getFmAttribute($value)
{
$fm = json_decode($value, true);

if($tags = data_get($fm, 'tags')) {
$fm['tags'] = implode(',', $tags);
}

return $fm;
}

public function setFmAttribute($value)
{
if($tags = data_get($value, 'tags')) {
$value['tags'] = explode(',', $tags);
}
// dd($value);
return $value;
}
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
use HasFactory;

protected $fillable = ["fm", "content"];
protected $casts = [
"fm" => "array"
];


public function getFmAttribute($value)
{
$fm = json_decode($value, true);

if($tags = data_get($fm, 'tags')) {
$fm['tags'] = implode(',', $tags);
}

return $fm;
}

public function setFmAttribute($value)
{
if($tags = data_get($value, 'tags')) {
$value['tags'] = explode(',', $tags);
}
// dd($value);
return $value;
}
}
toeknee
toeknee2mo ago
It will work for tags only
Matteo G
Matteo G2mo ago
hey, thank you so much! Works like a charm on the filament admin! but now it doesn't load properly on the rest of the website because it gets converted to a string (so looping over it does not work anymore :(). But I could change that section to explode the string it receives, though it seems a bit weird and inefficient (imploding to explode it right after...) ? They are stored as an array because of the way Eloquent does casts automatically, so i don't know how hard it would be to customize that process...
toeknee
toeknee2mo ago
Right I think I get you, why not just store it as strings and then explode on the frontend? It is a string by default, the only difference seems that you have some values stored as an array some how for tags
Matteo G
Matteo G2mo ago
I was also thinking of that, but I don't know how hard it would be to change how Laravel handles converting to an array. This is the import script I'm using on the actual website.
$fmParser = new FrontMatterParser(new SymfonyYamlFrontMatterParser);
$files = Storage::files('markdown/projects');
foreach ($files as $file) {
$parsed = $fmParser->parse(Storage::get($file));
$project = Project::query()->updateOrCreate(['slug' => MarkdownUtils::getSlug($file)], [
'path' => $file,
'published_at' => $parsed->getFrontMatter()['date'],
'content' => $parsed->getContent(),
'fm' => $parsed->getFrontMatter(),
]);
$project->save();
}
$fmParser = new FrontMatterParser(new SymfonyYamlFrontMatterParser);
$files = Storage::files('markdown/projects');
foreach ($files as $file) {
$parsed = $fmParser->parse(Storage::get($file));
$project = Project::query()->updateOrCreate(['slug' => MarkdownUtils::getSlug($file)], [
'path' => $file,
'published_at' => $parsed->getFrontMatter()['date'],
'content' => $parsed->getContent(),
'fm' => $parsed->getFrontMatter(),
]);
$project->save();
}
getFrontMatter returns an array representing the frontmatter on a markdown file. It is stored in the database as a JSON column
toeknee
toeknee2mo ago
Dude remove my getter/setter and just change:
'fm' => $parsed->getFrontMatter(),
'fm' => $parsed->getFrontMatter(),
to
'fm' => implode(',', $parsed->getFrontMatter()),
'fm' => implode(',', $parsed->getFrontMatter()),
and re-run the import and everything will be fine.
Matteo G
Matteo G2mo ago
just tried it, getting:
ErrorException Array to string conversion
ErrorException Array to string conversion
on the changed line 😦
toeknee
toeknee2mo ago
ahh sorry!!! can you show the getFrontMatter() function
Matteo G
Matteo G2mo ago
I didn't make it, it comes from the commonmark packages: League\CommonMark\Extension\FrontMatter\Data\SymfonyYamlFrontMatterParser League\CommonMark\Extension\FrontMatter\FrontMatterParser I'll see if I can find some readable source code
toeknee
toeknee2mo ago
Ok so it's ok you can do this instead
$fmParser = new FrontMatterParser(new SymfonyYamlFrontMatterParser);
$files = Storage::files('markdown/projects');
foreach ($files as $file) {
$parsed = $fmParser->parse(Storage::get($file));
$fm = $parsed->getFrontMatter();
foreach($fm as $key => $value) {
$fm[$key] = $key === 'tags' ? implode(',', $value) : $value;
}

$project = Project::query()->updateOrCreate(['slug' => MarkdownUtils::getSlug($file)], [
'path' => $file,
'published_at' => $parsed->getFrontMatter()['date'],
'content' => $parsed->getContent(),
'fm' => $fm,
]);
$project->save();
}
$fmParser = new FrontMatterParser(new SymfonyYamlFrontMatterParser);
$files = Storage::files('markdown/projects');
foreach ($files as $file) {
$parsed = $fmParser->parse(Storage::get($file));
$fm = $parsed->getFrontMatter();
foreach($fm as $key => $value) {
$fm[$key] = $key === 'tags' ? implode(',', $value) : $value;
}

$project = Project::query()->updateOrCreate(['slug' => MarkdownUtils::getSlug($file)], [
'path' => $file,
'published_at' => $parsed->getFrontMatter()['date'],
'content' => $parsed->getContent(),
'fm' => $fm,
]);
$project->save();
}
something like that should work
Matteo G
Matteo G2mo ago
the import worked! thanks! just generalized the code a bit to convert all arrays into a comma-separated string to ease the transition of all other models.
foreach($fm as $key => $value) {
$fm[$key] = is_array($value) ? implode(',', $value) : $value;
}
foreach($fm as $key => $value) {
$fm[$key] = is_array($value) ? implode(',', $value) : $value;
}
Want results from more Discord servers?
Add your server