Update field value based on a backend calculation

1. Introduction

The updateValueBackend field logic allows field values to be calculated on the backend.

When the conditions for the dependencies on other fields are met, a request is sent to the backend. The value resulting from that backend call is going to be set on the field.

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.

At the moment this does not work for relate fields

1.1 Example scenario

As an example this guide is going to use the following scenario.

On the Cases module we want to set the priority field based on the values from the type and the subject (name) fields.

The rules for setting the priority are the following:

  1. High Priority - Set priority to High Priority when:

    • When the type field has value User

    • and the name field contains Error. (the Subject is the same as name)

    • set the priority to High

  2. Medium Priority - Set priority to Medium Priority when:

    • When the type field has value User

    • and the name field contains Warning. (the Subject is the same as name)

    • set the priority to Medium

  3. Low Priority - Set priority to Low Priority when:

    1. When the type field has value User

    2. and the name field does not contain Warning nor Error. (the Subject is the same as name)

    3. set the priority to Low

The following gif shows an example of what we want:

Backend Value Update Example

2. Logic Metadata definition

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

The first thing to define is the logic entry in the metadata. This is where we are going to define the triggers for our backend calculation.

The configuration for the logic can be added to the vardefs.php or the detailviewdefs.php.

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

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

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

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

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

  2. Replace the priority entry on the custom detailviewdefs.php with the code on the snippet on 2.2 Logic definition section

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

  4. Run Repair and Rebuild on Admin menu

  5. (optional) If you have some kind of php cache like opcache or APCu, you will need to re-start apache.

2.2 Logic definition

The following were the changes applied in order to have the logic triggering when we want.

From:

    array(
      0 =>
      array(
        'name' => 'case_number',
        'label' => 'LBL_CASE_NUMBER',
      ),
      1 => 'priority',
    ),

To:

        array(
          0 =>
          array(
            'name' => 'case_number',
            'label' => 'LBL_CASE_NUMBER',
          ),
          1 =>
          [
              'name' => 'priority',
              'logic' => [
                  'case-calculate-priority' => [
                      'key' => 'updateValueBackend',
                      'modes' => ['edit', 'create'],
                      'params' => [
                          'fieldDependencies' => [
                              'type',
                              'name',
                          ],
                          'process' => 'case-calculate-priority',
                          'activeOnFields' => [
                              'type' => ['User'],
                              'name' => [
                                  ['operator' => 'not-empty' ]
                              ]
                          ]
                      ]
                  ],
              ]
          ],
        ),

2.2.1 Logic Properties description

  • Key

    1. The key within the named logic array is stating which logic type will be used. To use backend logic calculations we need to set updateValueBackend.

  • Modes

    • Modes are view modes we want our updateValueBackend logic to take effect on, in this example we just want on edit and create. Another example of a mode that could be selected could be list or detail for example.

Params
Field Dependencies

fieldDependencies is where we declare the field(s) that we want our logic to depend on.

In this example we depend on type and name as they are the two fields that the scenario rules depend on

...
'fieldDependencies' => [
    'type',
    'name',
]

...
Active on Fields

activeOnFields is where you declare the field/value conditions that trigger the logic to run. In this case call to the backend to calculate the value.

As stated on the scenario conditions we want:

  1. When the type field has value User

So we are going to set the logic to only trigger when the type field has value User

For the name field we have multiple value dependencies described on the scenario conditions:

  1. the name field contains Error. (the Subject is the same as name)

  2. the name field contains Warning. (the Subject is the same as name)

  3. the name field does not contain Warning nor Error. (the Subject is the same as name)

So we are going to set the logic to just check if the name field is not empty and let the backend do the work of checking the rules in more detail.

With this in mind we’ve set the activeOnFields as:

'activeOnFields' => [
    'type' => ['User'],
    'name' => [
        ['operator' => 'not-empty' ]
    ]
]
Backend requests triggering

Please have in mind that when we have entries for multiple fields within activeOnFields, these conditions work like an AND. Meaning that the logic is only triggered when all the conditions are true.

In our example the logic is only going to be triggered when the type is User AND the name is not empty.

Otherwise, nothing will happen, i.e. no request is going to be made to the backend.

The following gif shows the requests that are done and when.

Backend Value Update backend requestsExample

3. 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 logic metadata we need to work on the backend code that is going to handle the requests done to calculate the value.

The updateValueBackend logic 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.

3.1 Steps to add a new process handler to extensions

  1. Create the folder extensions/defaultExt/modules/Cases/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 CaseCalculatePriority.php, i.e. extensions/defaultExt/modules/Cases/Service/Fields/CaseCalculatePriority.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\Cases\Service\Fields;

  3. On CaseCalculatePriority.php add the code on the snippet on 3.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.

3.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 case-calculate-priority

  • 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\Cases\Service\Fields;

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

class CaseCalculatePriority implements ProcessHandlerInterface
{
    protected const MSG_OPTIONS_NOT_FOUND = 'Process options are not defined';
    protected const MSG_INVALID_TYPE = 'Invalid type';
    public const PROCESS_TYPE = 'case-calculate-priority';

    /**
     * CaseCalculatePriority 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
    {

        $options = $process->getOptions();
        $type = $options['record']['attributes']['type'] ?? '';
        if (empty($type)) {
            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
        }

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

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

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

        $value = 'P3';
        if (strpos(strtolower($name), 'warning') !== false) {
            $value = 'P2';
        }

        if (strpos(strtolower($name), 'error') !== false) {
            $value = 'P1';
        }

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

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

3.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: case-calculate-priority

requiredAuthRole()

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

getRequiredACLs()

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

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

For already existing cases 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 Case type is not set. If that happens we should throw an exception:

        $type = $options['record']['attributes']['type'] ?? '';
        if (empty($type)) {
            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 updateValueBackend logic, the response always needs to contain value entry like the following:

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

        ...

        $process->setData($responseData);

3.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 updateValueBackend 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 the priority according to the name

The rules in our example define that the priority is going to change depending on the value of the name field. For that we get the value of the name field from the record, then according to its contents we calculate the priority to set.

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

$value = 'P3';
if (strpos(strtolower($name), 'warning') !== false) {
    $value = 'P2';
}

if (strpos(strtolower($name), 'error') !== false) {
    $value = 'P1';
}

Set the priority value

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

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

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

3.3 More Info on ProcessHandlers

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

3.4 More examples

For more information on different field logic see here.

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