Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clone user actions for entity lazily #943

Merged
merged 16 commits into from
Dec 30, 2021
48 changes: 12 additions & 36 deletions src/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ public function assertIsModel(self $expectedModelInstance = null): void
}

if ($expectedModelInstance !== null && $expectedModelInstance !== $this) {
$expectedModelInstance->assertIsModel();

throw new Exception('Unexpected entity model instance');
}
}
Expand Down Expand Up @@ -437,11 +439,14 @@ public function createEntity(): self
{
$this->assertIsModel();

$this->_model = $this;
$userActionsBackup = $this->userActions;
try {
$this->_model = $this;
$this->userActions = [];
$model = clone $this;
} finally {
$this->_model = null;
$this->userActions = $userActionsBackup;
}
$model->_entityId = null;

Expand All @@ -458,6 +463,8 @@ public function createEntity(): self
*/
protected function init(): void
{
$this->assertIsModel();

$this->_init();

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

if ($this->read_only) {
return; // don't declare action for read-only model
}

// Declare our basic Crud actions for the model.
$this->addUserAction('add', [
'fields' => true,
'modifier' => Model\UserAction::MODIFIER_CREATE,
'appliesTo' => Model\UserAction::APPLIES_TO_NO_RECORDS,
'callback' => 'save',
'description' => 'Add ' . $this->getModelCaption(),
]);

$this->addUserAction('edit', [
'fields' => true,
'modifier' => Model\UserAction::MODIFIER_UPDATE,
'appliesTo' => Model\UserAction::APPLIES_TO_SINGLE_RECORD,
'callback' => 'save',
]);

$this->addUserAction('delete', [
'appliesTo' => Model\UserAction::APPLIES_TO_SINGLE_RECORD,
'modifier' => Model\UserAction::MODIFIER_DELETE,
'callback' => function ($model) {
return $model->delete();
},
]);

$this->addUserAction('validate', [
//'appliesTo' => any!
'description' => 'Provided with modified values will validate them but will not save',
'modifier' => Model\UserAction::MODIFIER_READ,
'fields' => true,
'system' => true, // don't show by default
'args' => ['intent' => 'string'],
]);
return; // don't declare user action for read-only model
}

$this->initUserActions();
}

private function initEntityIdAndAssertUnchanged(): void
Expand Down
78 changes: 47 additions & 31 deletions src/Model/UserAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
namespace Atk4\Data\Model;

use Atk4\Core\DiContainerTrait;
use Atk4\Core\Exception;
use Atk4\Core\Exception as CoreException;
use Atk4\Core\InitializerTrait;
use Atk4\Core\TrackableTrait;
use Atk4\Data\Exception;
use Atk4\Data\Model;

/**
Expand All @@ -25,9 +26,6 @@ class UserAction
use InitializerTrait;
use TrackableTrait;

/** @var Model|null */
private $entity;

/** Defining records scope of the action */
public const APPLIES_TO_NO_RECORDS = 'none'; // e.g. add
public const APPLIES_TO_SINGLE_RECORD = 'single'; // e.g. archive
Expand Down Expand Up @@ -79,6 +77,50 @@ class UserAction
/** @var bool Atomic action will automatically begin transaction before and commit it after completing. */
public $atomic = true;

public function isOwnerEntity(): bool
{
/** @var Model */
$owner = $this->getOwner();

return $owner->isEntity();
}

public function getModel(): Model
{
/** @var Model */
$owner = $this->getOwner();

return $owner->getModel(true);
}

public function getEntity(): Model
{
/** @var Model */
$owner = $this->getOwner();

$owner->assertIsEntity();

return $owner;
}

/**
* @return static
*/
public function getActionForEntity(Model $entity): self
{
/** @var Model */
$owner = $this->getOwner();

$entity->assertIsEntity($owner);
foreach ($owner->getUserActions() as $name => $action) {
if ($action === $this) {
return $entity->getUserAction($name); // @phpstan-ignore-line
}
}

throw new Exception('Action instance not found in model');
}

/**
* Attempt to execute callback of the action.
*
Expand Down Expand Up @@ -110,7 +152,7 @@ public function execute(...$args)
}

return $run();
} catch (Exception $e) {
} catch (CoreException $e) {
$e->addMoreInfo('action', $this);

throw $e;
Expand Down Expand Up @@ -210,32 +252,6 @@ public function getConfirmation()
return $this->confirmation;
}

/**
* Return model associated with this action.
*/
public function getModel(): Model
{
return $this->getOwner()->getModel(true); // @phpstan-ignore-line
}

public function getEntity(): Model
{
if ($this->getOwner()->isEntity()) { // @phpstan-ignore-line
return $this->getOwner(); // @phpstan-ignore-line
}

if ($this->entity === null) {
$this->setEntity($this->getOwner()->createEntity()); // @phpstan-ignore-line
}

return $this->entity;
}

public function setEntity(Model $entity): void
{
$this->entity = $entity;
}

