ThisLeeNoble's avatar

Seeding Many-to-Many records - are my relationships set up correctly?

I'm getting an error "Call to undefined method Illuminate\Database\Eloquent\Relations\HasManyThrough::attach()" when I attempt to seed links between Client and Location models. I have to use a link table because Locations may not always be linked to Clients and may be linked to other model types yet to be defined.

class Location extends Model
{
    use HasFactory;

    public $fillable = [
        'nickname',
        'building',
        'street',
        'town',
        'city',
        'county',
        'latitude',
        'longitude',
    ];

    public function client(): HasOneThrough {
        return $this->hasOneThrough(
            Client::class,
            ClientLocation::class
        );
    }
}
class Client extends Model
{
    use HasFactory;

    public $fillable = [
        'name',
        'day_rate',
    ];

    public function locations() : HasManyThrough {
        return $this->hasManyThrough(
            Location::class,
            ClientLocation::class,
            'location_id',
            'id'
        );
    }
}
class ClientLocation extends Pivot {
    protected $table = 'client_locations';

    public function client(): BelongsTo {
        return $this->belongsTo(Client::class);
    }

    public function location(): HasOne {
        return $this->hasOne(Location::class);
    }
}

I have already seeded some Location records (it would be nice to be able to seed them on the fly when creating Client records but not essential so I've abandoned that approach for now), so now all I need to do is create some Client records and link them to none, one or more Locations via the pivot table. This is what I have:

class ClientSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        //for($i = 0; $i < 10; $i++){ // commented out until I can get one to work

			// This will be random from 0-3 but for now I want to ensure I have at least one Location record
            $locations = Location::query()->inRandomOrder()->take(rand(1,3))->pluck('id')->toArray();
			// At this point $locations is an array if indices as expected
			
            $client = Client::factory()->create();

			// Tutorials and answers I've read say to do this
            $client->locations()->attach($locations);

			// I've tried with/without the parentheses
			// I've tried ->save($locations)
			// But I just get the error above that locations() returns a HasManyThrough

        //}  // end loop
    }
}

Yeah, so what have I done wrong? I'd love to know. Thanks, L

0 likes
3 replies
LaryAI's avatar
LaryAI
AI
Best Answer
Level 58

It looks like you are trying to set up a many-to-many relationship between Client and Location models using a pivot table client_locations. However, the relationships you have defined are not correct for a many-to-many relationship. Instead of using HasOneThrough and HasManyThrough, you should use belongsToMany on both models.

Here is how you can set up the relationships correctly:

  1. Update the Location model to use belongsToMany:
class Location extends Model
{
    use HasFactory;

    protected $fillable = [
        'nickname',
        'building',
        'street',
        'town',
        'city',
        'county',
        'latitude',
        'longitude',
    ];

    public function clients(): BelongsToMany {
        return $this->belongsToMany(Client::class, 'client_locations');
    }
}
  1. Update the Client model to use belongsToMany:
class Client extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'day_rate',
    ];

    public function locations(): BelongsToMany {
        return $this->belongsToMany(Location::class, 'client_locations');
    }
}
  1. The ClientLocation model is not necessary unless you have additional fields in the pivot table. If you do, you can keep it as a model extending Pivot, but it's not required for basic many-to-many relationships.

  2. Update your seeder to use the correct relationship method:

class ClientSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        //for($i = 0; $i < 10; $i++){ // commented out until I can get one to work

            // This will be random from 0-3 but for now I want to ensure I have at least one Location record
            $locations = Location::query()->inRandomOrder()->take(rand(1,3))->pluck('id')->toArray();
            // At this point $locations is an array of indices as expected
            
            $client = Client::factory()->create();

            // Attach the locations to the client
            $client->locations()->attach($locations);

        //}  // end loop
    }
}

With these changes, you should be able to seed the many-to-many relationship between Client and Location models without encountering the "Call to undefined method" error.

ThisLeeNoble's avatar

Ooh, you were pretty close there, Lary. I did what you said and it still failed with:

 Illuminate\Database\Eloquent\Relations\BelongsToMany::save(): Argument #1 ($model) must be of type Illuminate\Database\Eloquent\Model, array given

but it was pretty close. After a small tweak to the Seeder class like so:

$locations = Location::query()->inRandomOrder()->take(rand(1,3))->pluck('id');

            $client = Client::factory()->create();
			$client->locations()->attach($locations);

it works. (just took off the toArray() bit.

★★★★☆

1 like

Please or to participate in this conversation.