Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jcc5018's avatar

How do I create a group of SELECTs with a foreach loop in filament

I am having trouble with my form creation in that I dont know how to go about inserting basic php to generate form data.

In this instance, I want to loop through all my Rating tags in the database along with the choices for each.

But I dont know how to do this within the schema The available rating options may change so it needs to be generated dynamically

 Section::make('Ratings')
                       ->schema([
                           
                               foreach($model->hobbyRatings()->where('parent_id',null) as $rating){
 Select::make($rating->tag)
   ->options(function (Builder $query) use ($rating) {
 return $query->where('parent_id', $rating->id)->get();
                                         });
    }
                          

                       ])
                       
                       ->columnSpanFull(),

Also is it possible to save certain parts of the form to insert elsewhere like address fieldsets?

0 likes
17 replies
LaryAI's avatar
Level 58

To create a group of SELECTs with a foreach loop in Filament, you need to build the schema dynamically before passing it to the Section::make() method. You cannot use a foreach loop directly within the schema() method's array. Instead, you should create the schema array beforehand and then pass it to the schema() method.

Here's how you can do it:

// First, build the dynamic schema array
$dynamicSchema = [];

foreach ($model->hobbyRatings()->where('parent_id', null)->get() as $rating) {
    $dynamicSchema[] = Select::make($rating->tag)
        ->options(function (Builder $query) use ($rating) {
            return $query->where('parent_id', $rating->id)->pluck('name', 'id');
        });
}

// Then, create the section with the dynamic schema
$section = Section::make('Ratings')
    ->schema($dynamicSchema)
    ->columnSpanFull();

In the above code, we're first iterating over the hobbyRatings and building an array of Select fields. Each Select is configured with its options based on the related ratings. After the loop, we pass the $dynamicSchema array to the Section::make() method.

Regarding your second question about saving certain parts of the form to insert elsewhere, you can create reusable form components or partials. Here's an example of how you might create a reusable address fieldset:

// Create a reusable address fieldset as a function or a class method
function addressFieldset()
{
    return [
        TextInput::make('address_line_1')->label('Address Line 1'),
        TextInput::make('address_line_2')->label('Address Line 2'),
        TextInput::make('city')->label('City'),
        TextInput::make('state')->label('State'),
        TextInput::make('zip')->label('Zip Code'),
        // ... other address fields
    ];
}

// Use the addressFieldset in your form schema
$formSchema = [
    // ... other form fields
    Section::make('Address Details')
        ->schema(addressFieldset())
        ->columnSpanFull(),
    // ... other form fields
];

In this example, addressFieldset() is a function that returns an array of form fields related to an address. You can then call this function wherever you need to include the address fields in your form schemas.

jcc5018's avatar

