Development

Laravel's New Factory insert() Method.

6 minutes

Laravel 12.37 quietly shipped with a new factory method that might seem minor when you first look at it - Factory::insert(). You might actually be thinking wait a minute the insert method has been around for a while and you've already been using it in your code already, I mean I know we have.

This new method isn't some complex architectural change or a breaking update it's just a really nice simple addition that helps solve a very specific problem that you might have already encountered when writing your tests. Bulk inserting test data!

The Problem That's Been Staring Us in the Face

Here's an example of what we've all been doing when we need test data:

php
1<?php
2 
3// Creating 100 orders for a pagination test
4Order::factory()->count(100)->create();
5 
6// Setting up data for a sales report
7Customer::factory()->count(50)->create();
8Product::factory()->count(200)->create();
9Order::factory()->count(1000)->create();

Looks innocent enough, right? But here's what's actually happening under the hood:

  1. Laravel creates the first model instance

  2. Fires the creating event

  3. Inserts into the database

  4. Fires the created event

  5. Returns the model instance

  6. Repeats 99 more times

That's 100 database inserts. 200 model events. 100 model hydrations. All for a test where we just need to check if pagination is showing "10 of 100 results".

Now there are ways around this at the moment you could call Event::fake() to fake all the events that might be happening when you create the data, but it's just another overhead you need to remember to do in your tests, and what if somewhere in a test you actually need for a event to run then. Then you're going to have to get more specific on the Events you want to fake. It's just a lot more mental overload to remember in your tests.

Enter: The insert() Method

Laravel 12.37 introduces a beautifully simple solution: Factory::insert(). Instead of creating model instances one by one, it bulk inserts directly into the database. No models. No events. Just data.

php
1<?php // The old way - 100 individual inserts
2Order::factory()->count(100)->create();
3 
4// The new way - 1 bulk insert
5Order::factory()->count(100)->insert();

This is just so much better for our test performance particularly if in your event handles you had some complicated logic that wasnt necessary for your tests but was for your production application.

How It Actually Works (The Technical Bit)

The insert() method is essentially doing what we've been doing manually with The Query Builder, but with all the convenience of factories:

php
1<?php
2 
3// What we used to do manually
4DB::table('orders')->insert([
5 ['customer_id' => 1, 'total' => 99.99, 'status' => 'pending', ...],
6 ['customer_id' => 2, 'total' => 149.99, 'status' => 'pending', ...],
7 // ... 98 more
8]);
9 
10// What insert() does for us
11Order::factory()->count(100)->insert();

The beauty is that you still get all your factory states, sequences, and relationships. It just skips the Eloquent layer entirely:

php
1<?php
2 
3// This still works!
4Order::factory()
5 ->count(100)
6 ->sequence(
7 ['status' => 'pending'],
8 ['status' => 'processing'],
9 ['status' => 'completed'],
10 )
11 ->insert();

The "No Model Events" Feature (Not a Bug!)

Here's the killer feature that had us grinning from ear to ear: insert() doesn't fire model events.

Why is this brilliant? Because in tests, model events are usually a pain in the arse. How many times have you written something like this?

php
1<?php
2 
3public function test_bulk_order_creation()
4{
5 // Disable events so our test doesn't send 100 emails
6 Event::fake([OrderCreated::class]);
7 
8 // Or worse...
9 Notification::fake();
10 Mail::fake();
11 Queue::fake();
12 
13 Order::factory()->count(100)->create();
14 
15 // Now test the actual thing we care about
16}

With insert(), you don't need any of that faff:

php
1<?php
2 
3public function test_pagination_displays_correct_count()
4{
5 // Just insert the data. No events. No emails. No queued jobs.
6 Order::factory()->count(100)->insert();
7 
8 $response = $this->get('/orders');
9 
10 $response->assertSee('Showing 1 to 10 of 100 results');
11}

We've ran into this scenario before. We had a model observer that was doing some data syncing to a third party API. Every test that created products was making API calls (even in our test environment). The tests were slow, flaky, and we were probably DDoSing the APIs sandbox.

Now? We use insert() for tests that don't care about the API sync, and create() only when we're actually testing the sync functionality.

When You Still Need create()

