Skip to content

Commit ec4a51a

Browse files
authored
Clone user actions for entity lazily (#943)
1 parent 312d7ef commit ec4a51a

File tree

4 files changed

+148
-99
lines changed

4 files changed

+148
-99
lines changed

src/Model.php

+12-36
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,8 @@ public function assertIsModel(self $expectedModelInstance = null): void
350350
}
351351

352352
if ($expectedModelInstance !== null && $expectedModelInstance !== $this) {
353+
$expectedModelInstance->assertIsModel();
354+
353355
throw new Exception('Unexpected entity model instance');
354356
}
355357
}
@@ -437,11 +439,14 @@ public function createEntity(): self
437439
{
438440
$this->assertIsModel();
439441

440-
$this->_model = $this;
442+
$userActionsBackup = $this->userActions;
441443
try {
444+
$this->_model = $this;
445+
$this->userActions = [];
442446
$model = clone $this;
443447
} finally {
444448
$this->_model = null;
449+
$this->userActions = $userActionsBackup;
445450
}
446451
$model->_entityId = null;
447452

@@ -458,6 +463,8 @@ public function createEntity(): self
458463
*/
459464
protected function init(): void
460465
{
466+
$this->assertIsModel();
467+
461468
$this->_init();
462469

463470
if ($this->id_field) {
@@ -469,41 +476,10 @@ protected function init(): void
469476
$this->initEntityIdHooks();
470477

471478
if ($this->read_only) {
472-
return; // don't declare action for read-only model
473-
}
474-
475-
// Declare our basic Crud actions for the model.
476-
$this->addUserAction('add', [
477-
'fields' => true,
478-
'modifier' => Model\UserAction::MODIFIER_CREATE,
479-
'appliesTo' => Model\UserAction::APPLIES_TO_NO_RECORDS,
480-
'callback' => 'save',
481-
'description' => 'Add ' . $this->getModelCaption(),
482-
]);
483-
484-
$this->addUserAction('edit', [
485-
'fields' => true,
486-
'modifier' => Model\UserAction::MODIFIER_UPDATE,
487-
'appliesTo' => Model\UserAction::APPLIES_TO_SINGLE_RECORD,
488-
'callback' => 'save',
489-
]);
490-
491-
$this->addUserAction('delete', [
492-
'appliesTo' => Model\UserAction::APPLIES_TO_SINGLE_RECORD,
493-
'modifier' => Model\UserAction::MODIFIER_DELETE,
494-
'callback' => function ($model) {
495-
return $model->delete();
496-
},
497-
]);
498-
499-
$this->addUserAction('validate', [
500-
//'appliesTo' => any!
501-
'description' => 'Provided with modified values will validate them but will not save',
502-
'modifier' => Model\UserAction::MODIFIER_READ,
503-
'fields' => true,
504-
'system' => true, // don't show by default
505-
'args' => ['intent' => 'string'],
506-
]);
479+
return; // don't declare user action for read-only model
480+
}
481+
482+
$this->initUserActions();
507483
}
508484

509485
private function initEntityIdAndAssertUnchanged(): void

src/Model/UserAction.php

+47-31
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
namespace Atk4\Data\Model;
66

77
use Atk4\Core\DiContainerTrait;
8-
use Atk4\Core\Exception;
8+
use Atk4\Core\Exception as CoreException;
99
use Atk4\Core\InitializerTrait;
1010
use Atk4\Core\TrackableTrait;
11+
use Atk4\Data\Exception;
1112
use Atk4\Data\Model;
1213

1314
/**
@@ -25,9 +26,6 @@ class UserAction
2526
use InitializerTrait;
2627
use TrackableTrait;
2728

28-
/** @var Model|null */
29-
private $entity;
30-
3129
/** Defining records scope of the action */
3230
public const APPLIES_TO_NO_RECORDS = 'none'; // e.g. add
3331
public const APPLIES_TO_SINGLE_RECORD = 'single'; // e.g. archive
@@ -79,6 +77,50 @@ class UserAction
7977
/** @var bool Atomic action will automatically begin transaction before and commit it after completing. */
8078
public $atomic = true;
8179

80+
public function isOwnerEntity(): bool
81+
{
82+
/** @var Model */
83+
$owner = $this->getOwner();
84+
85+
return $owner->isEntity();
86+
}
87+
88+
public function getModel(): Model
89+
{
90+
/** @var Model */
91+
$owner = $this->getOwner();
92+
93+
return $owner->getModel(true);
94+
}
95+
96+
public function getEntity(): Model
97+
{
98+
/** @var Model */
99+
$owner = $this->getOwner();
100+
101+
$owner->assertIsEntity();
102+
103+
return $owner;
104+
}
105+
106+
/**
107+
* @return static
108+
*/
109+
public function getActionForEntity(Model $entity): self
110+
{
111+
/** @var Model */
112+
$owner = $this->getOwner();
113+
114+
$entity->assertIsEntity($owner);
115+
foreach ($owner->getUserActions() as $name => $action) {
116+
if ($action === $this) {
117+
return $entity->getUserAction($name); // @phpstan-ignore-line
118+
}
119+
}
120+
121+
throw new Exception('Action instance not found in model');
122+
}
123+
82124
/**
83125
* Attempt to execute callback of the action.
84126
*
@@ -110,7 +152,7 @@ public function execute(...$args)
110152
}
111153

112154
return $run();
113-
} catch (Exception $e) {
155+
} catch (CoreException $e) {
114156
$e->addMoreInfo('action', $this);
115157

116158
throw $e;
@@ -210,32 +252,6 @@ public function getConfirmation()
210252
return $this->confirmation;
211253
}
212254

213-
/**
214-
* Return model associated with this action.
215-
*/
216-
public function getModel(): Model
217-
{
218-
return $this->getOwner()->getModel(true); // @phpstan-ignore-line
219-
}
220-
221-
public function getEntity(): Model
222-
{
223-
if ($this->getOwner()->isEntity()) { // @phpstan-ignore-line
224-
return $this->getOwner(); // @phpstan-ignore-line
225-
}
226-
227-
if ($this->entity === null) {
228-
$this->setEntity($this->getOwner()->createEntity()); // @phpstan-ignore-line
229-
}
230-
231-
return $this->entity;
232-
}
233-
234-
public function setEntity(Model $entity): void
235-
{
236-
$this->entity = $entity;
237-
}
238-
239255
public function getCaption(): string
240256
{
241257
return $this->caption ?? ucwords(str_replace('_', ' ', $this->short_name));

src/Model/UserActionsTrait.php

+86-30
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Atk4\Data\Model;
66

77
use Atk4\Core\Factory;
8-
use Atk4\Data\Model;
98

109
trait UserActionsTrait
1110
{
@@ -14,22 +13,23 @@ trait UserActionsTrait
1413
*
1514
* @var string|array
1615
*/
17-
public $_default_seed_action = [Model\UserAction::class];
16+
public $_default_seed_action = [UserAction::class];
1817

1918
/**
20-
* @var array<string, Model\UserAction> Collection of user actions - using key as action system name
19+
* @var array<string, UserAction> Collection of user actions - using key as action system name
2120
*/
2221
protected $userActions = [];
2322

2423
/**
2524
* Register new user action for this model. By default UI will allow users to trigger actions
2625
* from UI.
2726
*
28-
* @param string $name Action name
2927
* @param array|\Closure $defaults
3028
*/
31-
public function addUserAction(string $name, $defaults = []): Model\UserAction
29+
public function addUserAction(string $name, $defaults = []): UserAction
3230
{
31+
$this->assertIsModel();
32+
3333
if ($defaults instanceof \Closure) {
3434
$defaults = ['callback' => $defaults];
3535
}
@@ -38,54 +38,88 @@ public function addUserAction(string $name, $defaults = []): Model\UserAction
3838
$defaults['caption'] = $this->readableCaption($name);
3939
}
4040

41-
/** @var Model\UserAction $action */
41+
/** @var UserAction $action */
4242
$action = Factory::factory($this->_default_seed_action, $defaults);
4343

4444
$this->_addIntoCollection($name, $action, 'userActions');
4545

4646
return $action;
4747
}
4848

49+
/**
50+
* Returns true if user action with a corresponding name exists.
51+
*/
52+
public function hasUserAction(string $name): bool
53+
{
54+
if ($this->isEntity() && $this->getModel()->hasUserAction($name)) {
55+
return true;
56+
}
57+
58+
return $this->_hasInCollection($name, 'userActions');
59+
}
60+
61+
private function addUserActionFromModel(string $name, UserAction $action): void
62+
{
63+
$this->assertIsEntity();
64+
$action->getOwner()->assertIsModel(); // @phpstan-ignore-line
65+
66+
// clone action and store it in entity
67+
$action = clone $action;
68+
$action->unsetOwner();
69+
$this->_addIntoCollection($name, $action, 'userActions');
70+
}
71+
4972
/**
5073
* Returns list of actions for this model. Can filter actions by records they apply to.
5174
* It will also skip system user actions (where system === true).
5275
*
53-
* @param string $appliesTo e.g. Model\UserAction::APPLIES_TO_ALL_RECORDS
76+
* @param string $appliesTo e.g. UserAction::APPLIES_TO_ALL_RECORDS
5477
*
55-
* @return array<string, Model\UserAction>
78+
* @return array<string, UserAction>
5679
*/
5780
public function getUserActions(string $appliesTo = null): array
5881
{
82+
if ($this->isEntity()) {
83+
foreach (array_diff_key($this->getModel()->getUserActions($appliesTo), $this->userActions) as $name => $action) {
84+
$this->addUserActionFromModel($name, $action);
85+
}
86+
}
87+
5988
return array_filter($this->userActions, function ($action) use ($appliesTo) {
6089
return !$action->system && ($appliesTo === null || $action->appliesTo === $appliesTo);
6190
});
6291
}
6392

6493
/**
65-
* Returns true if user action with a corresponding name exists.
66-
*
67-
* @param string $name UserAction name
94+
* Returns one action object of this model. If action not defined, then throws exception.
6895
*/
69-
public function hasUserAction(string $name): bool
96+
public function getUserAction(string $name): UserAction
7097
{
71-
return $this->_hasInCollection($name, 'userActions');
98+
if ($this->isEntity() && !$this->_hasInCollection($name, 'userActions') && $this->getModel()->hasUserAction($name)) {
99+
$this->addUserActionFromModel($name, $this->getModel()->getUserAction($name));
100+
}
101+
102+
return $this->_getFromCollection($name, 'userActions');
72103
}
73104

74105
/**
75-
* Returns one action object of this model. If action not defined, then throws exception.
106+
* Remove specified action.
76107
*
77-
* @param string $name Action name
108+
* @return $this
78109
*/
79-
public function getUserAction(string $name): Model\UserAction
110+
public function removeUserAction(string $name)
80111
{
81-
return $this->_getFromCollection($name, 'userActions');
112+
$this->assertIsModel();
113+
114+
$this->_removeFromCollection($name, 'userActions');
115+
116+
return $this;
82117
}
83118

84119
/**
85120
* Execute specified action with specified arguments.
86121
*
87-
* @param string $name UserAction name
88-
* @param mixed ...$args
122+
* @param mixed ...$args
89123
*
90124
* @return mixed
91125
*/
@@ -94,17 +128,39 @@ public function executeUserAction(string $name, ...$args)
94128
return $this->getUserAction($name)->execute(...$args);
95129
}
96130

97-
/**
98-
* Remove specified action(s).
99-
*
100-
* @return $this
101-
*/
102-
public function removeUserAction(string $name)
131+
protected function initUserActions(): void
103132
{
104-
foreach ((array) $name as $action) {
105-
$this->_removeFromCollection($action, 'userActions');
106-
}
107-
108-
return $this;
133+
// Declare our basic Crud actions for the model.
134+
$this->addUserAction('add', [
135+
'fields' => true,
136+
'modifier' => UserAction::MODIFIER_CREATE,
137+
'appliesTo' => UserAction::APPLIES_TO_NO_RECORDS,
138+
'callback' => 'save',
139+
'description' => 'Add ' . $this->getModelCaption(),
140+
]);
141+
142+
$this->addUserAction('edit', [
143+
'fields' => true,
144+
'modifier' => UserAction::MODIFIER_UPDATE,
145+
'appliesTo' => UserAction::APPLIES_TO_SINGLE_RECORD,
146+
'callback' => 'save',
147+
]);
148+
149+
$this->addUserAction('delete', [
150+
'appliesTo' => UserAction::APPLIES_TO_SINGLE_RECORD,
151+
'modifier' => UserAction::MODIFIER_DELETE,
152+
'callback' => function ($model) {
153+
return $model->delete();
154+
},
155+
]);
156+
157+
$this->addUserAction('validate', [
158+
//'appliesTo' => any!
159+
'description' => 'Provided with modified values will validate them but will not save',
160+
'modifier' => UserAction::MODIFIER_READ,
161+
'fields' => true,
162+
'system' => true, // don't show by default
163+
'args' => ['intent' => 'string'],
164+
]);
109165
}
110166
}

0 commit comments

Comments
 (0)