Add backend calculation field action

1. Introduction

Field actions allow you to add custom action buttons next to fields in SuiteCRM.

The updateValueBackend field action allows set field values based on a backend calculation.

This allows for more complex calculations. Like calculations where you need values from multiple modules or from external sources.

This guide describes the steps to setup a backend calculation for a field action.

At the moment this does not work for relate fields

Backend Calculation Field Action Functionality Example

2.Logic Metadata definition

When making these changes be sure to make them in the custom directory, ie: 'public/legacy/custom/modules/<module>/…​'

The first thing to define is the fieldActions entry in the metadata. This is where the action buttons for fields are defined.

The configuration for the actions can be added only to detailviewdefs.php.

In the following example we are going to add them to the Accounts module detailviewdefs.php.

2.1 Steps to add the actions on the custom detailviewdefs.php

  1. Copy the core Accounts detailviewdefs.php to the custom folder:

    1. From: public/legacy/modules/Accounts/metadata/detailviewdefs.php

    2. To: public/legacy/custom/modules/Accounts/metadata/detailviewdefs.php

  2. Add a new entry for your custom action button on the custom detailviewdefs.php with the code on the snippet below

    • Re-set permissions if needed (will depend on your setup)

  3. Run Repair and Rebuild on Admin menu

2.2 Action definition

The following were the changes applied in order to have the field action displaying.

    'name' => 'website',
    'type' => 'link',
    'label' => 'LBL_WEBSITE',
    'displayParams' => [
        'link_target' => '_blank',
    ],
    'fieldActions' => [
        'klass' => '',
        'containerKlass' => 'd-flex align-items-center',
        'position' => 'inline',
        'actions' => [
            'calculate' => [
                'key' => 'calculateValueBackend',
                'labelKey' => 'LBL_UPDATE',
                'modes' => ['edit'],
                'klass' => [' btn btn-sm btn-secondary p-1 m-0 ml-1'],
                'params' => [
                    'expanded' => true,
                    'process' => 'calculate-website-value',
                ]
            ],
        ]
    ],

3. Properties Description

  • Key

    • The key within the named actions array is stating which action type will be used for the following. In this case it’s calculateValueBackend.

  • Modes

    • Modes are views you would like your required action to take effect on, as shown above it will be edit.

  • Icon

    • If desired to have a icon button instead of a labelled-key button, this option can be used.

  • Klass

    • 'klass' can be customized to style the action button. The 'klass' where the out of action object is to customize buttons as a whole.

  • Param

    • 'params' can configure the action button.

  • Process

    • 'process' is the place where your action will fire a backend request. Simply, write the key of the process handler that is supposed to run.

  • Container Klass

    • 'containerKlass' can be customized to style for all action buttons.

  • Position

    • 'position' can customize the position of the action button in respect to it respective field. (e.g. position: inline or vertical)

4. Backend Handler

When making these changes be sure to make them within an extension on the 'extensions' directory, e.g.: 'extensions/<my-extension>/…​'

After defining the action metadata we need to work on the backend code that is going to handle the requests done to calculate the value.

The calculateValueBackend action uses the Process api. The requests done to the Process api are handler by php classes implementing the ProcessHandlerInterface

In the following example we are going to use the existing extensions/defaultExt to add our custom code.

4.1 Steps to add a new process handler to extensions

  1. Create the folder extensions/defaultExt/modules/Accounts/Service/Fields

    1. This is a best practice not a hard requirement

    2. As long as you add under the extensions/<your-ext>/backend or extensions/<your-ext>/modules it should work.

  2. Within that folder create the CalculateWebsiteValue.php, i.e. extensions/defaultExt/modules/Accounts/Service/Fields/CalculateWebsiteValue.php

    1. If you are not using the recommended path, make sure that the namespace follows the one you are using

    2. On our example the namespace is namespace App\Extension\defaultExt\modules\Accounts\Service\Fields;

  3. On CalculateWebsiteValue.php add the code on the snippet on 4.2 Process handler implementation section

  4. Re-set permissions (may not be needed, this will depend on your configuration)

  5. Run php bin/console cache:clear or delete the contents of the cache folder under the root of the project (optional) If you have some kind of php cache like opcache or APCu, you will need to re-start apache.

4.2 Process handler implementation

A class is recognized as a ProcessHandler if it implements the ProcessHandlerInterface.

Furthermore, for it to be matched with request made by the logic metadata we’ve defined, it needs the following:

  • Set the ProcessType to be the same as the value that was defined on the metadata, in this example it is calculate-website-value

  • On the response data include a value entry that is the value that is going to be used to update the field value on the frontend

The following snippet contains a sample implementation of the process handler for our scenario:

<?php

