Skip to content

Commit

Permalink
Allow Model.beforeSave to execute closure to satisfy injection requir…
Browse files Browse the repository at this point in the history
…ements (#90)

Allow Model.beforeFind to execute closure to satisfy injection requirement

---------

Co-authored-by: jared <[email protected]>
  • Loading branch information
jharder and jared authored Nov 18, 2023
1 parent fadfed9 commit 10b17a1
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 24 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ fields when creating a record, on the `modified_by` field again when updating
the record and it will use the associated user record's company `id` in the
`company_id` field when creating a record.

You can also provide a closure that accepts an EntityInterface and returns a bool:

```php
$this->addBehavior('Muffin/Footprint.Footprint', [
'events' => [
'Model.beforeSave' => [
'user_id' => 'new',
'company_id' => 'new',
'modified_by' => 'always',
'deleted_by' => function ($entity): bool {
return $entity->deleted !== null;
},
]
],
]);
```

### Adding middleware via event

In some cases you don't have direct access to the place where the `AuthenticationMiddleware` is added. Then you will have to add this to your `src/Application.php`
Expand Down
10 changes: 7 additions & 3 deletions src/Model/Behavior/FootprintBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Cake\ORM\Behavior;
use Cake\ORM\Query\SelectQuery;
use Cake\Utility\Hash;
use Closure;
use UnexpectedValueException;

class FootprintBehavior extends Behavior
Expand Down Expand Up @@ -163,9 +164,11 @@ protected function _injectEntity(EntityInterface $entity, ArrayObject $options,
$new = $entity->isNew() !== false;

foreach ($fields as $field => $when) {
if (!in_array($when, ['always', 'new', 'existing'])) {
if (!in_array($when, ['always', 'new', 'existing']) && !($when instanceof Closure)) {
throw new UnexpectedValueException(sprintf(
'When should be one of "always", "new" or "existing". The passed value "%s" is invalid',
'When should be one of "always", "new" or "existing", ' .
'or a closure that takes an EntityInterface and returns a bool. ' .
'The passed value "%s" is invalid.',
$when
));
}
Expand All @@ -177,7 +180,8 @@ protected function _injectEntity(EntityInterface $entity, ArrayObject $options,
if (
$when === 'always' ||
($when === 'new' && $new) ||
($when === 'existing' && !$new)
($when === 'existing' && !$new) ||
($when instanceof Closure && $when($entity))
) {
$entity->set(
$field,
Expand Down
25 changes: 25 additions & 0 deletions tests/Fixture/ArticlesFixture.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,41 @@ class ArticlesFixture extends TestFixture
'title' => 'article 1',
'created_by' => 1,
'modified_by' => 1,
'manager_id' => 10,
],
[
'title' => 'article 2',
'created_by' => 1,
'modified_by' => 2,
'manager_id' => null,
],
[
'title' => 'article 3',
'created_by' => 2,
'modified_by' => 1,
'company_id' => 2,
'manager_id' => null,
],
[
'title' => 'find article',
'created_by' => 4,
'modified_by' => 4,
'company_id' => 2,
'manager_id' => null,
],
[
'title' => 'final article',
'created_by' => 3,
'modified_by' => 4,
'company_id' => 4,
'manager_id' => null,
],
[
'title' => 'penultimate article',
'created_by' => 4,
'modified_by' => 4,
'company_id' => 1,
'manager_id' => null,
],
];
}
138 changes: 117 additions & 21 deletions tests/TestCase/Model/Behavior/FootprintBehaviorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,22 @@ public function setUp(): void
'created_by' => 'new',
'modified_by' => 'always',
'company_id' => 'always',
'manager_id' => function ($entity): bool {
return $entity->company_id == 1;
},
],
'Model.beforeFind' => [
'created_by',
'company_id',
],
'Model.beforeFind' => 'created_by',
],
'propertiesMap' => [
'company_id' => '_footprint.company.id',
'manager_id' => '_footprint.manager.id',
],
]);

$this->Table = $table;
$this->footprint = new Entity([
'id' => 2,
'company' => new Entity(['id' => 5]),
]);
}

public function tearDown(): void
Expand All @@ -51,58 +54,151 @@ public function tearDown(): void

