Skip to content

Commit

Permalink
Merge pull request #365 from ergebnis/feature/with-optional
Browse files Browse the repository at this point in the history
Enhancement: Implement WithOptionalStrategy
  • Loading branch information
ergebnis-bot committed Aug 4, 2020
2 parents 0751c29 + 8c6b6b0 commit a21bbd9
Show file tree
Hide file tree
Showing 9 changed files with 1,036 additions and 11 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/integrate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ on: # yamllint disable-line rule:truthy
- "main"

env:
MIN_COVERED_MSI: 99
MIN_MSI: 99
MIN_COVERED_MSI: 98
MIN_MSI: 98
PHP_EXTENSIONS: "mbstring"

jobs:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

For a full diff see [`0.2.1...main`][0.2.1...main].

### Added

* Implemented a `WithOptionalStrategy` ([#365]), by [@localheinz]

### Changed

* Moved resolution of `Count` to `FixtureFactory` ([#351]), by [@localheinz]
Expand Down Expand Up @@ -183,5 +187,6 @@ For a full diff see [`fa9c564...0.1.0`][fa9c564...0.1.0].
[#338]: https://github.com/ergebnis/factory-bot/pull/338
[#351]: https://github.com/ergebnis/factory-bot/pull/351
[#353]: https://github.com/ergebnis/factory-bot/pull/353
[#365]: https://github.com/ergebnis/factory-bot/pull/365

[@localheinz]: https://github.com/localheinz
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
MIN_COVERED_MSI:=99
MIN_MSI:=99
MIN_COVERED_MSI:=98
MIN_MSI:=98

.PHONY: it
it: coding-standards static-code-analysis tests tests-example ## Runs the coding-standards, static-code-analysis, tests, and tests-example targets
Expand Down
172 changes: 165 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ $closure = static function (Generator $faker, FactoryBot\FixtureFactory $fixture
};
```

The fixture factory will resolve the field definition to `null` or to the return value of invoking the closure with the instance of `Faker\Generator` composed into the fixture factory.
A fixture factory using the [`DefaultStrategy`](#defaultstrategy) will resolve the field definition to `null` or to the return value of invoking the closure with the instance of `Faker\Generator` composed into the fixture factory.

```php
<?php
Expand All @@ -330,11 +330,35 @@ $user = $fixtureFactory->createOne(Entity\User::class);
var_dump($user->location()); // null or a random city
```

A fixture factory using the [`WithOptionalStrategy`](#withoptionalstrategy) will resolve the field definition to the return value of invoking the closure with the instance of `Faker\Generator` composed into the fixture factory.

```php
<?php

use Ergebnis\FactoryBot;
use Example\Entity;
use Faker\Generator;

/** @var FactoryBot\FixtureFactory $fixtureFactory */
$fixtureFactory->define(Entity\User::class, [
'location' => FactoryBot\FieldDefinition::optionalClosure(static function (Generator $faker): string {
return $faker->city;
}),
]);

$withOptionalFixtureFactory = $fixtureFactory->withOptional();

/** @var Entity\User $user */
$user = $withOptionalFixtureFactory->createOne(Entity\User::class);

var_dump($user->location()); // a random city
```

##### `FieldDefinition::reference()`

`FieldDefinition::reference()` accepts the class name of an entity or embeddable.

The fixture factory will resolve the field definition to an instance of the entity or embeddable class populated through the fixture factory.
Every fixture factory will resolve the field definition to an instance of the entity or embeddable class populated through the fixture factory.

```php
<?php
Expand All @@ -359,7 +383,7 @@ var_dump($user->avatar()); // an instance of Entity\Avatar

`FieldDefinition::optionalReference()` accepts the class name of an entity or embeddable.

The fixture factory will resolve the field definition to `null` or to an instance of the entity or embeddable class populated through the fixture factory.
A fixture factory using the [`DefaultStrategy`](#defaultstrategy)] will resolve the field definition to `null` or to an instance of the entity or embeddable class populated through the fixture factory.

```php
<?php
Expand All @@ -378,6 +402,27 @@ $repository = $fixtureFactory->createOne(Entity\Repository::class);
var_dump($repository->template()); // null or an instance of Entity\Repository
```

A fixture factory using the [`WithOptionalStrategy`](#withoptionalstrategy)] will resolve the field definition to an instance of the entity or embeddable class populated through the fixture factory.

```php
<?php

use Ergebnis\FactoryBot;
use Example\Entity;

/** @var FactoryBot\FixtureFactory $fixtureFactory */
$fixtureFactory->define(Entity\Repository::class, [
'template' => FactoryBot\FieldDefinition::optionalReference(Entity\Repository::class),
]);

$withOptionalFixtureFactory = $fixtureFactory->withOptional();

/** @var Entity\Repository $repository */
$repository = $withOptionalFixtureFactory->createOne(Entity\Repository::class);

var_dump($repository->template()); // an instance of Entity\Repository
```

:exclamation: When resolving the reference, the fixture factory needs to be aware of the referenced entity or embeddable.

##### `FieldDefinition::references()`
Expand All @@ -401,7 +446,7 @@ $otherCount = FactoryBot\Count::between(

:bulb: When you create the count from minimum and maximum values, the fixture factory will resolve its actual value before creating references. This way, you can have variation in the number of references - any number between the minimum and maximum can be assumed.

The fixture factory will resolve the field definition to an array of instances of the entity or embeddable class populated through the fixture factory.
A fixture factory using the [`DefaultStrategy`](#defaultstrategy) will resolve the field definition to an array of instances of the entity or embeddable class populated through the fixture factory. Depending on the value of `$count`, the array might be empty.

```php
<?php
Expand All @@ -428,13 +473,42 @@ var_dump($organization->members()); // array with 5 instances of Entity\Use
var_dump($organization->repositories()); // array with 0-20 instances of Entity\Repository
```

A fixture factory using the [`WithOptionalStrategy`](#withoptionalstrategy) will resolve the field definition to an array of instances of the entity or embeddable class populated through the fixture factory. The array will contain at least one reference.

```php
<?php

use Ergebnis\FactoryBot;
use Example\Entity;

/** @var FactoryBot\FixtureFactory $fixtureFactory */
$fixtureFactory->define(Entity\Organization::class, [
'members' => FactoryBot\FieldDefinition::references(
Entity\User::class,
FactoryBot\Count::exact(5)
),
'repositories' => FactoryBot\FieldDefinition::references(
Entity\Repository::class,
FactoryBot\Count::between(0, 20)
),
]);

$withOptionalFixtureFactory = $fixtureFactory->withOptional();

/** @var Entity\Organization $organization */
$organization = $withOptionalFixtureFactory->createOne(Entity\Organization::class);

var_dump($organization->members()); // array with 5 instances of Entity\User
var_dump($organization->repositories()); // array with 1-20 instances of Entity\Repository
```

:exclamation: When resolving the references, the fixture factory needs to be aware of the referenced entity or embeddable.

##### `FieldDefinition::sequence()`

`FieldDefinition::sequence()` accepts a string containing the `%d` placeholder at least once and an optional initial number (defaults to `1`).

The fixture factory will resolve the field definition by replacing all occurrences of the placeholder `%d` in the string with the sequential number's current value. The sequential number will then be incremented by `1` for the next run.
Every fixture factory will resolve the field definition by replacing all occurrences of the placeholder `%d` in the string with the sequential number's current value. The sequential number will then be incremented by `1` for the next run.

```php
<?php
Expand Down Expand Up @@ -464,7 +538,7 @@ var_dump($userTwo->login()); // 'user-2'

`FieldDefinition::optionalSequence()` accepts a string containing the `%d` placeholder at least once and an optional initial number (defaults to `1`).

The fixture factory will resolve the field definition to `null` or by replacing all occurrences of the placeholder `%d` in the string with the sequential number's current value. The sequential number will then be incremented by `1` for the next run.
A fixture factory using the [`DefaultStrategy`](#defaultstrategy) will resolve the field definition to `null` or by replacing all occurrences of the placeholder `%d` in the string with the sequential number's current value. The sequential number will then be incremented by `1` for the next run.

```php
<?php
Expand All @@ -490,6 +564,34 @@ var_dump($userOne->location()); // null or 'City 1'
var_dump($userTwo->location()); // null or 'City 1' or 'City 2'
```

A fixture factory using the [`WithOptionalStrategy`](#withoptionalstrategy) will resolve the field definition by replacing all occurrences of the placeholder `%d` in the string with the sequential number's current value. The sequential number will then be incremented by `1` for the next run.

```php
<?php

use Ergebnis\FactoryBot;
use Example\Entity;

/** @var FactoryBot\FixtureFactory $fixtureFactory */
$fixtureFactory->define(Entity\User::class, [
'location' => FactoryBot\FieldDefinition::optionalSequence(
'City %d',
1
),
]);

$withOptionalFixtureFactory = $fixtureFactory->withOptional();

/** @var Entity\User $userOne */
$userOne = $withOptionalFixtureFactory->createOne(Entity\User::class);

/** @var Entity\User $userTwo */
$userTwo = $withOptionalFixtureFactory->createOne(Entity\User::class);

var_dump($userOne->location()); // 'City 1'
var_dump($userTwo->location()); // 'City 2'
```

##### `FieldDefinition::value()`

`FieldDefinition::value()` accepts an arbitrary value.
Expand Down Expand Up @@ -536,7 +638,7 @@ var_dump($user->login()); // 'localheinz'

`FieldDefinition::optionalValue()` accepts an arbitrary value.

The fixture factory will resolve the field definition to `null` or the value.
A fixture factory using the [`DefaultStrategy`](#defaultstrategy) will resolve the field definition to `null` or the value.

```php
<?php
Expand All @@ -555,6 +657,27 @@ $user = $fixtureFactory->create(Entity\User::class);
var_dump($user->location()); // null or 'Berlin'
```

A fixture factory using the [`WithOptionalStrategy`](#withoptionalstrategy) will resolve the field definition to the value.

```php
<?php

use Ergebnis\FactoryBot;
use Example\Entity;

/** @var FactoryBot\FixtureFactory $fixtureFactory */
$fixtureFactory->define(Entity\User::class, [
'location' => FactoryBot\FieldDefinition::optionalValue('Berlin'),
]);

$withOptionalFixtureFactory = $fixtureFactory->withOptional();

/** @var Entity\User $user */
$user = $withOptionalFixtureFactory->create(Entity\User::class);

var_dump($user->location()); // 'Berlin'
```

### Loading entity definitions

Instead of creating entity definitions inline, you can implement the [`EntityDefinitionProvider`](src/EntityDefinitionProvider.php) interface and load entity definitions contained within a directory with the fixture factory.
Expand Down Expand Up @@ -616,6 +739,41 @@ abstract class AbstractTestCase extends Framework\TestCase

Now that you have created (or loaded) entity definitions, you can create Doctrine entities populated with fake data.

#### `DefaultStrategy`

The fixture factory uses a `DefaultStrategy` for resolving field definitions.

This strategy involves random behavior, and

- [`FieldDefinition::optionalClosure()`](#fielddefinitionoptionalclosure) might be resolved to `null` a concrete value
- [`FieldDefinition::optionalReference()`](#fielddefinitionoptionalreference) might be resolved to `null` or a concrete reference
- [`FieldDefinition::optionalSequence()`](#fielddefinitionoptionalsequence) might be resolved to `null` or a concrete value
- [`FieldDefinition::optionalValue()`](#fielddefinitionoptionalvalue) might be resolved to `null` or a concrete value
- [`FieldDefinition::references()`](#fielddefinitionreferences) might be resolved to an empty `array` or an `array` of references

:bulb: You might have a scenario where you have entity definitions that use optional field definitions, but would like to create an entity where these optional field definitions are resolved to concrete values or references. In this case you can use a fixture factory with the `WithOptionalStrategy`.

#### `WithOptionalStrategy`

To create a fixture factory using the `WithOptionalStrategy` out of an available fixture factory, invoke `withOptional()`:

```php
<?php

use Ergebnis\FactoryBot;

/** @var FactoryBot\FixtureFactory $fixtureFactory */
$withOptionalFixtureFactory = $fixtureFactory->withOptional();
```

This strategy still involves random behavior, but

- [`FieldDefinition::optionalClosure()`](#fielddefinitionoptionalclosure) will be resolved to a concrete value
- [`FieldDefinition::optionalReference()`](#fielddefinitionoptionalreference) will be resolved to a concrete value
- [`FieldDefinition::optionalSequence()`](#fielddefinitionoptionalsequence) will be resolved to a concrete value
- [`FieldDefinition::optionalValue()`](#fielddefinitionoptionalvalue) will be resolved to a concrete value
- [`FieldDefinition::references()`](#fielddefinitionreferences) will be resolved to a non-empty `array`

#### `FixtureFactory::createOne()`

`FixtureFactory::createOne()` accepts the class name of an entity and an optional map of entity field names to field definitions that should override the field definitions for that specific entity.
Expand Down
8 changes: 8 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,12 @@
<code>$expected</code>
</MixedAssignment>
</file>
<file src="test/Unit/Strategy/WithOptionalStrategyTest.php">
<MixedAssignment occurrences="4">
<code>$resolved</code>
<code>$expected</code>
<code>$resolved</code>
<code>$expected</code>
</MixedAssignment>
</file>
</files>
19 changes: 19 additions & 0 deletions src/FixtureFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,25 @@ public function createMany(string $className, Count $count, array $fieldDefiniti
}, \range(1, $resolved));
}

/**
* Returns a fixture factory that utilizes the WithOptionalStrategy.
*
* With this strategy
*
* - an optional reference is always resolved
* - references resolve to an array containing at least one reference
*
* @return self
*/
public function withOptional(): self
{
$instance = clone $this;

$instance->resolutionStrategy = new Strategy\WithOptionalStrategy();

return $instance;
}

/**
* Enables persisting of entities after creation.
*/
Expand Down
54 changes: 54 additions & 0 deletions src/Strategy/WithOptionalStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2020 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/factory-bot
*/

namespace Ergebnis\FactoryBot\Strategy;

use Ergebnis\FactoryBot\Count;
use Ergebnis\FactoryBot\FieldDefinition;
use Ergebnis\FactoryBot\FixtureFactory;
use Faker\Generator;

/**
* @internal
*/
final class WithOptionalStrategy implements ResolutionStrategy
{
public function resolveFieldValue(
Generator $faker,
FixtureFactory $fixtureFactory,
FieldDefinition\Resolvable $fieldDefinition
) {
return $fieldDefinition->resolve(
$faker,
$fixtureFactory
);
}

public function resolveCount(Generator $faker, Count $count): int
{
if ($count->minimum() === $count->maximum()) {
return $count->minimum();
}

$resolved = $faker->numberBetween(
$count->minimum(),
$count->maximum()
);

if (0 === $resolved) {
return 1;
}

return $resolved;
}
}
Loading

0 comments on commit a21bbd9

Please sign in to comment.