public function getCaption(): string
{
return $this->caption ?? ucwords(str_replace('_', ' ', $this->short_name));
Expand Down
116 changes: 86 additions & 30 deletions src/Model/UserActionsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Atk4\Data\Model;

use Atk4\Core\Factory;
use Atk4\Data\Model;

trait UserActionsTrait
{
Expand All @@ -14,22 +13,23 @@ trait UserActionsTrait
*
* @var string|array
*/
public $_default_seed_action = [Model\UserAction::class];
public $_default_seed_action = [UserAction::class];

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

/**
* Register new user action for this model. By default UI will allow users to trigger actions
* from UI.
*
* @param string $name Action name
* @param array|\Closure $defaults
*/
public function addUserAction(string $name, $defaults = []): Model\UserAction
public function addUserAction(string $name, $defaults = []): UserAction
{
$this->assertIsModel();

if ($defaults instanceof \Closure) {
$defaults = ['callback' => $defaults];
}
Expand All @@ -38,54 +38,88 @@ public function addUserAction(string $name, $defaults = []): Model\UserAction
$defaults['caption'] = $this->readableCaption($name);
}

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

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

return $action;
}

/**
* Returns true if user action with a corresponding name exists.
*/
public function hasUserAction(string $name): bool
{
if ($this->isEntity() && $this->getModel()->hasUserAction($name)) {
return true;
}

return $this->_hasInCollection($name, 'userActions');
}

private function addUserActionFromModel(string $name, UserAction $action): void
{
$this->assertIsEntity();
$action->getOwner()->assertIsModel(); // @phpstan-ignore-line

// clone action and store it in entity
$action = clone $action;
$action->unsetOwner();
$this->_addIntoCollection($name, $action, 'userActions');
}

/**
* Returns list of actions for this model. Can filter actions by records they apply to.
* It will also skip system user actions (where system === true).
*
* @param string $appliesTo e.g. Model\UserAction::APPLIES_TO_ALL_RECORDS
* @param string $appliesTo e.g. UserAction::APPLIES_TO_ALL_RECORDS
*
* @return array<string, Model\UserAction>
* @return array<string, UserAction>
*/
public function getUserActions(string $appliesTo = null): array
{
if ($this->isEntity()) {
foreach (array_diff_key($this->getModel()->getUserActions($appliesTo), $this->userActions) as $name => $action) {
$this->addUserActionFromModel($name, $action);
}
}

return array_filter($this->userActions, function ($action) use ($appliesTo) {
return !$action->system && ($appliesTo === null || $action->appliesTo === $appliesTo);
});
}

/**
* Returns true if user action with a corresponding name exists.
*
* @param string $name UserAction name
* Returns one action object of this model. If action not defined, then throws exception.
*/
public function hasUserAction(string $name): bool
public function getUserAction(string $name): UserAction
{
return $this->_hasInCollection($name, 'userActions');
if ($this->isEntity() && !$this->_hasInCollection($name, 'userActions') && $this->getModel()->hasUserAction($name)) {
$this->addUserActionFromModel($name, $this->getModel()->getUserAction($name));
}

return $this->_getFromCollection($name, 'userActions');
}

/**
* Returns one action object of this model. If action not defined, then throws exception.
* Remove specified action.
*
* @param string $name Action name
* @return $this
*/
public function getUserAction(string $name): Model\UserAction
public function removeUserAction(string $name)
{
return $this->_getFromCollection($name, 'userActions');
$this->assertIsModel();

$this->_removeFromCollection($name, 'userActions');

return $this;
}

/**
* Execute specified action with specified arguments.
*
* @param string $name UserAction name
* @param mixed ...$args
* @param mixed ...$args
*
* @return mixed
*/
Expand All @@ -94,17 +128,39 @@ public function executeUserAction(string $name, ...$args)
return $this->getUserAction($name)->execute(...$args);
}

/**
* Remove specified action(s).
*
* @return $this
*/
public function removeUserAction(string $name)
protected function initUserActions(): void
{
foreach ((array) $name as $action) {
$this->_removeFromCollection($action, 'userActions');
}

return $this;
// Declare our basic Crud actions for the model.
$this->addUserAction('add', [
'fields' => true,
'modifier' => UserAction::MODIFIER_CREATE,
'appliesTo' => UserAction::APPLIES_TO_NO_RECORDS,
'callback' => 'save',
'description' => 'Add ' . $this->getModelCaption(),
]);

$this->addUserAction('edit', [
'fields' => true,
'modifier' => UserAction::MODIFIER_UPDATE,
'appliesTo' => UserAction::APPLIES_TO_SINGLE_RECORD,
'callback' => 'save',
]);

$this->addUserAction('delete', [
'appliesTo' => UserAction::APPLIES_TO_SINGLE_RECORD,
'modifier' => UserAction::MODIFIER_DELETE,
'callback' => function ($model) {
return $model->delete();
},
]);

$this->addUserAction('validate', [
//'appliesTo' => any!
'description' => 'Provided with modified values will validate them but will not save',
'modifier' => UserAction::MODIFIER_READ,
'fields' => true,
'system' => true, // don't show by default
'args' => ['intent' => 'string'],
]);
}
}
Loading