Skip to content

Commit

Permalink
Merge pull request #168 from vierge-noire/next
Browse files Browse the repository at this point in the history
v2.7
  • Loading branch information
pabloelcolombiano authored Jul 2, 2022
2 parents ed57534 + fb50d1c commit 0ae9285
Show file tree
Hide file tree
Showing 12 changed files with 459 additions and 25 deletions.
12 changes: 11 additions & 1 deletion docs/associations.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ will create an article, with an author having itself an address in Kenya.

The second parameter of the method with can be:
* an array of field and their values
* an integer: the number
* a string (or an array of strings), which will be assigned to the tables display field
* an integer: the number of associated entities created
* a factory

Ultimately, the square bracket notation provides a mean to specify the number of associated
Expand All @@ -58,6 +59,15 @@ This can be useful if your business logic uses hard coded values, or constants.
Note that when an association has the same name as a virtual field,
the virtual field will overwrite the data prepared by the associated factory.

Similarly to the `make` method, it is possible to inject a string into an associated factory:
```php
$country = CountryFactory::make()->with('Cities', 'Nairobi')->persist();
````
or
```php
$country = CountryFactory::make()->with('Cities', ['Nairobi', 'Mombasa'])->persist();
```

## Factory injection

When building associations, you may simply provide a factory as parameter. Example:
Expand Down
9 changes: 9 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ $articles = ArticleFactory::make([
])->getEntities();
```