public function testSave()
{
$entity = new Entity(['title' => 'new article']);
$entity = $this->Table->save($entity, ['_footprint' => $this->footprint]);
// Properties may still be assigned even if
// closure would be satisfied.
$entity = new Entity(['title' => 'new article', 'manager_id' => 7]);
$footprint = new Entity([
'id' => 2,
'company' => new Entity(['id' => 1]),
'manager' => new Entity(['id' => 10]),
]);

$entity = $this->Table->save($entity, ['_footprint' => $footprint]);
$expected = [
'id' => $entity->id,
'title' => 'new article',
'created_by' => 2,
'modified_by' => 2,
'company_id' => 5,
'company_id' => 1,
'manager_id' => 7,
];

$this->assertSame(
$expected,
$entity->extract(['id', 'title', 'created_by', 'modified_by', 'company_id'])
$entity->extract(['id', 'title', 'created_by', 'modified_by', 'company_id', 'manager_id'])
);

// Closure fields won't set if disallowed
// even if provided.
$entity = new Entity();
$entity->title = 'new title';
$footprint = new Entity([
'id' => 3,
'company' => new Entity(['id' => 5]),
'manager' => new Entity(['id' => 4]),
]);
$entity->title = 'new title';

$entity = $this->Table->save($entity, ['_footprint' => $footprint]);
$expected = ['id' => $entity->id, 'title' => 'new title', 'created_by' => 2, 'modified_by' => 3];
$this->assertSame($expected, $entity->extract(['id', 'title', 'created_by', 'modified_by']));
$expected = [
'id' => $entity->id,
'title' => 'new title',
'created_by' => 3,
'modified_by' => 3,
'company_id' => 5,
'manager_id' => null,
];

$this->assertSame($expected, $entity->extract(['id', 'title', 'created_by', 'modified_by', 'company_id', 'manager_id']));

// Fields won't set if a footprint isn't provided
$entity = new Entity(['title' => 'without footprint']);

$entity = $this->Table->save($entity);
$expected = ['id' => $entity->id, 'title' => 'without footprint', 'created_by' => null, 'modified_by' => null];
$this->assertSame($expected, $entity->extract(['id', 'title', 'created_by', 'modified_by']));
$expected = [
'id' => $entity->id,
'title' => 'without footprint',
'created_by' => null,
'modified_by' => null,
'manager_id' => null,
];

$this->assertSame($expected, $entity->extract(['id', 'title', 'created_by', 'modified_by', 'manager_id']));

// Satisfying closure manually still permits
// explicit field assignments
$entity = new Entity(['title' => 'different manager', 'company_id' => 1]);
$footprint = new Entity([
'id' => 3,
'company' => new Entity(['id' => 5]),
'manager' => new Entity(['id' => 4]),
]);

$entity = $this->Table->save($entity, ['_footprint' => $footprint]);
$expected = [
'id' => $entity->id,
'title' => 'different manager',
'created_by' => 3,
'modified_by' => 3,
'company_id' => 1,
'manager_id' => 4,
];

$this->assertSame($expected, $entity->extract(['id', 'title', 'created_by', 'modified_by', 'company_id', 'manager_id']));
}

public function testFind()
{
$result = $this->Table->find('all', _footprint: $this->footprint)
$footprint = new Entity(['id' => 4]);

$result = $this->Table->find('all', _footprint: $footprint)
->enableHydration(false)
->first();

$expected = ['id' => 3, 'title' => 'article 3', 'created_by' => 2, 'modified_by' => 1];
$expected = [
'id' => 4,
'title' => 'find article',
'created_by' => 4,
'modified_by' => 4,
'company_id' => 2,
'manager_id' => null,
];
$this->assertSame($expected, $result);

// Test to show value of "id" is not used from footprint if
// "Articles.created_by" is already set in condition.
$result = $this->Table->find('all', _footprint: $this->footprint)
->where(['Articles.created_by' => 1])
$result = $this->Table->find('all', _footprint: $footprint)
->where(['Articles.created_by' => 3])
->enableHydration(false)
->first();

$expected = ['id' => 1, 'title' => 'article 1', 'created_by' => 1, 'modified_by' => 1];
$expected = [
'id' => 5,
'title' => 'final article',
'created_by' => 3,
'modified_by' => 4,
'company_id' => 4,
'manager_id' => null,
];
$this->assertSame($expected, $result);

// Test to show value of "id" is not used from footprint even
// "Articles.manager_id" validates the Model.beforeSave closure
$result = $this->Table->find('all', _footprint: $footprint)
->where(['Articles.company_id' => 1])
->enableHydration(false)
->first();

$expected = [
'id' => 6,
'title' => 'penultimate article',
'created_by' => 4,
'modified_by' => 4,
'company_id' => 1,
'manager_id' => null,
];
$this->assertSame($expected, $result);
}

public function testInjectEntityException()
{
$this->expectException('UnexpectedValueException');
$this->expectExceptionMessage('When should be one of "always", "new" or "existing". The passed value "invalid" is invalid');
$this->expectExceptionMessage('When should be one of "always", "new" or "existing", ' .
'or a closure that takes an EntityInterface and returns a bool. ' .
'The passed value "invalid" is invalid.');

$footprint = new Entity([
'id' => 2,
]);

$this->Table->behaviors()->Footprint->setConfig(
'events',
Expand All @@ -113,6 +209,6 @@ public function testInjectEntityException()
]
);
$entity = new Entity(['title' => 'new article']);
$entity = $this->Table->save($entity, ['_footprint' => $this->footprint]);
$entity = $this->Table->save($entity, ['_footprint' => $footprint]);
}
}
2 changes: 2 additions & 0 deletions tests/schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
'title' => ['type' => 'string', 'length' => 255],
'created_by' => ['type' => 'integer'],
'modified_by' => ['type' => 'integer'],
'company_id' => ['type' => 'integer'],
'manager_id' => ['type' => 'integer'],
],
'constraints' => [
'primary' => [
Expand Down

0 comments on commit 10b17a1

Please sign in to comment.