Generate clean HTTP request payloads from Laravel Eloquent factories.
When testing HTTP endpoints, you need request payloads that match the shape your endpoint expects, not the shape your model stores.
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.
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).
composer require --dev baconfy/factory-payloadRequires PHP 8.3+ and Laravel 11, 12 or 13.
This package supports three equivalent ways to declare which attributes belong in the HTTP payload. Choose the style that best matches your project.
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(),
];
}
}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.
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:
- If the class has a static
keys(): arraymethod, its return value is used as the whitelist (compatible withspatie/laravel-dataand similar libraries). - 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']).
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'],
]);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 |
composer test- Renato Dehnhardt
- Josh Donnell — for adding the
#[PayloadAttributes]attribute support - All contributors
Licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). See LICENSE for details.