When injecting a single string in the factory, the latter will assign the injected string to the
[display field](https://book.cakephp.org/4/en/orm/retrieving-data-and-resultsets.html#finding-key-value-pairs) of the factory's table:
```php
$articles = ArticleFactory::make('Foo')->getEntity();
$articles = ArticleFactory::make('Foo', 3)->getEntities();
$articles = ArticleFactory::make(['Foo', 'Bar', 'Baz'])->getEntities();
```


In order to persist the data generated, use the method `persist` instead of `getEntity` resp. `getEntities`:
```php
$articles = ArticleFactory::make(3)->persist();
Expand Down
31 changes: 30 additions & 1 deletion docs/factories.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,35 @@ use Cake\Core\Configure;
Configure::write('TestFixtureNamespace', 'MyApp\Test\Factory');
```

## Setters

By default, each entity's setters are applied. You may deactivate one or several setters by default
by defining the protected property `skippedSetters` of a given factory. You may also __overwrite__ this set of setters
with the public method `skipSettersFor`.

```php
namespace App\Test\Factory;
...
class UserFactory extends BaseFactory
{
protected $skippedSetters = [
'password',
];
...
}
```

or

```php
UserFactory::make([
'username' => '[email protected]',
'password' => 'secret',
])->skipSetterFor('password')->getEntity();
```

This can be useful for setters with heavy computation costs, such as hashing.

## Property uniqueness

It is not rare to have to create entities associated with an entity that should remain
Expand Down Expand Up @@ -111,7 +140,7 @@ will be cautious whenever the property `name` is set by the developer.

Running
```php
CityFactory::make(5)->with('Country', ['name' => 'Foo'])->persist();
CityFactory::make(5)->with('Country', 'Foo')->persist();
```
will create 5 cities all associated to one unique country. If you perform that same operation again,
you will have 10 cities, all associated to one single country.
Expand Down
43 changes: 38 additions & 5 deletions src/Factory/BaseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ abstract class BaseFactory
* @var array Unique fields. Uniqueness applies only to persisted entities.
*/
protected $uniqueProperties = [];
/**
* @var array Fields on which the setters should be skipped.
*/
protected $skippedSetters = [];
/**
* The number of records the factory should create
*
Expand Down Expand Up @@ -114,7 +118,7 @@ abstract protected function getRootTableRegistryName(): string;
abstract protected function setDefaultTemplate(): void;

/**
* @param array|callable|null|int|\Cake\Datasource\EntityInterface $makeParameter Injected data
* @param array|callable|null|int|\Cake\Datasource\EntityInterface|string $makeParameter Injected data
* @param int $times Number of entities created
* @return static
*/
Expand All @@ -125,13 +129,13 @@ public static function make($makeParameter = [], int $times = 1): BaseFactory
$times = $makeParameter;
} elseif (is_null($makeParameter)) {
$factory = static::makeFromNonCallable();
} elseif (is_array($makeParameter) || $makeParameter instanceof EntityInterface) {
} elseif (is_array($makeParameter) || $makeParameter instanceof EntityInterface || is_string($makeParameter)) {
$factory = static::makeFromNonCallable($makeParameter);
} elseif (is_callable($makeParameter)) {
$factory = static::makeFromCallable($makeParameter);
} else {
throw new InvalidArgumentException('
::make only accepts an array, an integer, an EntityInterface or a callable as first parameter.
::make only accepts an array, an integer, an EntityInterface, a string or a callable as first parameter.
');
}

Expand Down Expand Up @@ -264,9 +268,13 @@ public function getAssociated(): array
*/
protected function toArray(): array
{
$dataCompiler = $this->getDataCompiler();
// Casts the default property to array
$this->skipSetterFor($this->skippedSetters);
$dataCompiler->setSkippedSetters($this->skippedSetters);
$entities = [];
for ($i = 0; $i < $this->times; $i++) {
$compiledData = $this->getDataCompiler()->getCompiledTemplateData();
$compiledData = $dataCompiler->getCompiledTemplateData();
if (is_array($compiledData)) {
$entities = array_merge($entities, $compiledData);
} else {
Expand Down Expand Up @@ -502,7 +510,7 @@ protected function setDefaultData(callable $fn)
* The data can be an array, an integer, an entity interface, a callable or a factory
*
* @param string $associationName Association name
* @param array|int|callable|\CakephpFixtureFactories\Factory\BaseFactory|\Cake\Datasource\EntityInterface $data Injected data
* @param array|int|callable|\CakephpFixtureFactories\Factory\BaseFactory|\Cake\Datasource\EntityInterface|string $data Injected data
* @return $this
*/
public function with(string $associationName, $data = [])
Expand Down Expand Up @@ -560,6 +568,31 @@ public function mergeAssociated(array $data)
return $this;
}

/**
* Per default setters defined in entities are applied.
* Here the user may define a list of fields for which setters should be ignored
*
* @param string|string[]|mixed $skippedSetters Field or list of fields for which setters ought to be skipped
* @param bool $merge Merge the first argument with the setters already skipped. False by default.
* @return $this
* @throws \CakephpFixtureFactories\Error\FixtureFactoryException is no string or array is passed
*/
public function skipSetterFor($skippedSetters, bool $merge = false)
{
if (!is_string($skippedSetters) && !is_array($skippedSetters)) {
throw new FixtureFactoryException(
'BaseFactory::skipSettersFor() accepts an array of string or a string as argument.'
);
}
$skippedSetters = (array)$skippedSetters;
if ($merge) {
$skippedSetters = array_unique(array_merge($this->skippedSetters, $skippedSetters));
}
$this->skippedSetters = $skippedSetters;

return $this;
}

/**
* Query the factory's related table without before find.
*
Expand Down
81 changes: 74 additions & 7 deletions src/Factory/DataCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@ class DataCompiler
public const IS_ASSOCIATED = '___data_compiler__is_associated';

private $dataFromDefaultTemplate = [];
/**
* @var array|\Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|callable
*/
private $dataFromInstantiation = [];
private $dataFromPatch = [];
private $dataFromAssociations = [];
private $dataFromDefaultAssociations = [];
private $primaryKeyOffset = [];
private $enforcedFields = [];
private $skippedSetters = [];

private static $inPersistMode = false;

Expand All @@ -61,7 +65,7 @@ public function __construct(BaseFactory $factory)
/**
* Data passed in the instantiation by array
*
* @param array|\Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] $data Injected data.
* @param array|\Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|string $data Injected data.
* @return void
*/
public function collectFromInstantiation($data): void
Expand Down Expand Up @@ -154,14 +158,14 @@ public function getCompiledTemplateData()

if (is_array($this->dataFromInstantiation) && isset($this->dataFromInstantiation[0])) {
$compiledTemplateData = [];
foreach ($this->dataFromInstantiation as $entity) {
if ($entity instanceof BaseFactory) {
foreach ($entity->getEntities() as $subEntity) {
foreach ($this->dataFromInstantiation as $data) {
if ($data instanceof BaseFactory) {
foreach ($data->getEntities() as $subEntity) {
$compiledTemplateData[] = $this->compileEntity($subEntity, $setPrimaryKey);
$setPrimaryKey = false;
}
} else {
$compiledTemplateData[] = $this->compileEntity($entity, $setPrimaryKey);
$compiledTemplateData[] = $this->compileEntity($data, $setPrimaryKey);
// Only the first entity gets its primary key set.
$setPrimaryKey = false;
}
Expand All @@ -174,12 +178,15 @@ public function getCompiledTemplateData()
}

/**
* @param array|callable|\Cake\Datasource\EntityInterface $injectedData Data from the injection.
* @param array|callable|\Cake\Datasource\EntityInterface|string $injectedData Data from the injection.
* @param bool $setPrimaryKey Set the primary key if this entity is alone or the first of an array.
* @return \Cake\Datasource\EntityInterface
*/
public function compileEntity($injectedData = [], bool $setPrimaryKey = false): EntityInterface
{
if (is_string($injectedData)) {
$injectedData = $this->setDisplayFieldToInjectedString($injectedData);
}
if ($injectedData instanceof EntityInterface) {
$entity = $injectedData;
} else {
Expand Down Expand Up @@ -209,13 +216,60 @@ public function compileEntity($injectedData = [], bool $setPrimaryKey = false):
*/
private function patchEntity(EntityInterface $entity, array $data): EntityInterface
{
$data = $this->setDataWithoutSetters($entity, $data);

return empty($data) ? $entity : $this->getFactory()->getTable()->patchEntity(
$entity,
$data,
$this->getFactory()->getMarshallerOptions()
);
}

/**
* When injecting a string as data, the compiler should understand that this is the value that
* should a assigned to the display field of the table.
*
* @param string $data data injected
* @return string[]
* @throws \CakephpFixtureFactories\Error\FixtureFactoryException if the display field of the factory's table is not a string
*/
private function setDisplayFieldToInjectedString(string $data): array
{
$displayField = $this->getFactory()->getTable()->getDisplayField();
if (is_string($displayField)) {
return [$displayField => $data];
}

$factory = get_class($this->getFactory());
$table = get_class($this->getFactory()->getTable());
throw new FixtureFactoryException(
'The display field of a table must be a string when injecting a string into its factory. ' .
"You injected '$data' in $factory but $table's display field is not a string."
);
}

/**
* Sets fields individually skipping the setters.
* CakePHP does not offer to skipp setters on a patchEntity/newEntity
* Therefore fields which skipped setters should be set individually,
* and removed from the dat parched.
*
* @param \Cake\Datasource\EntityInterface $entity entity build
* @param array $data data to set
* @return array $data without the fields for which the setters are ignored
*/
private function setDataWithoutSetters(EntityInterface $entity, array $data): array
{
foreach ($data as $field => $value) {
if (in_array($field, $this->skippedSetters)) {
$entity->set($field, $value, ['setter' => false]);
unset($data[$field]);
}
}

return $data;
}

/**
* Step 1: Create an entity from the default template.
*
Expand All @@ -227,8 +281,10 @@ private function getEntityFromDefaultTemplate(): EntityInterface
if (is_callable($data)) {
$data = $data($this->getFactory()->getFaker());
}
$entityClassName = $this->getFactory()->getTable()->getEntityClass();
$entity = new $entityClassName();

return $this->getFactory()->getTable()->newEntity($data, $this->getFactory()->getMarshallerOptions());
return $this->patchEntity($entity, $data);
}

/**
Expand Down Expand Up @@ -608,4 +664,15 @@ public function addEnforcedFields(array $fields)
$this->enforcedFields
);
}

/**
* Sets the fields which setters should be skipped
*
* @param array $skippedSetters setters to skip
* @return void
*/
public function setSkippedSetters(array $skippedSetters): void
{
$this->skippedSetters = $skippedSetters;
}
}
7 changes: 7 additions & 0 deletions tests/Factory/AuthorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
*/
class AuthorFactory extends BaseFactory
{
protected $skippedSetters = [
'field_with_setter_1',
];

protected function getRootTableRegistryName(): string
{
return 'Authors';
Expand All @@ -36,6 +40,9 @@ protected function setDefaultTemplate(): void
->setDefaultData(function (Generator $faker) {
return [
'name' => $faker->name,
'field_with_setter_1' => $faker->word,
'field_with_setter_2' => $faker->word,
'field_with_setter_3' => $faker->word,
];
})
->withAddress();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ public function up()
'default' => null,
'null' => true,
])
->addColumn('field_with_setter_1', 'string', [
'default' => null,
'null' => true,
])
->addColumn('field_with_setter_2', 'string', [
'default' => null,
'null' => true,
])
->addColumn('field_with_setter_3', 'string', [
'default' => null,
'null' => true,
])
->addIndex('address_id')
->addIndex('business_address_id')
->addTimestamps('created', 'modified')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public function initialize(array $config): void
],
]);

// Since the display field is an array, the injection of string in the
// BillFactory is prohibited.
$this->setDisplayField(['street', 'amount']);

parent::initialize($config);
}

Expand Down
Loading

0 comments on commit 0ae9285

Please sign in to comment.