Skip to content

ADP1903

Romans Malinovskis edited this page Mar 21, 2019 · 2 revisions

ADP1903 - Actions

ATK Data Proposal 1903 - Actions aims to create a universal way how users can extend Models with features without inheriting or complicating models themselves. The extensions provide additional functionality into the model by creating new executable actions.

I'll start with some examples to help you illustrate, how Actions are used.

1. Send "Invoice" or "Statement" to your client

Assuming that you have model for Client and also model for Invoice, you may want to add ability to send certain reports to your client. To implement that you need two steps in the backend:

  1. Prepare Invoice PDF or Client Statement PDF
  2. Email attachment (or link) to generated PDF

I'm also going to assume that the PDF preparation is already implemented. So the only thing is needed is to send email with attachment.

My proposal is that a Mail Gateway addon, that takes care of creating email message and delivering would also offer a capability of integrating with your existing models 'Invoice' and 'Client'. Consider this code:

// in Invoice
class Invoice extends Model {
  
  // Trait can be used to add new methods to my model
  use MailGateway\MessageSender;

  function init() {
   
    // Using a trait here allows me to register Actions through an add-on.
    $this->addSendMessageAction(
      'email_invoice_PDF',
      function() {
        $m = new MailGateway\Message();
        $m->to($this->ref('client_id')['email']);
        $m->setTemplate('...')
        $m->addAttachment($this->generateInvoicePDF());
        return $m;
      }
    );   
  }
}

In this example, the code is very high level. The result would be that any Card / MasterCRUD in the system in addition to "Edit" and "Delete" operations will now have "Email Invoice PDF" action:

image-20190318203758558

Clicking the button would display a preview of the email before sending, but that's up to "MailGateway" addon, really.

The benifit here is that Add-on has no knowledge of "Invoice" model and even no knowledge of UI but could still globally register an action, which the rest of ATK core and add-ons would respect.

If the add-on does not wish to integrate with ATK Actions, (third party add-on), it can still be used with a bit of extra boiler-plate code:

// in Client
class Client extends Model {
  
  // no traits.. for third-party integration

  function init() {
   
    // use default addAction.
    $this->addAction(
      'email_invoice_PDF',
      function() {
        $m = new MailGateway\Message();
        $m->to($this['email']);
        $m->setTemplate('...')
        $m->addAttachment($this->generateInvoicePDF());
        
        $m->send();
      },
      [],  // no arguments - optional
      "Are you sure?" // confirmation
    );   
  }
}

The effect is similar, but you define action yourself. The body of the action will be executed when button is pressed.

2. Specify User model

When you wish to add "Authentication" in ATK, you currently add Authenication controller into your App and specify 'User' model. When doing so, you have to specify username/password combination.

However, Login add-on can make use of "Actions" to do many interesting things.

Action for changing password. The idea here is that Authentication controller can extend your "User" model with a new action when you execute:

$auth->setModel(new User());

This can register new action for the user model, such as ability to change password. It can also be linked with the UI:

$page->add(new Button('Change Password'))->action($auth->model, 'change_password');

The button's method "action" would rely on new ActionExecutor component, which would display a modal asking for the required "old" and "new" passwords. Once supplied, the action method would be executed. How is the action registered? Quite simple:

$user->addAction(
  'change_password',
  function ($old, $new1, $new2) {
    if ($new1 != $new2) {
      throw new ValidationException('Passwords do not match', 'new2');
    }
    ...
  },
  [
    'old'=>['type'=>'password'],
    'new1'=>['type'=>'password'],
    'new2'=>['type'=>'password']
  ]
)

Here AcitonExecutor will look at the arguments and since not arguments are provided during the invocation, the user will be prompted to supply the values. The way how arguments are specified here are consistent with the Form::addField() so any type or field decorator would be acceptable. Form will also normalize data and pass it to the method as arguments.

3. ACL and extensions

Current implementation of ACL works like this. After model is initialized and init() method is executed, ACL can then review the fields and alter them as per certain rules. Those rules can be implementation specific, for example:

  • If $auth->user['is_admin'] is false, then change certain fields to "read-only". (or the other way around)

Similarly ACL can use afterLoad hook for models to change field properties depending on model status:

  • if $this['status']!="draft" then change amount to read-only.

etc. We want to ACL to also be able to affect Actions in the same way, so default action implementation would have some capabilities:

$action = $model->getAction('change_password');

$action->disable(); // action exists, but can't be executed. enable() also exists.
$action->destroy(); // removes action entirely
$action->callback = ..; // substitute or wrap callback
$action->icon = 'book'; // set icon associated with the action
$action->confirm = false; // disable confirmation text. Can also be callback.
$action->caption = 'Юникод'; // caption to override name

$action->executor = new UI\MethodExecutor(); // use a custom method executor UI object/seed

All of those tools can be also used by ACL giving a lot of flexibility of modifying actions. Also - keep in mind that Action class can also be extended (like field) resulting in more parameters and features. ATK UI and add-ons are not guaranteed to implement those extenisons but they should always remain compatible with the standard Action class:

$model->add(new MyCustomAction(...), 'changePassword');

4. In your own code

If you have a method in your own model, that you would like to add to the CRUD, it's very simple now with the actions. First in your model init add:

$this->addAction('ban_user');

Second, for your CRUD you can add:

$crud->addAction('ban_user');

We will also extend the setModel():

$crud->setModel(
  new User($app->db), 
  ['name', 'surname', 'email'],
  ['add', 'edit', 'delete', 'ban_user']
);

This would be consistent with other views too:

$page->add(new Card())->setModel(
  new User($app->db),
  null,
  ['delete', 'ban_user']
);

5. In Saasty

Saasty is user-friendly Web application builder. Fundamentally it relies on capabilities of ATK and add-ons to enhance user experience.

Using Saasty API you can enable add-on and register your actions so that they can be used by user throughout the application builder interface:

$user = $saasty->ref('Models')->loadBy('name', 'User');

$user->ref('Traits')->insert('Login\AuthUserTrait');
$user->ref('Actions')->insert([
  'change_password',
  'changePassword',  // name of the method,
  ['password', 'password', 'password'], 
  ['caption'=>'Change Password']
]);

The API here is fully consistent with the addAction call. The code cannot be supplied directly, but the trait is used.

The effect of this call (typically executed during add-on installation) will make action usable in the user application, but would also be correctly recognized by UI Builder so that it can be associated with UI components manually by the user.

Additionally action can now be used from "Action Builder" interface.

6 integration with UI

$crud=$page->add(['CRUD', 'hideDisabledActions'=>true]);
$crud->setModel(new Client($app->db), ['name','surname', 'status'], ['delete', 'send_email_PDF']);
$crud->addColumn(new DropdownActions(['edit', 'archive', 'blah']);