namespace App\Extension\defaultExt\modules\Accounts\Service\Fields;


use ApiPlatform\Exception\InvalidArgumentException;
use App\Process\Entity\Process;
use App\Process\Service\ProcessHandlerInterface;

class CalculateWebsiteValue implements ProcessHandlerInterface
{
    protected const MSG_OPTIONS_NOT_FOUND = 'Process options are not defined';
    protected const PROCESS_TYPE = 'calculate-website-value';

    /**
     * CalculateWebsiteValue constructor.
     */
    public function __construct()
    {
    }

    /**
     * @inheritDoc
     */
    public function getProcessType(): string
    {
        return self::PROCESS_TYPE;
    }

    /**
     * @inheritDoc
     */
    public function requiredAuthRole(): string
    {
        return 'ROLE_USER';
    }

    /**
     * @inheritDoc
     */
    public function getRequiredACLs(Process $process): array
    {
        $options = $process->getOptions();
        $module = $options['module'] ?? '';
        $id = $options['id'] ?? '';

        $editACLCheck = [
            'action' => 'edit',
        ];

        if ($id !== '') {
            $editACLCheck['record'] = $id;
        }

        return [
            $module => [
                $editACLCheck
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    public function configure(Process $process): void
    {
        //This process is synchronous
        //We aren't going to store a record on db
        //thus we will use process type as the id
        $process->setId(self::PROCESS_TYPE);
        $process->setAsync(false);
    }

    /**
     * @inheritDoc
     *
     */
    public function validate(Process $process): void
    {
        if (empty($process->getOptions())) {
            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
        }

        $options = $process->getOptions();
        [
            'record' => $record,
        ] = $options;

        if (empty($record)) {
            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
        }
    }

    /**
     * @inheritDoc
     */
    public function run(Process $process)
    {
        $options = $process->getOptions();

        $type = $options['record']['attributes']['type'] ?? '';
        $name = $options['record']['attributes']['name'] ?? '';

        $value = 'https://suitecrm.com';

        $responseData = ["value" => $value];

        $process->setStatus('success');
        $process->setMessages([]);
        $process->setData($responseData);
    }

}

4.2.1 Process handler interface methods

getProcessType()

In this we need to return the id of our process, the same that is defined on the metadata logic key entry. In our example: calculate-website-value

requiredAuthRole()

Our process should only be accessed by logged-in users, thus we return ROLE_USER;

getRequiredACLs()

For new accounts, we only want users with edit access to the Accounts module to be able to call our ProcessHandler. Thus, we defined:

$editACLCheck = [
    'action' => 'edit',
];

For already existing accounts we need an extra check to make sure that the users has access to that specific record. Therefore, we conditionally add a check for the record id:

if ($id !== '') {
    $editACLCheck['record'] = $id;
}

validate()

The ProcessHandler won’t be able to do any calculations if the record doesn’t exist. If that happens we should throw an exception:

$options = $process->getOptions();
[
    'record' => $record,
] = $options;

if (empty($record)) {
    throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
}

And since our business logic states that this should only run if the type is User we’ve added another check:

if ($type !== 'User') {
    throw new InvalidArgumentException(self::MSG_INVALID_TYPE);
}

run()

This is the method that actually does what the process is supposed to do and returns the appropriate response.

Please have in mind that for the calculateValueBackend action, the response always needs to contain value entry like the following:

$responseData = [
    'value' => $value
];

...

$process->setData($responseData);

4.2.2 Process handler implementation description

Let’s take an in depth look at the implementation of our logic, located in the run() method.

Get the input record

One of the inputs we need for our logic to work is the data in the record.

To get the data sent in the request you can call the getOptions method of the process

$options = $process->getOptions();

The calculateValueBackend logic, besides other data, sends the current data on the record. It sends a record entry that follows the standard format for records, the same one that is used on the api to get a record. The field values of the record are located within the attributes entry:

$options = $process->getOptions();
$record = $options['record'];
$attributes = $record['attributes'];

To get a field on the record we could do (in this example we are getting the 'type'):

$options = $process->getOptions();
$record = $options['record'];
$attributes = $record['attributes'];
$type = $attributes['type'];

*Calculate value

$options = $process->getOptions();

$type = $options['record']['attributes']['type'] ?? '';
$name = $options['record']['attributes']['name'] ?? '';

$value = 'https://suitecrm.com';

Set the value

Finally, for all of this to work we set the value that we want to use for the our calculation on the response data.

$responseData = [
    'value' => $value
];

$process->setStatus('success');
$process->setMessages([]);
$process->setData($responseData);

4.3 More Info on ProcessHandlers

For more information how to create a process handler see the Adding a Process Handler guide.

Content is available under GNU Free Documentation License 1.3 or later unless otherwise noted.