beneaththesurfacelabs / universal-factory
Laravel-style Factories for non-Eloquent classes
Fund package maintenance!
BeneathTheSurfaceLabs
Requires
- php: ^8.2
- illuminate/contracts: ^10.0||^11.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^9.0.0||^8.22.0
- pestphp/pest: ^2.34
- pestphp/pest-plugin-arch: ^2.7
- pestphp/pest-plugin-laravel: ^2.3
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1
- phpstan/phpstan-phpunit: ^1.3
This package is auto-updated.
Last update: 2024-10-31 05:18:13 UTC
README
Create Laravel-style Factory classes to quickly generate test data within your applications
Why?
Laravel's existing factory implementation is truly amazing, but has become increasingly coupled to Eloquent models over the years.
Prior to these changes, it was possible to use Laravel Factories for many different kinds of data, including things like DTOs, FormRequests, etc.
In order to restore this ability, we can use this package to complement Laravel's existing Eloquent Factories.
Installation
You can install the package via composer:
composer require beneaththesurfacelabs/universal-factory
You can publish the config file with:
php artisan vendor:publish --tag="universal-factory-config"
This is the contents of the published config file:
<?php return [ /* |-------------------------------------------------------------------------- | Default Namespace for Universal Factories |-------------------------------------------------------------------------- | | This value defines the default namespace for the universal factories. You can | change it to fit your application's needs. | */ 'default_namespace' => 'App\\Factories\\', /* |-------------------------------------------------------------------------- | Universal Factory Method Name |-------------------------------------------------------------------------- | | This value allows the user to specify the name of the factory method | provided by the HasUniversalFactory trait. | For example, if your source class does too much, and already has a poorly | designed static factory() method that we cannot just override. | */ 'method_name' => 'factory', ];
Usage
These Universal Factories are API-compatible with most features found within Laravel's Eloquent Factories. They are used in an identical fashion.
To use them, perform the following steps:
- Add the HasUniversalFactory trait to your class.
- Create your factory class using the included Artisan command, or by hand
- Use the same features you know and love from Laravel's Eloquent Factories
- Factory States
- Callbacks such as afterMaking
- Nested Factory Definitions
- Integration With Faker
Example Classes and their Universal Factories
Example Class UserInfo:
<?php namespace BeneathTheSurfaceLabs\UniversalFactory\Tests\Examples; use BeneathTheSurfaceLabs\UniversalFactory\Traits\HasUniversalFactory; class UserInfo { use HasUniversalFactory; public function __construct( public string $externalId, public string $name, public string $email, public \DateTime $birthday, public int $age, public ProfileData $profileData, ) {} // If the below method is omitted, the package will look for a class named UserInfoFactory // within the same namespace as this class -- BeneathTheSurfaceLabs\UniversalFactory\Tests\Examples public static function newFactory(): UserInfoFactory { return UserInfoFactory::new(); } }
Example Factory Class UserInfoFactory:
<?php namespace BeneathTheSurfaceLabs\UniversalFactory\Tests\Examples; use BeneathTheSurfaceLabs\UniversalFactory\UniversalFactory; class UserInfoFactory extends UniversalFactory { /* If the 'class' property is omitted, the package will check for a class with the same name (minus 'Factory'), within the same namespace as the factory In this example, if omitted, the package would look for: \BeneathTheSurfaceLabs\UniversalFactory\Tests\Examples\UserInfo */ protected $class = UserInfo::class; /** * Define the class's default attributes. * * @return array<string, mixed> */ public function definition(): array { return [ 'externalId' => fn () => substr(str_replace(['+', '.', 'E'], '', microtime(true)), -10), // functional attribute definitions 'name' => $this->faker->name, // typical faker usage 'email' => $this->faker->email, 'birthday' => $this->faker->dateTime, 'age' => $this->faker->numberBetween(21, 40), 'profileData' => ProfileData::factory(), // nested factory within definitions ]; } public function configure(): static { /* Add global callbacks within the configure() method here, or add state specific callbacks within state methods. Ex. unrestrictedAge() and restrictedAge() below */ // This callback would happen anytime this factory was to generate a class $this->afterMaking(fn (UserInfo $userInfo) => $userInfo->profileData = ProfileData::factory()->withProfileFor($userInfo)->make()); return $this; } public function unrestrictedAge(): self { // create state-specific methods return $this->state(function (array $attributes) { $birthday = fake()->dateTimeBetween('now', '-21 years'); $attributes['birthday'] = $birthday; $attributes['age'] = (new \DateTime)->diff($birthday)->y; return $attributes; }); } public function restrictedAge(): self { return $this->state(function (array $attributes) { $birthday = fake()->dateTimeBetween('-12 years', 'now'); $attributes['birthday'] = $birthday; $attributes['age'] = (new \DateTime)->diff($birthday)->y; return $attributes; }); } }
Class Construction
This package supports a few common strategies to instruct your factory how to construct your classes. Developers can easily override these with their own class construction implementation.
By default, this package will use the ClassConstructionStrategy::CONTAINER_BASED
strategy, which takes advantage of the Laravel container to attempt to construct your class.
Another, ClassConstructionStrategy::REFLECTION_BASED
, uses PHP's Reflection classes to examine your class, and directly set the parameters it is able to inspect.
The last strategy, ClassConstructionStrategy::ARRAY_BASED
, assumes your class constructor takes an array of parameters, which will map to your class properties. This is similar to how Eloquent models are constructed.
Of course, if your class requires something more custom or complex to be constructed, you can easily override the newClass() method within your factory class.
Example Class ProfileData (Notice the constructor)
<?php namespace BeneathTheSurfaceLabs\UniversalFactory\Tests\Examples; use BeneathTheSurfaceLabs\UniversalFactory\Traits\HasUniversalFactory; class ProfileData { use HasUniversalFactory; public ?string $facebookProfileUrl; public ?string $facebookAvatarUrl; public ?string $twitterProfileUrl; public ?string $twitterAvatarUrl; public ?string $gitHubProfileUrl; public ?string $githubAvatarUrl; public ?string $personalUrl; public function __construct(array $profileData) { $this->facebookProfileUrl = $profileData['facebookProfileUrl'] ?? null; $this->facebookAvatarUrl = $profileData['facebookAvatarUrl'] ?? null; $this->twitterProfileUrl = $profileData['twitterProfileUrl'] ?? null; $this->twitterAvatarUrl = $profileData['twitterAvatarUrl'] ?? null; $this->gitHubProfileUrl = $profileData['gitHubProfileUrl'] ?? null; $this->githubAvatarUrl = $profileData['githubAvatarUrl'] ?? null; $this->personalUrl = $profileData['personalUrl'] ?? null; } }
Example Factory Class ProfileDataFactory (Sets Array Based Construction)
<?php namespace BeneathTheSurfaceLabs\UniversalFactory\Tests\Examples; use Illuminate\Support\Str; use BeneathTheSurfaceLabs\UniversalFactory\UniversalFactory; use BeneathTheSurfaceLabs\UniversalFactory\Enum\ClassConstructionStrategy; class ProfileDataFactory extends UniversalFactory { protected ClassConstructionStrategy $classConstructionStrategy = ClassConstructionStrategy::ARRAY_BASED; /** * Define the class's default attributes. * * @return array<string, mixed> */ public function definition(): array { return [ 'facebookProfileUrl' => fake()->url(), 'facebookAvatarUrl' => fake()->imageUrl(), 'twitterProfileUrl' => fake()->url(), 'twitterAvatarUrl' => fake()->imageUrl(), 'gitHubProfileUrl' => fake()->url(), 'githubAvatarUrl' => fake()->imageUrl(), 'personalUrl' => fake()->url(), ]; } public function withProfileFor(UserInfo $userInfo): self { $usernameGenerator = function (UserInfo $userInfo) { $method = fake()->boolean() ? 'slug' : 'studly'; return Str::$method( $userInfo->name, fake()->randomElement(['-', '_', '.']). (fake()->boolean() ? $userInfo->birthday->format(fake()->randomElement(['Y', 'y', 'my'])) : ''), ); }; $urls = [ 'facebook' => 'https://facebook.com/'.$usernameGenerator($userInfo), 'twitter' => 'https://x.com/'.$usernameGenerator($userInfo), 'github' => 'https://github.com/'.$usernameGenerator($userInfo), 'personal' => 'https://'.Str::slug($userInfo->name).'.com/', ]; return $this->state(function (array $attributes) use ($urls) { $attributes['facebookProfileUrl'] = $urls['facebook']; $attributes['twitterProfileUrl'] = $urls['twitter']; $attributes['gitHubProfileUrl'] = $urls['github']; $attributes['personalUrl'] = $urls['personal']; return $attributes; }); } }
Example newClass() Override
public function newClass(array $attributes = []) { return MyCustomClass::fromUserId($attributes['user_id']); }
Testing
To run the test suite, run the following:
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Issues & Security Vulnerabilities
Please submit any issues (installation, usage, security, etc.) using the GitHub Issues tab above.
Credits
License
The MIT License (MIT). Please see License File for more information.