@LaryAI Where does the $section part fit in within

 public static function form(Form $form): Form
    {
        return $form ->schema([ ...])

martinbean's avatar

@jcc5018

This reply was automagically generated by our local A.I. Think of it as a one-off attempt to instantly solve your problem. It will not respond to further replies.

jcc5018's avatar

@martinbean i was hoping a direct reply would help. Any idea what to do with that information if its the correct approach? I am not used to this pattern of doing things

kokoshneta's avatar

There are presumably various different ways to do this, perhaps better ones, but I would usually generate the content in a separate method in the resource class. I don’t fully understand how your rating table looks or works, but it looks like you have hierarchic ratings, so each rating has a parent ID referring to another rating in the same table, correct? And you want to make a select dropdown for each of your model’s associated top-level ratings, with that rating’s children being the selectable options?

If that’s the case, I would guess something like this ought to do it.

First, make sure you have relationships in your Rating model to fetch children and parent:

class Rating extends Model {
	public function children() {
		return $this->hasMany(static::class, 'parent_id');
	}

	public function parent() {
		return $this->belongsTo(static::class, 'parent_id');
	}
}

Next, use these to eager-load nested ratings before the loop in your custom resource method, so you avoid n+1 issues (I’m guessing the text that should appear in the dropdown is the tag for each child rating?):

protected static function getRatings(Model $model) {
	$ratings = [];
	$model->loadMissing('hobbyRatings.children');

	foreach ($model->hobbyRatings as $rating) {
		$ratings[] = Select::make($rating->tag)
			->options($rating->children->pluck('tag', 'id'))
			->label('...if you want')
		;
	}

	return $ratings;
}

And finally use this method to generate the content for the ratings section:

public static function form(Form $form) : Form {
	return $form
		->schema([
			Section::make('Ratings')
				->schema(fn (Model $record) => static::getRatings($record))
		])
	;
}

You can of course also just inline the whole thing inside the ->schema() closure, if you prefer, instead of using a custom class method:

public static function form(Form $form) : Form {
	return $form
		->schema([
			Section::make('Ratings')
				->schema(function (Model $record) {
					$ratings = [];
					$record->loadMissing('hobbyRatings.children');
				
					foreach ($record->hobbyRatings as $rating) {
						$ratings[] = Select::make($rating->tag)
							->options($rating->children->pluck('tag', 'id'))
							->label('...if you want')
						;
					}
				
					return $ratings;
				})
			,
		])
	;
}
jcc5018's avatar

@kokoshneta Thanks, At this time is is not yet working out though.

This is in the Hobby Resource.

Ratings are in the tag table with tag_type = 13 ('Rating") as defined in the taxonomy table.

TAG: {id,tag,slug,description, parent_id, tag_type TAGGABLE: tag_id, taggable_type , taggable_id

in Hobby model I have:

    public function hobbyRatings(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable')->where('tag_type', 13)
                    ->withTimestamps();
    }

This all works in the previous attempt at a laravel project before i tried filament. (but a bunch of other things broke and it was just easier to start over without the clutter. So I am trying filament, but I dont really understand how to modify things for scenarios such as this. Basically a nested form.

I have a lot of models with nested forms so I need to understand how this works also. all the references to schema this and that, and what I could put in that tag I think is part of the confusion. If I can just create a basic php/blade form at this point and insert it somehow I think that would make things easier.

The bootstrap version of this looked like this: The new version is using tailwind with livewire and/or filament

 @foreach($ratings->whereNull('parent_id') as $rating)

            <div class="mb-3">
                <div class="form-row  align-items-center">

                        <label class="control-label control-label-left "
                               for="{{$rating->slug}}">{{$rating->tag}}</label>

                        <select name="ratings[]" id="{{$rating->slug}}"
                                class="form-control" data-bs-role="select">
                            <option selected value="">Select</option>
                            @foreach($ratings->where('parent_id', $rating->id) as $option )
                                <option value="{{ $option->id }}"
                                    {{in_array($option->id, $hobby->hobbyRatings->pluck('id')->toArray()) ?
                                    'selected' :
                                    '' }}
                                >
                                    {{ $option->tag }} - {{ $option->description }}
                                </option>
                            @endforeach
                        </select>

                </div>

I could just make this easy and make these ratings as columns in the hobby table, but that wouldnt allow for additional rating options in the future unless I keep restructuring my database. Right now there should be 6 parent level ratings, with 4-8 options each

I also have this scope in the TAG model

 public function scopeType($query, $type)
    {
        return $query->where('tag_type', $type)->with('parent', 'taxonomies');
    }


//I added this one as well in an attempt to Id tag types by name instead of number, but I am not sure if I have it set up correctly

    public function tagType($type)
    {
        return DB::table('tags')
                 ->select('tags.*')
                 ->join('taxonomies', 'taxonomies.id', '=', 'tags.tag_type')
                 ->where('taxonomies.type', $type)->with('parent')
                 ->get();
    }

I will need to filter by these ratings in other parts of my site, both front end and backend, so if this would be better off as a livewire component that I can structure normally, then let's try that. I just don't know how to get the component as a nested form

kokoshneta's avatar

@jcc5018 Okay, let’s start with the model and relationship side of things and leave Filament for later.

So, if I understand you correctly, both ratings and rating options are actually Tag models, correct? What tag type do the rating options have? Are they also type 13, or is there a taxonomic type for rating options? If there aren’t, I would suggest making one and then changing them; your taxonomy table would then have something like this:

  id       type       
 ---- --------------- 
  13   Rating         
  14   Rating Option  

And your tag table would have something along these lines:

  id      tag          slug           description         parent_id   tag_type  
 ---- ------------ ------------ ------------------------ ----------- ---------- 
  25   Agree        agree        How much do you agree?   NULL              13  
  26   Not at all   not-at-all   Not at all               25                14  
  26   Very much    very-much    Very much                25                14  

The children() and parent() relationships would then be in the Tag class, and you should be able to use $hobby->hobbyRatings->children to retrieve your rating options; or Hobby::with('hobbyRatings.children')->get() to retrieve all hobbies with their ratings and rating options eager loaded.

Do you have things set up in this way so that works?

jcc5018's avatar

@kokoshneta Yes the Rating "tags" are in a tags table along with 900 other tags of about 60 taxonomies (tag_types) There is no reason to make rating option a separate taxonomy.

I have no problem with the sample code above for blade where it loops through the parents and then loops through to find the children. But I am not using blade, or dont know how to reference a blade view within filament.

with a simple get ratings call in the controller of the original version of my project. What I dont understand is how to display loops or Form parts within Filament schema. Or where does the function call go?

within ->schema (FUNCTION) , after ->schema ()->FUNCTION , or somewhere totally different? Or as an includes ("rating_form")

...

 protected function getRatings ()
    {
        return Tag::type(13)->get();
    }
jcc5018's avatar

@kokoshneta I am going to download the nested form plugin to see if that helps. wont be able to do much with it today though.

kokoshneta's avatar

I have no problem with the sample code above for blade where it loops through the parents and then loops through to find the children.

The snippet you posted from the old Bootstrap version seems to do that correctly, using a simple collection that’s filtered. But the version you posted in the question would result in an extra, unneeded database query for every rating – that was the reason for the refactoring.

Also, in the code snippet in your last post, you’re fetching all ratings with Tag::type(13)->get(). In the original code in the question, you’re only showing ratings already associated with the hobby by doing $hobby->hobbyRatings()->where('parent_id', NULL). I assume you probably want to show all ratings.

But I am not using blade, or dont know how to reference a blade view within filament.

You can make your own custom components with their own custom Blade templates, but it’s more work than I think you need for this. In general, while Filament components are evaluated to Blade templates, the fact that you don’t need to deal with the Blade templates directly is one of its greatest strengths – you’d normally want to avoid plain Blade templates in Filament panels.

What I dont understand is how to display loops or Form parts within Filament schema. Or where does the function call go? within ->schema (FUNCTION) , after ->schema ()->FUNCTION , or somewhere totally different? Or as an includes ("rating_form")

I’m not sure I quite understand you here.

Each Filament component (including the base form component itself) has a ->schema() function which takes a single parameter, and that parameter is evaluated and rendered as the children of the component. Layout components like Section don’t really have any contents other than their children, so essentially, whatever you pass as the parameter to ->schema() is what's rendered on the page.

To get multiple components, you pass an array – or a function call (or closure) that returns an array. If you use a closure, you can access a variety of utilities by injecting them into the closure using specific parameter names. For example, in a HobbyResource, you can access the current Hobby model by injecting $record.

In essence, what you’re looking for here is something like this:

Section::make('Ratings')
	->schema([
		Select::make($rating1->tag)
			->options([
				$rating1->option1,
				$rating1->option2,
				$rating1->option3,
			)],
		Select::make($rating2->tag)
			->options([
				$rating2->option1,
				$rating2->option2,
				$rating2->option3,
			)],
		Select::make($rating2->tag)
			->options([
				$rating3->option1,
				$rating3->option2,
				$rating3->option3,
			)],
		... etc.
	])
;

– except with dynamic, repeating components, of course. To achieve that, you want to pass a function call or closure that returns an array of components. The actual loop is then inside this function/closure.

You can use any function or method you want, really, as long as it returns the correct array. Something like this, using a closure:

Section::make('Ratings')
	->schema(function (Model $record) {
		$components = [];
		$allRatings = Tag::type(13)->get();
		$modelRatings = $record->hobbyRatings;

		foreach ($allRatings->where('parent_id', NULL) as $rating) {
			$component = Select::make($rating->tag)
				->options($allRatings->where('parent_id', $rating->id)->pluck('tag', 'id');

			if ($modelRatings->contains($rating->id)) {
				$component->default($modelRatings->where('parent_id', $rating->id)->first()->id);
			}

			$components[] = $component;
		}

		return $components;
	})
;

(I’m assuming here that each rating can only have one option chosen.)

jcc5018's avatar

@kokoshneta Alright, we are getting closer. And i appreciate your help thus far. I'm not knowledgeable enough to even know where to begin on much of this.

The individual ratings and associated options are displaying, but it is not saving the data to the taggables table based on the hobbyRatings relationship. I tried adding the ->relationships() designation, but that just made 6 selects with the same heading, and all the relation tags

had to modify a bit as there was no return.

 Section::make('Ratings')
                       ->schema(function (Hobby $record) {
                           $components   = [];
                           $allRatings   = Tag::type(13)->get();
                           $modelRatings = $record->hobbyRatings;

                           foreach (
                               $allRatings->where('parent_id', null) as $rating
                           ) {
                               $component = Select::make($rating->tag)
                                                  ->options($allRatings->where('parent_id',
                                                      $rating->id)->pluck('tag',
                                                      'id'));

                               if ($modelRatings->contains($rating->id)) {
                                   $component->default($modelRatings->where('parent_id',
                                       $rating->id)->first()->id);
                               }

                               $components[] = $component;
                           }

                           return $components;
                       })->columnSpanFull(),

Now i saw some instructions for morphTo relationships in the docs and was wondering if that would be applicable here, but the samples didn't really seem to match what I am doing so I'm not sure.

I'm also wondering if the relationship manager could be utilized except without the table, and add new button.

IE set the relationship and just insert the form as each hobby would only have one set of ratings. But that still wont dynamically create the form as above, so maybe not.

Theres a bunch in the docs that looks like a potential solution but they aren't explained enough for me to really know how to utilize them properly.

kokoshneta's avatar

The individual ratings and associated options are displaying, but it is not saving the data to the taggables table based on the hobbyRatings relationship.

Ah yes. My mind went half-way there last night, but I was too tired to think it through to its logical conclusion.

The fundamental problem here, I think, is that you have quite an unusual design setup: a dropdown is expected to relate to a single column (dropdown name = database column name), with each dropdown option yielding a single value that can be stored in that column.

Your setup is unusual in that your dropdown and your dropdown options are identical, that is, they both correspond to models from the same database table. That means each dropdown option actually corresponds to a new, separate relationship, as far as I understand it. That’s what makes it so unusual, and quite hard to understand conceptually. In fact, I think I need to understand the setup a bit more.

Firstly, what do these ‘ratings’ actually represent? What kind of ratings are they? Ratings generally don’t have options that require being saved in a database with a taxonomy – normally they’re just a number out of 5/10/100/whatever, a simple integer. What kind of entity are these options you have actually?

Secondly, how do you actually store ratings and options on models? Are there just separate rows in the taggable table for ratings and their options (that would seem very odd to me – the options are then completely decoupled from the ratings they belong to, and there’s no way to ensure that a hobby only has one option for each rating)? Or is there a column in the taggable table to hold the ID of the selected option for that rating for that hobby (which would make more sense and make everything much simpler)?

For example, say you have a Hobby model with ID 10. This model has two ratings associated with it have IDs 25 and 50. For rating #25, the selected option has ID 32; for rating #50, the selected option has ID 54. Given this, how would your taggable table look? Would it be like this (= the first scenario described above):

  tag_id    taggable_type     taggable_id  
 -------- ------------------ ------------- 
      25   App\Models\Hobby            10  
      50   App\Models\Hobby            10  
      32   App\Models\Hobby            10  
      54   App\Models\Hobby            10  

– or is it more like this (= the latter, more sensible approach):

  tag_id    taggable_type     taggable_id   tag_option  
 -------- ------------------ ------------- ------------ 
      25   App\Models\Hobby            10           32  
      50   App\Models\Hobby            10           54  
jcc5018's avatar

@kokoshneta I think the easiest thing to compare it too is a quiz. Or really any heiretical relationship. I have 60 distinct taxonomies and 900 individual tags for various models with similar set up so i need to figure out this structure. I know the structure itself works, I just dont know how to deal with it with filament. This is another reason i am considering a separate component that just displays tags in either a dropdown, radio or checkbox display based on the component needs that would work for any tags. With hobbies, I actually reference 3 different taxonomies. One for categories, ratings, and then any further refinement with any other tags that may help sort things.

The problem would be similar if I wanted to just have a tag structure like WordPress, where its

1 Parent a. child b child 2.Parent 2 etc

Ratings in this case is just another set of tags for a hobby model. Such as safety rating, or how much it might cost.

The taggable Db is as described in table one. The parent_id should never appear in the taggable table. if parent id is 1 and choices 2-4, only 2-4 need to be referenced to find the selected value as a similar loop would display all parents as labels and then the appropriate choice as the selected option. assuming all hobbies with id 1

it may associate with tag id , 45, 36,50 which 45 may be a category like sports (CATEGORY), and 36 may represent "dangerous", and 50 could represent "Expensive " (RATINGS)

In my original design, all my controllers would have something like this if they needed to reference a tag:

   protected function getRatings ()
    {
        return Tag::type(13)->get();
    }

    /**
     * @return mixed
     */
    protected function getCategories ()
    {
        return Tag::type(1)->get();
    }

That would require me to manually look up the type ID though, so probably not the most efficient. But it was working. Now my code for ratings really doesnt need a multiple selection, but it wouldnt be the end of the world if there were if someone manually inputted a value somehow. But some tags would allow multi tags per taxonomy.

Or a user can filter with multiple rating choices.

But with all that said, I have the select boxes working but how do they get saved in a taggables polymorphic relationship within filament?

kokoshneta's avatar

@jcc5018 I still don’t quite understand what the options are, then. If you say that 50 represents ‘Expensive’ and that’s a rating (so it has type 13), what would be the dropdown options for the ‘Expensive’ rating, i.e., what would be the content of the rows that have 50 as the parent ID?

jcc5018's avatar

@kokoshneta Expensive (a option) is one of the options for cost (the parent) and was just a sample, but here's some real data. I need to provide it in json form as i have no idea how to format a table on here.

{
        "id": 169,
        "tag": "Cost to Enter",
        "parent_id": null,
        "tag_type": 13,
        "description": "Financial commitment needed to get started with the hobby."
    },
    {
        "id": 170,
        "tag": "Very Low",
        "parent_id": "169",
        "tag_type": 13,
        "description": "You can get started with this hobby for under $ 100"
    },
    {
        "id": 171,
        "tag": "Low",
        "parent_id": "169",
        "tag_type": 13,
        "description": "Investment of between $ 100- $ 500 may be needed"
    },
    {
        "id": 172,
        "tag": "Medium",
        "parent_id": "169",
        "tag_type": 13,
        "description": "Investment between  $ 500-$ 1000 may be needed"
    },
    {
        "id": 173,
        "tag": "High",
        "parent_id": "169",
        "tag_type": 13,
        "description": "Investment of $ 1000-$ 10,000 may be needed"
    },
    {
        "id": 174,
        "tag": "Very High",
        "parent_id": "169",
        "tag_type": 13,
        "description": "Investment over $ 10,000 may be needed."
    },
    {
        "id": 175,
        "tag": "Not Applicable",
        "parent_id": "169",
        "tag_type": 13,
        "description": null
    },

the only numbers that matter in the taggable table are 170-175 as they will be the selected option. I should never see id 169 in this case unless someone manually inserted a record. And perhaps there is some validation that needs to happen to prevent it, but that is beyond the scope of this question which is working with Filament. I imagine in its current form, if 169 would appear, the code wouldnt display any selected option at all considering the restraint would be select where parent_id equals 169. so 169 itself wouldn't match.

There is no need for a parent and child ID to be in the taggables table as only the child gets compared with the collection to determine selection.

jaseofspades88's avatar

Looks like RepeatableEntry is what you need to use here. I've used them and they work very well with many to many and morph many relationships. The documentation is good for Filament and the resources are great too. Check here for the repeatable https://filamentphp.com/docs/3.x/infolists/entries/repeatable#overview but if you simply forget about the loop within your resource, this is what the repeatable entry does.

Here's an example from one of my code bases, which is a ContactResource that has a morphable addresses relationship called phoneNumbers. Note the schema you created from the repeatable is what you get for each entry. Treat it like another schema where you can put anything, including those tags you wish to add.

RepeatableEntry::make('phoneNumbers')
	->hiddenLabel()
 	->schema([
    	TextEntry::make('type')
        	->icon(fn (PhoneNumber $number) => $number->type->getIcon())
            ->columns(1)
            ->extraAttributes(['class' => 'font-semibold'])
            ->hiddenLabel(),
		TextEntry::make('number')
			->extraAttributes(['class' => 'cursor-pointer hover:font-semibold'])
            ->columns(1)
            ->hiddenLabel(),
	])

I hope this helps!

Filament v3

jcc5018's avatar

@jaseofspades88 Docs are ok, but could use some work on some parts that are too vague for newer coders.

But I am not sure how a repeater field that displays inputs with the same blank inputs for each entry helps me to pull labels and options from the database to dynamically generate a series of INPUTS.

Think of it as creating a multiple-choice test. Do you want the same question repeated over and over or different questions with different options. If there is a way to implement this so each question is different with repeater, let me know

Please or to participate in this conversation.