# Laravel 8: Model Factories and the New Directory Structure

> A deep dive into Laravel 8's class-based factory system and new directory layout, focused on generating test and seed data.

- Published: 2020-08-02
- Category: Framework & Library
- Tags: Laravel
- Reading time: 3 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/laravel-8-model-factories-and-new-directory-structure/
- Language: en-US
- Author: Muhammet Şafak

---
Laravel 8 shipped in September 2020, and the change that caught my attention most was the [overhaul of model factories](/en/blog/writing-my-first-tests-with-phpunit/). The old factory system worked, but it was clunky: global functions, magic string references, no type safety. The new system moves all of that into a class-based structure.

I focused this post specifically on factories because generating test and seed data becomes increasingly important as projects grow. A well-designed factory setup makes that work genuinely sustainable.

## Old vs. new: a side-by-side comparison

In Laravel 7 and earlier, a factory definition looked like this:

```php
// database/factories/UserFactory.php (old style)
$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name'     => $faker->name,
        'email'    => $faker->unique()->safeEmail,
        'password' => bcrypt('password'),
    ];
});
```

And you used it through a global function:

```php
$user = factory(App\User::class)->create();
```

In Laravel 8, a factory is a class:

```php
// database/factories/UserFactory.php
namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    protected $model = User::class;

    public function definition(): array
    {
        return [
            'name'     => $this->faker->name(),
            'email'    => $this->faker->unique()->safeEmail(),
            'password' => bcrypt('password'),
        ];
    }
}
```

And usage is through the model itself:

```php
$user = User::factory()->create();
```

With the old system, your IDE had no idea what `factory(App\User::class)` would return — it was resolving a string class name at runtime. With the new system, `User::factory()` returns a typed object; the IDE sees the methods, autocomplete works. It sounds like a minor detail, but in large factory files this difference shows up in day-to-day writing speed.

## State management

The most valuable feature of the new system is defining distinct states with the `state` method:

```php
public function suspended()
{
    return $this->state(fn (array $attributes) => [
        'suspended_at' => now(),
        'suspended_reason' => 'Policy violation',
    ]);
}

public function admin()
{
    return $this->state(fn (array $attributes) => [
        'role' => 'admin',
    ]);
}
```

States are chainable:

```php
// A suspended admin
$user = User::factory()->suspended()->admin()->create();

// Ten active users
$users = User::factory()->count(10)->create();
```

Without this structure, testing different states of the same model meant either writing separate factory files or manually passing attributes in every test. Both approaches make maintenance harder.

## Related models

In real projects, factories shine brightest when it comes to related data. Creating a post that automatically creates a user along with it:

```php
// PostFactory.php
public function definition(): array
{
    return [
        'user_id'    => User::factory(),
        'title'      => $this->faker->sentence(),
        'body'       => $this->faker->paragraphs(3, true),
        'published'  => true,
    ];
}
```

When another factory is provided as a value, Laravel resolves it automatically. In a test:

```php
// A post belonging to an existing user
$post = Post::factory()->for($existingUser)->create();

// A post with comments
$post = Post::factory()
    ->has(Comment::factory()->count(5))
    ->create();
```

One pitfall to watch for: when using `for()` or `has()`, make sure factories don't call each other recursively. For example, if `PostFactory` uses `User::factory()` and `UserFactory` uses `Post::factory()`, a call to `Post::factory()->create()` can spiral into an infinite loop. The fix is to pass an existing object via `for()` or to define the relationship in only one direction.

## New directory structure

Laravel 8 introduced [`app/Models` as the default directory](/en/blog/migrating-to-laravel-5-directory-structure-and-env/) for models. In previous versions, models lived directly under `app/` — `app/User.php`, `app/Post.php`, and so on. It was a small thing, but it accumulated into clutter over time.

In the new structure, models live under `app/Models/`:

```
app/
  Models/
    User.php
    Post.php
    Comment.php
  Http/
    Controllers/
    Requests/
```

There's no need to force-migrate existing projects to this layout; Laravel 8 is backward compatible. For new projects, this is now the natural home for your models.

## Factories and seeders together

Using factories inside seeder files lets you stand up a full test environment quickly:

```php
// database/seeders/DatabaseSeeder.php
public function run(): void
{
    User::factory()
        ->count(10)
        ->has(Post::factory()->count(5)->has(Comment::factory()->count(3)))
        ->create();
}
```

When this seeder runs, it creates 10 users, each with 5 posts, each post with 3 comments. One command: `php artisan db:seed`.

Compared to the old system, the difference is significant. Producing the same data previously meant writing a much longer seeder or setting up manual loops. The new system lets you express nested relationships in chainable syntax; when you read the seeder, the data shape is immediately obvious.

---

Laravel 8's factory changes look like a small refactor on the surface, but they deliver a real payoff in practice. Type safety, IDE support, chainable state definitions — taken together, they make writing test data genuinely enjoyable. I see this update as a good signal: the framework is maturing and chipping away at its small inconsistencies.

Generating test data is half the work of writing a test. For a model with complex nested relationships, setting up data with the old system could sometimes take longer than writing the test itself. The new factory system removes that obstacle: the test tells you what it does, and the data setup is reduced to a few chained calls. This isn't just about speed — readability improves too. What you're testing takes center stage, not the mechanics of how you built the data.
