A state is a named method that overrides attributes: OrderFactory::shipped() returns $this->state(['status' => 'shipped']). afterCreating() callbacks run once the model exists; withItems() attaches products through the order_items pivot (Many to Many):
public function withItems(int $lines = 2): static
{
return $this->afterCreating(function (Order $order) use ($lines) {
foreach (Product::inRandomOrder()->limit($lines)->get() as $product) {
$order->products()->attach($product, [
'quantity' => fake()->numberBetween(1, 3), 'unit_price' => $product->price,
]);
}
});
}In DemoShopSeeder, sequence() cycles values or calls a closure with $index, and recycle() hands existing models to any definition that would call another factory:
$categories = Category::whereNotNull('parent_id')->get();
$customers = Customer::factory()->count(8)->create();
$products = Product::factory()->count(12)->recycle($categories)
->sequence(fn (Sequence $s) => ['sku' => sprintf('BK-%04d', $s->index + 1)])
->create();
Product::factory()->count(2)->outOfStock()->recycle($categories)->create();
Order::factory()->count(20)->recycle($customers)->withItems(2)
->sequence(['status' => 'paid'], ['status' => 'shipped'], ['status' => 'pending'])
->create();
Review::factory()->count(15)->recycle($customers)->recycle($products)->create();The result, counted in MySQL 524 : 8 customers, 14 products, 20 orders (7 paid, 7 shipped, 6 pending), 40 order lines and 15 reviews. Without recycle(), the orders and reviews would have added 35 customers and 15 products. States, for() and callbacks combine; in tinker:
use App\Models\{Customer, Order};
$o = Order::factory()->shipped()->for(Customer::first())->withItems(3)->create();
echo $o->status, ' | ', $o->customer->name, ' | ', $o->products()->count(), " lines\n";shipped | Jermaine Doyle | 3 lines
The other direction is has(Order::factory()->count(3)) or the magic hasOrders(3); hasAttached() fills pivot columns, and a built-in trashed() state makes soft-deleted models.