Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 77 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ The `HasFactory` trait is applied directly to the class you would like to genera

To use the `HasFactory` trait, you must implement the `toFactoryInstance` and `getFactoryDefinition` methods:

> [!note]
> The `HasFactory` trait does not provide you the capability of defining state methods or callbacks.
> If you need this functionality, you should define a separate `Factory` class instead.

```php
namespace App\Data;

Expand Down Expand Up @@ -141,6 +137,83 @@ Once implemented, you may call the `Reservation::factory()` method to create a n
$factory = Reservation::factory();
```

#### Dynamic State Methods

The `HasFactory` trait supports defining dynamic state methods. You can define state methods in your class using the format `get{StateName}State` and call them dynamically on the factory:

```php
namespace App\Data;

use DateTime;
use Faker\Generator;
use DirectoryTree\Dummy\HasFactory;

class Reservation
{
use HasFactory;

public function __construct(
public string $name,
public string $email,
public DateTime $datetime,
public string $status = 'pending',
public string $type = 'standard',
) {}

// Dynamic state methods
public static function getConfirmedState(): array
{
return ['status' => 'confirmed'];
}

public static function getPremiumState(): array
{
return [
'type' => 'premium',
'status' => 'confirmed',
];
}

public static function getCancelledState(): array
{
return ['status' => 'cancelled'];
}

protected static function toFactoryInstance(array $attributes): self
{
return new static(
$attributes['name'],
$attributes['email'],
$attributes['datetime'],
$attributes['status'] ?? 'pending',
$attributes['type'] ?? 'standard',
);
}

protected static function getFactoryDefinition(Generator $faker): array
{
return [
'name' => $faker->name(),
'email' => $faker->email(),
'datetime' => $faker->dateTime(),
];
}
}
```

You can then use these state methods dynamically:

```php
// Create a confirmed reservation
$confirmed = Reservation::factory()->confirmed()->make();

// Create a premium reservation
$premium = Reservation::factory()->premium()->make();

// Chain multiple states
$premiumCancelled = Reservation::factory()->premium()->cancelled()->make();
```

### Class Factory

If you need more control over the dummy data generation process, you may use the `Factory` class.
Expand Down
33 changes: 33 additions & 0 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace DirectoryTree\Dummy;

use BadMethodCallException;
use Closure;
use Faker\Factory as FakerFactory;
use Faker\Generator;
Expand All @@ -23,6 +24,7 @@ class Factory
*/
public function __construct(
protected ?int $count = null,
protected ?string $class = null,
protected ?Closure $using = null,
protected Collection $states = new Collection,
protected Collection $afterMaking = new Collection,
Expand Down Expand Up @@ -267,6 +269,7 @@ protected function newInstance(array $arguments = []): static
{
return new static(...array_values(array_merge([
'count' => $this->count,
'class' => $this->class,
'using' => $this->using,
'states' => $this->states,
'afterMaking' => $this->afterMaking,
Expand All @@ -292,4 +295,34 @@ protected function newFaker(): Generator

return FakerFactory::create();
}

/**
* Handle dynamic state method calls.
*/
public function __call(string $method, array $parameters): static
{
if (! $this->class) {
throw new BadMethodCallException('Cannot call state methods on a factory without a using class.');
}

if (! method_exists($this->class, $stateMethod = $this->getStateMethod($method))) {
throw new BadMethodCallException("Method [{$method}] does not exist on [{$this->class}].");
}

if (! is_callable([$this->class, $stateMethod])) {
throw new BadMethodCallException("Method [{$method}] is not callable on [{$this->class}].");
}

return $this->state(
call_user_func([$this->class, $stateMethod], $parameters)
);
}

/**
* Get the state method name for the given method name.
*/
protected function getStateMethod(string $method): string
{
return 'get'.ucfirst($method).'State';
}
}
2 changes: 1 addition & 1 deletion src/HasFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function (Generator $faker, array $attributes) {
*/
protected static function newFactory(array $attributes): Factory
{
return Factory::new($attributes);
return (new Factory(class: static::class))->state($attributes);
}

/**
Expand Down
60 changes: 60 additions & 0 deletions tests/Fixtures/HasFactoryWithStatesStub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace DirectoryTree\Dummy\Tests\Fixtures;

use DirectoryTree\Dummy\Data;
use DirectoryTree\Dummy\HasFactory;
use Faker\Generator;

class HasFactoryWithStatesStub
{
use HasFactory;

/**
* Admin state method.
*/
public static function getAdminState(): array
{
return [
'role' => 'admin',
'name' => 'Admin User',
'email' => '[email protected]',
];
}

/**
* Inactive state method.
*/
public static function getInactiveState(): array
{
return [
'status' => 'inactive',
];
}

/**
* Premium state method.
*/
public static function getPremiumState(): array
{
return [
'role' => 'premium',
'subscription' => 'premium',
];
}

protected static function toFactoryInstance(array $attributes): Data
{
return new Data($attributes);
}

protected static function getFactoryDefinition(Generator $faker): array
{
return [
'name' => $faker->name(),
'email' => $faker->email(),
'role' => 'user',
'status' => 'active',
];
}
}
34 changes: 34 additions & 0 deletions tests/HasFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use DirectoryTree\Dummy\Data;
use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryInstanceStub;
use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryStub;
use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryWithStatesStub;

it('can generate fake data instance', function () {
$instance = HasFactoryStub::factory()->make();
Expand All @@ -25,3 +26,36 @@
expect($instance)->toBeInstanceOf(HasFactoryInstanceStub::class);
expect($instance->attributes)->toHaveKeys(['name', 'email']);
});

it('can use dynamic state methods', function () {
$instance = HasFactoryWithStatesStub::factory()->admin()->make();

expect($instance)->toBeInstanceOf(Data::class);
expect($instance->role)->toBe('admin');
expect($instance->name)->toBe('Admin User');
expect($instance->email)->toBe('[email protected]');
});

it('can chain multiple dynamic state methods', function () {
$instance = HasFactoryWithStatesStub::factory()->admin()->inactive()->make();

expect($instance)->toBeInstanceOf(Data::class);
expect($instance->role)->toBe('admin');
expect($instance->name)->toBe('Admin User');
expect($instance->email)->toBe('[email protected]');
expect($instance->status)->toBe('inactive');
});

it('can use dynamic state methods with premium state', function () {
$instance = HasFactoryWithStatesStub::factory()->premium()->make();

expect($instance)->toBeInstanceOf(Data::class);
expect($instance->role)->toBe('premium');
expect($instance->subscription)->toBe('premium');
});

it('throws exception for non-existent state methods', function () {
expect(function () {
HasFactoryWithStatesStub::factory()->nonExistentState()->make();
})->toThrow(BadMethodCallException::class);
});