Factory relationships — has(), for(), hasAttached()
Concept
Factory relationships allow you to build a complete object graph — parent models with their associated children — in a single fluent expression. Laravel's factory system provides three primary relationship methods: has() for has-many relationships, for() for belongs-to relationships, and hasAttached() for many-to-many pivot relationships. These methods operate on the Factory instance before any database write occurs, so the full graph is constructed and committed in a single coordinated pass.
The has() method creates child records and associates them after creating the parent. User::factory()->has(Post::factory()->count(3)) creates one user and three posts, setting posts.user_id to the new user's id automatically. Laravel infers the foreign key from the relationship name using the same convention as Eloquent. If the factory relationship name does not match a real method on the parent model, you can pass the relationship name explicitly: has(Post::factory()->count(3), 'articles').
The for() method creates the parent and associates it with a child factory. Post::factory()->for(User::factory()->admin()) creates an admin user first, then creates a post with user_id set to that user. You can pass an existing model instance instead of a factory to avoid creating a new parent: Post::factory()->for($existingUser)->create(). This is extremely common in feature tests where you set up a single user in setUp() and reuse it across multiple factory calls.
hasAttached() populates pivot tables for many-to-many relationships. It accepts a factory, an optional attributes array or closure for pivot columns, and an optional relationship name. User::factory()->hasAttached(Role::factory()->count(2), ['granted_at' => now()]) creates two roles and inserts rows into the role_user pivot table with the granted_at column set.
| Method | Relationship direction | Pivot support |
|---|---|---|
has(Factory, $relation) | Parent has many children | No |
for(Factory|Model, $relation) | Child belongs to parent | No |
hasAttached(Factory, $pivotAttrs, $relation) | Many-to-many | Yes |
Magic hasPosts() / forUser() | Inferred from method name | No |
Code Example
<?php
use App\Models\User;
use App\Models\Post;
use App\Models\Tag;
use App\Models\Comment;
// --- has() — one user with 5 published posts ---
$user = User::factory()
->has(
Post::factory()->count(5)->state(['status' => 'published']),
'posts' // relationship method name on User model
)
->create();
// Magic shorthand (Laravel infers from camelCase 'hasPosts' → 'posts' relationship)
$user = User::factory()
->hasPosts(5, ['status' => 'published'])
->create();
// --- for() — post belonging to an existing user ---
$post = Post::factory()
->for($user)
->has(Comment::factory()->count(10), 'comments')
->create();
// --- for() with nested factory ---
$post = Post::factory()
->for(User::factory()->admin())
->create();
// --- hasAttached() — user with roles and pivot data ---
$user = User::factory()
->hasAttached(
Tag::factory()->count(3),
fn (array $attributes, Tag $tag) => ['weight' => rand(1, 10)],
'tags'
)
->create();
// --- Full object graph in one expression ---
$user = User::factory()
->admin()
->hasPosts(
Post::factory()
->count(3)
->hasComments(5)
->hasAttached(Tag::factory()->count(2), [], 'tags')
)
->create();Interview Q&A
Q: How does has() know which foreign key to set on child records, and how do you override it?
has() calls $childFactory->for($parentModel, $relationshipName) internally after the parent is created. It resolves the relationship name from the second argument you pass (or infers it from the factory's model name). It then calls that relationship method on the parent model and reads getForeignKeyName() from the resulting HasMany or HasOne relation to determine the column name. To override, either pass an explicit relationship name as the second argument to has() or define a for{RelationName}() method on the factory that manually sets the foreign key.
Q: What is the difference between passing a factory to for() versus passing an existing model instance?
Passing a factory causes a new parent record to be created each time the factory runs. Passing an existing model instance reuses that record and only sets the foreign key. In tests, reusing an existing model is usually correct when you need multiple child records to share the same parent (e.g., testing that all posts by a specific user are returned by an endpoint). Creating a fresh parent via factory is correct when each child should have an independent parent with distinct state.
Q: How do you populate pivot columns (extra data on the pivot table) when using hasAttached()?
Pass either a plain array or a closure as the second argument to hasAttached(). A plain array applies the same pivot attributes to every pivot row. A closure receives the current pivot attributes array and the related model instance, letting you compute dynamic values — for example, setting expires_at relative to a date on the related model. Under the hood, hasAttached() calls attach() on the many-to-many relationship with the merged pivot attributes after all models have been created.