Skip to content

Commit

Permalink
Feature: Add deferred resolution to model factory
Browse files Browse the repository at this point in the history
  • Loading branch information
kjohnson committed Nov 23, 2024
1 parent ac30397 commit 14f5ccd
Show file tree
Hide file tree
Showing 3 changed files with 512 additions and 1 deletion.
114 changes: 114 additions & 0 deletions src/Models/ModelFactory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Model Factory

## Introduction

Factory classes are used to programmatically create instances of models. This is useful for seeding databases, creating test data, and other situations where you need to create a lot of model instances.

```php
<?php

namespace Give\Campaigns\Factories;

class CampaignFactory extends Give\Framework\Models\Factories\ModelFactory
{
public function definition(): array
{
return [
'title' => __('GiveWP Campaign', 'give'),
'description' => $this->faker->paragraph(),
];
}
}
```

Each instance of a model created by a factory class is populated with default values defined in the `definition` method, which can be hard-coded or generated dynamically using integrated the `fakerphp/faker` library.

## Creating Models with Factories

A model can be instantiated using the factory `make()` method, which uses the defaults provided by the `definition()` method to create the model instance.

```php
use Give\Campaigns\Models\Campaign;

$campaign = Campaign::factory()->make();
```

Additionally, multiple model instances can be created using the `count()` method.

```php
use Give\Campaigns\Models\Campaign;

$campaigns = Campaign::factory()->count(3)->make();
```

### Overriding Attributes

You can override these default values by passing an array of attributes to the factory's `make()` or `create()` method.

```php
use Give\Campaigns\Models\Campaign;

$campaign Campaign::factory()->create([
'title' => 'My Custom Campaign',
]);
```

### Persisting Models

The `create()` method instantiates model instances and persists them to the database using model's `save()` method.

```php
use Give\Campaigns\Models\Campaign;

$campaign Campaign::factory()->create();
```

### Deferred Resolution

Sometimes you may need to defer the resolution of an attribute until the model is being created. Either the attribute definition is not a simple value or is otherwise expensive to instantiate.

Factory attribute definitions can be deferred using `Closure` callbacks instead of a hard-coded or generated value.

```php
public function definition(): array
{
return [
'title' => __('GiveWP Campaign', 'give'),
'description' => function() {
return prompt('Write a short description for a fundraising campaign.')
},
];
}
```

Additionally, deferred attributes are not resolved when the attribute is overridden, which prevents unnecessary computation when the default value is not used.

```php
$campaign = Campaign::factory()->create([
'description' => 'My custom description',
]);
```

## Model Relationships with Factories

Attribute definitions can also be other model factories, which can be resolved to a property value, such as an ID, to create required dependencies.

```php
public function definition(): array
{
return [
'title' => __('GiveWP Campaign', 'give'),
'formId' => DonationForm::factory()->createAndResolveTo('id'),
];
}
```

This is particularly useful when defining model relationships, where the related model is only instantiated when a model is not explicity provided as an override.

If an existing model (or model ID) is provided as an override, the factory will not instantiate an additional model.

```php
Campaign::factory()->create([
'formId' => $donationForm->id,
]);
```
30 changes: 29 additions & 1 deletion src/Models/ModelFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace StellarWP\Models;

use Closure;
use Exception;
use StellarWP\DB\DB;

Expand Down Expand Up @@ -53,6 +54,18 @@ public function make( array $attributes = [] ) {
return $this->count === 1 ? $results[0] : $results;
}

/**
* @unreleased
*/
public function makeAndResolveTo($property): Closure
{
return function() use ($property) {
return is_array($results = $this->make())
? array_column($results, $property)
: $results->$property;
};
}

/**
* @since 1.0.0
*
Expand All @@ -74,15 +87,30 @@ public function create( array $attributes = [] ) {
return $this->count === 1 ? $instances[0] : $instances;
}

/**
* @unreleased
*/
public function createAndResolveTo($property): Closure
{
return function() use ($property) {
return is_array($results = $this->create())
? array_column($results, $property)
: $results->$property;
};
}

/**
* Creates an instance of the model from the attributes and definition.
*
* @unreleased Add support for resolving Closures.
* @since 1.0.0
*
* @return M
*/
protected function makeInstance( array $attributes ) {
return new $this->model( array_merge( $this->definition(), $attributes ) );
return new $this->model(array_map(function($attribute) {
return $attribute instanceof Closure ? $attribute() : $attribute;
}, array_merge($this->definition(), $attributes)));
}

/**
Expand Down
Loading

0 comments on commit 14f5ccd

Please sign in to comment.