Factories

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 the entity 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 the definition 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 the definition 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();