Factories
🗒️ Introduction
Like Eloquent Factories this package enables you to define factories for your entities, integrating CycleORM with Laravel's elegant syntax and functionality.
This feature is available through the wayofdev/laravel-cycle-orm-factories, be sure to check installation instructions.
When testing your application or seeding your database, you may need to insert a few records into your database. Instead of manually specifying the value of each column, this package, same as Laravel, allows you to define a set of default attributes for each of your Entities using factory classes.
Consider a Post
Entity example to illustrate how to write a factory:
<?php
declare(strict_types=1);
namespace Database\Factories;
use WayOfDev\DatabaseSeeder\Factories\AbstractFactory;
use Domain\Post\Post;
class PostFactory extends AbstractFactory
{
public function definition(): array
{
return [
'title' => $this->faker->sentence,
'content' => $this->faker->paragraph,
'user' => UserFactory::new()->makeOne(),
];
}
public function makeEntity(array $definition): Cart
{
return new Post(
$definition['title'],
$definition['content']
$definition['user']
);
}
public function entity(): string
{
return Post::class;
}
}
🛠️ Defining Entity Factories
Writing Entity Factories
Factory classes can be placed in standard Laravel database/factories
directory. Each factory class should extend the WayOfDev\DatabaseSeeder\Factories\AbstractFactory
class and define a definition
method. This method should return an array of key-value pairs that represent the default attributes of the entity.
Factory classes should implement:
-
definition
— method is where you can define all the properties of the entity. The method should return an array where the keys are the property names and the values are the values that should be set for those properties. This method crafts the definition array for entity construction or property setting. -
entity
— method should return the fully qualified class name of the target entity that the factory is responsible for creating. In some cases, the factory may use the class name returned by theentity
method to create a new instance of the entity without calling its constructor. Instead, it may use reflection to directly set the properties of the entity using data from thedefinition
method. -
makeEntity
(optional) — method allows you to control the process of creating an entity through its constructor. The method takes an array of definitions as an argument, which is generated by thedefinition
method.
Use the definition
method to specify properties, utilizing the Faker library, which provides a wide range of fake data generation methods such as names, addresses, phone numbers, etc.
⚡️ Creating Entities using Factories
Instantiating
Define your factories to instantiate entities in tests or seeders. Create a single entity instance using the new()
static method:
/** @var Post $post */
$post = PostFactory::new();
This will create a new instance of the factory. It provides several useful methods for generating entities.
Overriding initial definition values
You may also override the initial definition values by passing an array to the new()
method:
/** @var Post $post */
$post = PostFactory::new([
'title' => 'Custom Title',
'content' => 'Custom Content',
]);
Creating multiple Entities
Generate multiple entities with the times()
method:
/** @var Collection $posts */
$posts = PostFactory::times(3)->make();
Returns Illuminate\Support\Collection
of entities, with three entities in it.
🫥 Creating Entities without persistence.
Creating Collection
Use the make()
method for non-persistent entity creation:
/** @var Collection $posts */
$posts = PostFactory::make();
Returns Illuminate\Support\Collection
of entities, with one entity in it.
Single Entity
For a single non-persistent entity, without collection, use the makeOne()
method:
/** @var Post $post */
$post = PostFactory::makeOne();
Which will return a single Post
entity.
📥 Persisting Entities
Persisting with create()
method
If you would like to persist the entities that are generated by the factory, you may use the create()
method:
/** @var Collection $posts */
$posts = PostFactory::new()->create();
also can be paired with times()
method:
/** @var Collection $posts */
$posts = PostFactory::times(3)->create();
Which will return Illuminate\Support\Collection
of entities, with three entities in it, that were persisted into the database.
Persisting with createOne()
method
To persist a single entity, you may use the createOne()
method:
/** @var Post $post */
$post = PostFactory::createOne();
Which will return a single Post
entity, that was persisted into the database.
🔄 Factory States
States (state
and entityState
methods) in factory classes allow for the definition of additional, specific configurations of entities. These states enable more readable and expressive code, clearly conveying the intended modifications or conditions of an entity.
Factory state()
method
State manipulation methods allow you to define discrete modifications that can be applied to your entity factories in any combination. For example, your Database\Factories\PostFactory
factory might contain a status state method that modifies one of its default attribute values.
State transformation methods typically call the state method provided by WayOfDev\DatabaseSeeder\Factories\AbstractFactory
class. The state method accepts a closure which will receive the array of raw attributes defined for the factory and should return an array of attributes to modify:
💡 It is non-destructive, it will only update the properties passed in the returned array and will not remove any properties from the definition array.
namespace Database\Factories;
use Faker\Generator;
use Domain\Post\Status;
use WayOfDev\DatabaseSeeder\Factories\AbstractFactory;
class PostFactory extends AbstractFactory
{
// Example that uses Faker to generate random status
public function randomStatus(): self
{
return $this->state(function (Generator $faker, array $definition) {
return [
'status' => $faker->randomElement(Status::values()),
];
});
}
// Example that sets status to published
public function published(): self
{
return $this->state(function (Generator $faker, array $definition) {
return [
'status' => Status::PUBLISHED,
];
});
}
Factory entityState()
method
In addition to the state method, there also the entityState method. This method allows developers to change the state of an entity object using the available methods of that entity. It takes a closure as an argument, which should accept the entity as an argument and should return the modified entity. This allows developers to take full advantage of the object-oriented nature of their entities and use the methods that are already defined on the entity to change its state.
Can also be used in same manner as Laravel trashed states
namespace Database\Factories;
use Faker\Generator;
use Domain\Post\Post;
use WayOfDev\DatabaseSeeder\Factories\AbstractFactory;
class PostFactory extends AbstractFactory
{
// ...
public function deleted(): self
{
return $this->entityState(static function (Post $post) {
return $user->softDelete();
});
}
And then you can use it in your tests or seeders:
/** @var Post $post */
$post = PostFactory::new()->deleted()->createOne();
🔁 Factory Relationships
Creating Factories with Relationships currently require some manual work, however, it is still possible to define relationships between entities using the state()
method.
Example:
Consider a Post
Entity that has many Comment
Entities and that belongs to User
Entity. You can define a factory for the Post
Entity and then define a factory for the Comment
Entity. and User
Entity. Then you can use state()
method to define the relationship between the Post
and Comment
and Author
Entities.
To demonstrate various relationships, let's consider the following example, that we will use further in this section:
Post Entity
namespace Domain\Post;
use Illuminate\Support\Collection;
use Domain\User\User;
use Domain\Post\PostRepository;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Relation\BelongsTo;
use Cycle\Annotated\Annotation\Relation\HasMany;
use Ramsey\Uuid\Uuid;
#[Entity(repository: PostRepository::class)]
class Post
{
#[Column(type: 'string', primary: true)]
private string $id;
#[Column(type: 'string')]
private string $title;
#[Column(type: 'string')]
private string $content;
#[BelongsTo(target: User::class, innerKey: 'user_id', outerKey: 'id', nullable: true)]
private ?User $user;
#[HasMany(target: Comment::class, innerKey: 'id', outerKey: 'post_id', load: 'eager')]
private Collection $comments;
public function __construct(string $title, string $content, ?User $user)
{
$this->id = Uuid::uuid7()->toString();
$this->title = $title;
$this->content = $content;
$this->user = $user;
$this->comments = new Collection();
}
public function id(): string
{
return $this->id;
}
public function title(): string
{
return $this->title;
}
public function content(): string
{
return $this->content;
}
public function user(): ?User
{
return $this->user;
}
public function comments(): Collection
{
return $this->comments;
}
}
Comment Entity
namespace Domain\Post\Comment;
use Domain\User\User;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Relation\BelongsTo;
use Domain\User\User;
use Ramsey\Uuid\Uuid;
#[Entity(repository: CommentRepository::class)]
class Comment
{
#[Column(type: 'string', primary: true)]
private string $id;
#[Column(type: 'string')]
private string $content;
#[BelongsTo(target: Post::class, innerKey: 'post_id', outerKey: 'id', nullable: false)]
private ?Post $post;
#[BelongsTo(target: User::class, innerKey: 'user_id', outerKey: 'id', nullable: false)]
private ?User $user;
public function __construct(string $content, Post $post, ?User $user)
{
$this->id = Uuid::uuid7()->toString();
$this->content = $content;
$this->post = $post;
$this->user = $user;
}
public function id(): string
{
return $this->id;
}
public function content(): string
{
return $this->content;
}
public function post(): Post
{
return $this->post;
}
public function user(): ?User
{
return $this->user;
}
}
User Entity
namespace Domain\User;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Relation\HasMany;
use Domain\Post\Post;
use Illuminate\Support\Collection;
use Ramsey\Uuid\Uuid;
#[Entity(repository: UserRepository::class)]
class User
{
#[Column(type: 'string', primary: true)]
private string $id;
#[Column(type: 'string')]
private string $name;
#[HasMany(target: Post::class, innerKey: 'id', outerKey: 'user_id', load: 'eager')]
private Collection $posts;
#[HasMany(target: Comment::class, innerKey: 'id', outerKey: 'user_id', load: 'eager')]
private Collection $comments;
public function __construct(string $name)
{
$this->id = Uuid::uuid7()->toString();
$this->name = $name;
$this->posts = new Collection();
$this->comments = new Collection();
}
public function id(): string
{
return $this->id;
}
public function posts(): Collection
{
return $this->posts;
}
public function comments(): Collection
{
return $this->comments;
}
}
Has One Relationship
In PostFactory
, you can define a state that creates a new User
and then associates that User with the Post:
namespace Database\Factories;
use Faker\Generator;
use Domain\Post\Status;
use WayOfDev\DatabaseSeeder\Factories\AbstractFactory;
class PostFactory extends AbstractFactory
{
// example with User passed from outside
public function withUser(User $user): self
{
return $this->state(fn (Generator $faker, array $definition) => [
'user' => $user,
]);
}
// example with User created by factory
public function withUser(): self
{
return $this->state(fn (Generator $faker, array $definition) => [
'user' => UserFactory::new()->createOne(), // or makeOne to allow cascade persisting
]);
}
}
Example, shows how to create collection of Post
entities with associated User
entity:
$collection = PostFactory::times(3)->withUser(
UserFactory::new()->createOne()
)->create();
Has Many Relationship
As Post
entity in constructor does not accept Comment
collection, you can define a state in CommentFactory
that creates a new Comment
and then associates that Comment with the Post via state:
namespace Database\Factories;
use Faker\Generator;
class CommentFactory extends AbstractFactory
{
public function withPost(Post $post): self
{
return $this->state(fn (Generator $faker, array $definition) => [
'post' => $post,
]);
}
}
Example, shows how to create collection of Comment
entities with associated Post
entity:
$collection = CommentFactory::times(3)->withPost(
PostFactory::new()->createOne()
)->create();