baconfy/factory-payload

Generate HTTP request payloads from Laravel Eloquent factories.

Maintainers

Package info

github.com/baconfy/factory-payload

pkg:composer/baconfy/factory-payload

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 1

v1.2.0 2026-05-01 16:01 UTC

This package is auto-updated.

Last update: 2026-05-01 16:10:03 UTC


README

Generate clean HTTP request payloads from Laravel Eloquent factories.

Tests Latest Version License Total Downloads PHP Version

Why?

When testing HTTP endpoints, you need request payloads that match the shape your endpoint expects, not the shape your model stores.

The simple case

Without this package:

$response = $this->postJson(route('posts.store'), [
    'title' => fake()->sentence(),
    'body' => fake()->paragraph(),
]);

With this package:

$response = $this->postJson(route('posts.store'), Post::factory()->payload());

One line. Self-documenting. Reusable across every test that hits this endpoint.

When you need overrides

Sometimes you need to control specific fields, for example to test validation or assert against known values:

$response = $this->postJson(
    route('posts.store'),
    Post::factory()->payload(['title' => ''])
);

$response->assertJsonValidationErrors(['title']);

Overrides always pass through, even if the field isn't part of the model's stored attributes (useful for things like password_confirmation).

Installation

composer require --dev baconfy/factory-payload

Requires PHP 8.3+ and Laravel 11, 12 or 13.

Usage

This package supports three equivalent ways to declare which attributes belong in the HTTP payload. Choose the style that best matches your project.

Using #[PayloadAttributes]

Declare the payload attributes directly on the factory class:

namespace Database\Factories;

use App\Models\Post;
use Baconfy\FactoryPayload\Attributes\PayloadAttributes;
use Illuminate\Database\Eloquent\Factories\Factory;

#[PayloadAttributes('title', 'body')]
class PostFactory extends Factory
{
    protected $model = Post::class;

    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
            'body' => fake()->paragraph(),
            'user_id' => User::factory(),
            'published_at' => now(),
        ];
    }
}

Using HasPayloadAttributes

Add the HasPayloadAttributes trait and define $payloadAttributes on your factory:

namespace Database\Factories;

use App\Models\Post;
use Baconfy\FactoryPayload\HasPayloadAttributes;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    use HasPayloadAttributes;

    protected $model = Post::class;

    /**
     * @var array<int, string>
     */
    protected array $payloadAttributes = ['title', 'body'];

    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
            'body' => fake()->paragraph(),
            'user_id' => User::factory(),
            'published_at' => now(),
        ];
    }
}

Both examples produce the same payload in your tests:

$payload = Post::factory()->payload();
// ['title' => 'Lorem ipsum...', 'body' => 'Dolor sit amet...']

Notice how user_id and published_at are automatically excluded because they belong to persistence, not to the HTTP request.

Using a DTO class

If your project already declares request shapes through Data Transfer Objects (DTOs), you can resolve the payload shape directly from the DTO class. Useful when you have multiple endpoints (create, update, etc.) sharing the same model.

namespace App\Data;

class PostCreateData
{
    public static function keys(): array
    {
        return ['title', 'body'];
    }
}
$payload = Post::factory()->payload(PostCreateData::class);
// ['title' => 'Lorem ipsum...', 'body' => 'Dolor sit amet...']

The DTO resolution follows these rules:

  1. If the class has a static keys(): array method, its return value is used as the whitelist (compatible with spatie/laravel-data and similar libraries).
  2. Otherwise, falls back to the class's public properties via Reflection:
class PostUpdateData
{
    public ?string $title = null;
    public ?string $body = null;
}

$payload = Post::factory()->payload(PostUpdateData::class);
// ['title' => '...', 'body' => '...']

If the class doesn't exist, an InvalidArgumentException is thrown with a clear message.

Note: When passing a DTO class, overrides are not supported in the same call. If you need both, use the array form: payload(['title' => 'custom']).

With Pest datasets

Test multiple invalid scenarios in one go:

it('rejects invalid post payloads', function (array $overrides, string $errorField): void {
    $response = $this->postJson(
        route('posts.store'),
        Post::factory()->payload($overrides)
    );

    $response->assertStatus(422)->assertJsonValidationErrors([$errorField]);
})->with([
    'missing title' => [['title' => ''], 'title'],
    'missing body' => [['body' => ''], 'body'],
    'title too long' => [['title' => str_repeat('a', 300)], 'title'],
]);

Behavior

The behavior below applies to all three ways of declaring payload attributes:

Scenario Result
No payload attributes declared Returns only the overrides
#[PayloadAttributes] or $payloadAttributes declared Filters raw() by whitelist, then merges overrides
Override key exists in whitelist Override wins
Override key not in whitelist Override still passes through
Factory has count() set payload() still returns a single array
DTO class passed to payload() Resolves shape from keys() or public properties
Invalid DTO class passed Throws InvalidArgumentException

Testing

composer test

Credits

License

Licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). See LICENSE for details.