Let's be clear: insert() isn't replacing create(). There are plenty of times when you need those model instances:

When You Need the Models Back

php
1<?php
2 
3// This won't work - insert() returns void
4$orders = Order::factory()->count(5)->insert();
5$orders->each->process(); // ❌ Nope!
6 
7// You need create() here
8$orders = Order::factory()->count(5)->create();
9$orders->each->process(); // ✅ Perfect

When Testing Model Events

php
1<?php
2 
3public function test_order_creation_sends_confirmation_email()
4{
5 Mail::fake();
6 
7 // Must use create() to trigger the event
8 Order::factory()->create(['customer_email' => 'test@example.com']);
9 
10 Mail::assertSent(OrderConfirmation::class);
11}

When You Need Relationships

php
1<?php
2 
3// If you need to access relationships on the model
4$customer = Customer::factory()
5 ->has(Order::factory()->count(3))
6 ->create();
7 
8// You need the actual model instance
9$this->assertCount(3, $customer->orders);

Our Testing Strategy Now

Here's how we're thinking about factories after this update:

  1. Default to insert() when you just need data in the database

  2. Use create() when you need model instances or events

  3. Document why you're using create() if it's not obvious

We've even added a simple comment convention:

php
1<?php
2 
3// Need instances for assertion
4$orders = Order::factory()->count(5)->create();
5 
6// Need events for email test
7$order = Order::factory()->create();
8 
9// Just need data
10Order::factory()->count(100)->insert();

The Migration Path

If you're sitting on a large test suite and want to migrate to using insert(), here's our approach:

1. Find the Low-Hanging Fruit

Look for tests with Event::fake(), Mail::fake(), or Queue::fake() that are only faking to avoid side effects:

2. Start with Pagination and Count Tests

These almost never need model instances:

php
1<?php
2 
3// Easy wins
4public function test_pagination()
5{
6 Product::factory()->count(50)->insert(); // Changed from create()
7 // ... rest of test
8}

3. Profile Your Slowest Tests

shell
1php artisan test --profile

Focus on the top 10 slowest tests first. We found that 80% of our test time was in just 20 tests.

4. Leave Event Tests Alone

If a test is actually testing events, observers, or model callbacks, leave it with create(). That's what it's for.

One Gotcha We Found

There's one thing to watch out for: timestamps. If you're using insert(), Laravel won't automatically set created_at and updated_at. The factory handles this, but if you're doing something custom:

php
1<?php
2 
3// This won't have timestamps
4DB::table('orders')->insert([
5 'customer_id' => 1,
6 'total' => 100
7]);
8 
9// Factories handle it for you
10Order::factory()->insert(); // Has timestamps
11 
12// But if you override...
13Order::factory()->state([
14 'created_at' => null // This will actually be null!
15])->insert();

The Bottom Line

This is one of those updates that seems small but this could have a big impact on large test suites that are creating lots of data during tests. Our test suites have run faster since we changed a number of our mass creates to use inserts, our CI pipelines are cheaper, and we're not sitting around waiting for tests to run.

Is it revolutionary? No. But it's the kind of thoughtful, practical improvement that makes Laravel such a joy to work with. It solves a real problem that real developers face every day.

The fact that it took until version 12.37 to get this feature almost makes it better - it shows that Laravel is still actively solving the pain points we hit in daily development, not just chasing shiny new features.

Getting Started

Update to Laravel 12.37:

shell
1composer update laravel/framework

Find your slowest tests:

shell
1php artisan test --profile

Start replacing create() with insert() where you don't need model instances. Watch your test suite fly.

That's it. No configuration. No setup. Just faster tests.

What's Your Experience?

We've seen improvements across our projects, but every codebase is different. If you've tried out insert() in your test suite, we'd love to hear about your results. What kind of performance gains are you seeing? Any creative uses we haven't thought of?

And if you're dealing with slow test suites in your Laravel applications and want some help optimising them, give us a shout. We've been wrestling with Laravel test performance for over a decade, and we'd love to share what we've learned.

Looking to scale your development team?

Don’t let limited resources hold you back. Get in touch to discuss your needs and discover how our team augmentation services can supercharge your success.

Get in touch