Skip to content

Commit

Permalink
Merge pull request #209 from vierge-noire/next
Browse files Browse the repository at this point in the history
v2.8
  • Loading branch information
pabloelcolombiano committed Jun 5, 2023
2 parents cba679d + bf24dee commit 3cc6b9d
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 3 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
}
},
"scripts": {
"test": "phpunit --colors=always",
"mysql": "bash run_tests.sh Mysql",
"pgsql": "bash run_tests.sh Postgres",
"sqlite": "bash run_tests.sh Sqlite",
Expand Down
16 changes: 16 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,19 @@ $articles = ArticleFactory::make(function(ArticleFactory $factory, Generator $fa
];
}, 3)->persist();
```

### Dot notation for array fields

You might come across fields storing data in array format, with a given default value set in your factories.
It is possible to overwrite only a part of the array using the dot notation.

Considering for example that the field `array_field` stores an array with keys `key1`and `key2`, you can
overwrite the value of `key2` only and keep the default value of `key1` as follows:

```php
use App\Test\Factory\ArticleFactory;
...
$article = ArticleFactory::make(['array_field.key2' => 'newValue'])->persist();
// or
$article = ArticleFactory::make()->setField('array_field.key2', 'newValue')->persist();
```
4 changes: 4 additions & 0 deletions docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ This method will return the number of entries in the table of the given factory.
## ArticleFactory::get()
This method will return an entity based on its primary key.
More documentation on the `get` method [here](https://book.cakephp.org/4/en/orm/retrieving-data-and-resultsets.html#getting-a-single-entity-by-primary-key).

## ArticleFactory::firstOrFail($condition)
This method will return an entity based on the optional conditions passed as parameter.
More documentation on the `firstOrFail` method [here](https://book.cakephp.org/4/en/orm/query-builder.html#getting-results).
12 changes: 12 additions & 0 deletions src/Factory/BaseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -641,4 +641,16 @@ public static function count(): int
{
return self::find()->count();
}

/**
* Count the factory's related table entries without before find.
*
* @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions The conditions to filter on.
* @return \Cake\Datasource\EntityInterface|array The first result from the ResultSet.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When there is no first record.
*/
public static function firstOrFail($conditions = null)
{
return self::find()->where($conditions)->firstOrFail();
}
}
42 changes: 39 additions & 3 deletions src/Factory/DataCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Cake\Datasource\EntityInterface;
use Cake\ORM\Association\BelongsTo;
use Cake\ORM\Association\HasOne;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;
use CakephpFixtureFactories\Error\FixtureFactoryException;
use CakephpFixtureFactories\Error\PersistenceException;
Expand Down Expand Up @@ -222,17 +223,52 @@ public function compileEntity($injectedData = [], bool $setPrimaryKey = false):
private function patchEntity(EntityInterface $entity, array $data): EntityInterface
{
$data = $this->setDataWithoutSetters($entity, $data);
if (empty($data)) {
return $entity;
}

$data = $this->castArrayNotation($entity, $data);

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

/**
* Detect if a field.subvalue is found in the patch data.
* If so, merge recursively with the existing data
*
* @param \Cake\Datasource\EntityInterface $entity entity to patch
* @param array $data data to patch
* @return array
* @throws \CakephpFixtureFactories\Error\FixtureFactoryException if an array notation is merged with a string, or a non array
*/
private function castArrayNotation(EntityInterface $entity, array $data): array
{
foreach ($data as $key => $value) {
if (!strpos($key, '.')) {
continue;
}
$subData = Hash::expand([$key => $value]);
$rootKey = array_key_first($subData);
$entityValue = $entity->get($rootKey) ?? [];
if (!is_array($entityValue)) {
throw new FixtureFactoryException(
"Value $entityValue cannot be merged with array notation $key => $value"
);
}
$data[$rootKey] = array_replace_recursive($entityValue, $subData[$rootKey]);
unset($data[$key]);
}

return $data;
}

/**
* 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.
* should be assigned to the display field of the table.
*
* @param string $data data injected
* @return string[]
Expand All @@ -257,7 +293,7 @@ private function setDisplayFieldToInjectedString(string $data): array
* 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.
* and removed from the data patched.
*
* @param \Cake\Datasource\EntityInterface $entity entity build
* @param array $data data to set
Expand Down
1 change: 1 addition & 0 deletions tests/Factory/ArticleFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* @method \TestApp\Model\Entity\Article[] getEntities()
* @method \TestApp\Model\Entity\Article|\TestApp\Model\Entity\Article[] persist()
* @method static \TestApp\Model\Entity\Article get(mixed $primaryKey, array $options = [])
* @method static \TestApp\Model\Entity\Article firstOrFail($conditions = null)
*/
class ArticleFactory extends BaseFactory
{
Expand Down
7 changes: 7 additions & 0 deletions tests/Factory/AuthorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@
* @method \TestApp\Model\Entity\Author[] getEntities()
* @method \TestApp\Model\Entity\Author|\TestApp\Model\Entity\Author[] persist()
* @method static \TestApp\Model\Entity\Author get(mixed $primaryKey, array $options = [])
* @method static \TestApp\Model\Entity\Author firstOrFail($conditions = null)
*/
class AuthorFactory extends BaseFactory
{
public const JSON_FIELD_DEFAULT_VALUE = [
'subField1' => 'subFieldValue1',
'subField2' => 'subFieldValue2',
];

protected $skippedSetters = [
'field_with_setter_1',
];
Expand All @@ -43,6 +49,7 @@ protected function setDefaultTemplate(): void
'field_with_setter_1' => $faker->word,
'field_with_setter_2' => $faker->word,
'field_with_setter_3' => $faker->word,
'json_field' => self::JSON_FIELD_DEFAULT_VALUE,
];
})
->withAddress();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ public function up()
'default' => null,
'null' => true,
])
->addColumn('json_field', 'string', [
'default' => null,
'null' => true,
])
->addIndex('address_id')
->addIndex('business_address_id')
->addTimestamps('created', 'modified')
Expand Down
1 change: 1 addition & 0 deletions tests/TestApp/src/Model/Entity/Author.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* @property int $address_id
* @property int|null $business_address_id
* @property string|null $biography
* @property string|null $json_field
* @property \Cake\I18n\FrozenTime $created
* @property \Cake\I18n\FrozenTime|null $modified
*
Expand Down
2 changes: 2 additions & 0 deletions tests/TestApp/src/Model/Table/AuthorsTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public function initialize(array $config): void
],
]);

$this->getSchema()->setColumnType('json_field', 'json');

parent::initialize($config);
}
}
133 changes: 133 additions & 0 deletions tests/TestCase/Factory/BaseFactoryArrayNotationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);

/**
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) 2020 Juan Pablo Ramirez and Nicolas Masson
* @link https://webrider.de/
* @since 1.0.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/

namespace CakephpFixtureFactories\Test\TestCase\Factory;

use Cake\Core\Configure;
use Cake\TestSuite\TestCase;
use CakephpFixtureFactories\Error\FixtureFactoryException;
use CakephpFixtureFactories\Test\Factory\ArticleFactory;
use CakephpFixtureFactories\Test\Factory\AuthorFactory;

class BaseFactoryArrayNotationTest extends TestCase
{
public static function setUpBeforeClass(): void
{
Configure::write('FixtureFactories.testFixtureNamespace', 'CakephpFixtureFactories\Test\Factory');
}

public static function tearDownAfterClass(): void
{
Configure::delete('FixtureFactories.testFixtureNamespace');
}

public function testBaseFactoryArrayNotation_default_value()
{
AuthorFactory::make()->persist();

$author = AuthorFactory::firstOrFail();
$this->assertSame(AuthorFactory::JSON_FIELD_DEFAULT_VALUE, $author->json_field);
}

public function testBaseFactoryArrayNotation_overwrite_default_value()
{
$value = ['c' => 'd'];
AuthorFactory::make(['json_field' => $value])->persist();

$author = AuthorFactory::firstOrFail();
$this->assertSame($value, $author->json_field);
}

public function testBaseFactoryArrayNotation_overwrite_one_field()
{
$author = AuthorFactory::make(['json_field.subField1' => 'newValue'])->getEntity();

$expectedValue = AuthorFactory::JSON_FIELD_DEFAULT_VALUE;
$expectedValue['subField1'] = 'newValue';

$this->assertSame($expectedValue, $author->json_field);
$this->assertNull($author->get('json_field.subField1'));
}

public function testBaseFactoryArrayNotation_overwrite_one_field_with_set_field()
{
$author = AuthorFactory::make()
->setField('json_field.subField1', 'newValue')
->getEntity();

$expectedValue = AuthorFactory::JSON_FIELD_DEFAULT_VALUE;
$expectedValue['subField1'] = 'newValue';

$this->assertSame($expectedValue, $author->json_field);
$this->assertNull($author->get('json_field.subField1'));
}

public function testBaseFactoryArrayNotation_overwrite_one_field_with_deep_association()
{
$author = AuthorFactory::make(['json_field.subField1' => [
'subSubField1' => 'subSubValue1',
'subSubField2' => 'subSubValue2',
]])
->setField('json_field.subField1.subSubField2', 'blah')
->getEntity();

$expectedValue = [
'subField1' => [
'subSubField1' => 'subSubValue1',
'subSubField2' => 'blah',
],
'subField2' => 'subFieldValue2',
];

$this->assertSame($expectedValue, $author->json_field);
}

public function testBaseFactoryArrayNotation_overwrite_one_field_with_set_field_on_association()
{
$article = ArticleFactory::make()->withAuthors(
AuthorFactory::make(2)
->setField('json_field.subField1', 'newValue')->getEntities()
)->getEntity();

$expectedValue = AuthorFactory::JSON_FIELD_DEFAULT_VALUE;
$expectedValue['subField1'] = 'newValue';

$this->assertSame($expectedValue, $article->authors[0]->json_field);
$this->assertSame($expectedValue, $article->authors[1]->json_field);
$this->assertNull($article->authors[0]->get('json_field.subField1'));
$this->assertNull($article->authors[1]->get('json_field.subField1'));
}

public function testBaseFactoryArrayNotation_with_undefined_value()
{
$author = AuthorFactory::make()
->setField('non-existing_json_field.subField1', 'newValue')
->getEntity();

$expectedValue = AuthorFactory::JSON_FIELD_DEFAULT_VALUE;

$this->assertSame(['subField1' => 'newValue'], $author->get('non-existing_json_field'));
$this->assertSame($expectedValue, $author->json_field);
}

public function testBaseFactoryArrayNotation_with_non_array_value()
{
$this->expectException(FixtureFactoryException::class);
$this->expectExceptionMessage('Value foo cannot be merged with array notation json_field.subField1 => newValue');

AuthorFactory::make(['json_field' => 'foo'])
->setField('json_field.subField1', 'newValue')
->getEntity();
}
}
34 changes: 34 additions & 0 deletions tests/TestCase/Factory/BaseFactoryStaticFinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
namespace CakephpFixtureFactories\Test\TestCase\Factory;

use Cake\Datasource\Exception\RecordNotFoundException;
use Cake\Event\EventInterface;
use Cake\ORM\Query;
use Cake\ORM\TableRegistry;
Expand Down Expand Up @@ -56,4 +57,37 @@ public function testBaseFactoryStaticFind()
$this->assertSame(0, ArticleFactory::find('published')->count());
$this->assertSame($n, ArticleFactory::count());
}

public function testBaseFactoryStaticFirstOrFail()
{
$articles = ArticleFactory::make([
['title' => 'title 1'],
['title' => 'title 2'],
])->persist();

$firstArticleId = $articles[0]['id'];

$retrievedArticle = ArticleFactory::firstOrFail(['title' => 'title 1']);
$this->assertSame($firstArticleId, $retrievedArticle->id);
$this->assertSame(2, ArticleFactory::count());
}

public function testBaseFactoryStaticFirstOrFail_No_Parameters()
{
$article = ArticleFactory::make()->persist();

$retrievedArticle = ArticleFactory::firstOrFail();
$this->assertSame($article->id, $retrievedArticle->id);
}

public function testBaseFactoryStaticFirstOrFailNotFound()
{
ArticleFactory::make([
['title' => 'title 1'],
['title' => 'title 2'],
])->persist();

$this->expectException(RecordNotFoundException::class);
ArticleFactory::firstOrFail(['title' => 'title 3']);
}
}

0 comments on commit 3cc6b9d

Please sign in to comment.