Help with updateValueBackend

Hi gents

I am currently playing around with backend process handlers. The idea: when typing a ZIP code, a backend handler should query a custom table in the DB and return the city (ie: typing 8000, should result in “Zurich”). I tried to follow the documentation here: Adding a Process Handler :: SuiteCRM Documentation

So I added the following code to the public/legacy/custom/modules/Contacts/metadata/editviewdefs.php:

1 => 
        array (
          0 => 
          array (
            'name' => 'primary_address_postalcode',
            'comment' => 'Postal code for primary address',
            'label' => 'LBL_PRIMARY_ADDRESS_POSTALCODE',
          ),
          1 => 
          array (
            'name' => 'alt_address_postalcode',
            'comment' => 'Postal code for alternate address',
            'label' => 'LBL_ALT_ADDRESS_POSTALCODE',
          ),
        ),
        2 => 
        array (
          0 => 
          array (
            'name' => 'primary_address_city',
            'comment' => 'City for primary address',
            'label' => 'LBL_PRIMARY_ADDRESS_CITY',
            'logic' => [
              'Zip2City' => [
                'key' => 'updateValueBackend',
                'modes' => ['edit', 'create'],
                'params' => [
                  'fieldDependencies' => ['primary_address_postalcode'],
                  'process' => 'Zip2City',
                  'activeOnFields' => [
                    'primary_address_postalcode' => [
                      'operator' => 'not-empty',
                    ],
                  ]
                ]
              ],
            ]
          ),
          1 => 
          array (
            'name' => 'alt_address_city',
            'comment' => 'City for alternate address',
            'label' => 'LBL_ALT_ADDRESS_CITY',
          ),
        ),

Next, I created the handler in config/extensions/defaultExt/modules/Contacts/Service/Fields/Zip2City.php with the following code:

<?php

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

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

class Zip2City implements ProcessHandlerInterface
{
    protected const MSG_OPTIONS_NOT_FOUND = 'Process options are not defined';
    public const PROCESS_TYPE = 'Zip2City';

    /**
     * Zip2City 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);
        }
    }

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

        $zip = $options['record']['attributes']['primary_address_postalcode'] ?? '';

        $value = '';
        // query the DB
        $stmt = $db->prepare('SELECT city FROM zip_to_city WHERE deleted=0 AND zip = ?');
        $stmt->bind_param('s', $zip);
        $stmt->execute();
        $result = $stmt->get_result();

        // Return first record and ignor the rest if there are more than 1 records
        if ($result && $row = $result->fetch_assoc()) {
            $value = $row['city'];
        } else {
            $value = 'No record found';
        }

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

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

Then I did a Quick Repair & Restore and cleared the Symphony cache (./bin/console cache:clear).

Now, when I enter a ZIP code in the contact form (new or edit) actually nothing happens. I also tried to simplify the handler with a simple “Hello world” but nothing works.

I am quite new with SuiteCRM and would appriciate your help/feedback.

My system: SuiteCRM 8.8 with PHP 8.1.31.

Cheers,
Carsten

1 Like

Did it work if you remove database code from the run method and just keep value = ‘hello’?

That’s what I tried when I simplyfied the code. :smiley: no luck

Can you please change this part by having operator in another brackets as shown below.

'activeOnFields' => [
                    'primary_address_postalcode' => [
                          [
                               'operator' => 'not-empty',
                          ],
                    ],
                  ]

I’ve already tried that. But unfortunately without success too.

I read in another thread that a graphql should be sent out when chaning the value. This is not happening. Maybe I am wrong.

This may help if you haven’t already tried.

Please make your changes in detailviewdefs.php file.

1 Like

In the logic item name, and key name, the nomenclature is to use lower case with dashes. I don’t know if that is keeping things from working, but try it. Use something like zip-2-city instead of Zip2City.

I would also check in the browser tools, in an earlier request when you hit “clear cache and reload”, you should see the front-end asking for all the metadata and receiving a ~3MB answer in return. Check if your updateValueBackend logic appears there.

@pgr naming convention is not a problem. The PascalCase ‘Zip2City’ worked. But I followed the valid name ‘zip-2-city’ instead of it.

@CarstenB , Can you please try the following steps which worked as per your requirement of updating city based on zip.
Steps:

  1. Create a file at public/legacy/custom/Extension/modules/Contacts/Ext/Vardefs/_override_zipfield.php with the following
<?php

$dictionary['Contact']['fields']['primary_address_city']['logic']=[
              'Zip2City' => [
                'key' => 'updateValueBackend',
                'modes' => ['edit', 'create'],
                'params' => [
                  'fieldDependencies' => ['primary_address_postalcode'],
                  'process' => 'zip-2-city',
                  'activeOnFields' => [
                    'primary_address_postalcode' => [
                      'operator' => 'not-empty',
                    ],
                  ]
                ]
              ],
            ];

 ?>
  1. Create a file at extensions/defaultExt/modules/Contacts/Process/Service/Fields/Zip2City.php with the code
<?php

namespace App\Extension\defaultExt\modules\Contacts\Process\Service\Fields;

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

class Zip2City implements ProcessHandlerInterface
{
    protected const MSG_OPTIONS_NOT_FOUND = 'Process options are not defined';
    public const PROCESS_TYPE = 'zip-2-city';

    /**
     * Zip2City 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']['primary_address_postalcode'] ?? '';
        if (empty($type)) {
            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
        }
    }

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

        $zip = $options['record']['attributes']['primary_address_postalcode'] ?? '';

        $responseData = [
            'value' => 'postal code' .$zip,
        ];

        $process->setStatus('success');
        $process->setMessages([]);
        $process->setData($responseData);
    }
}
  1. Quick Repair & Rebuild
  2. Run php bin/console cache:clear

@Harshad: you are the best! :partying_face: it works!

So just to clearify for my mind: puting the hander into “…/Process/Service/Fields” instead of “…/Service/Fields” and call it from the “field override” instead of the “editviewdef” is the solution? The ways of SuiteCRM are sometimes unfathomable :rofl:

Now next challange: replicate it to all ZIP fields in contacts, accounts, leads and prospects. For the different modules, I need to create the same logic as above described, I get it. But is there a way to pass the field name as a variable to the handler? Like:

...
'params' => [
      'fieldDependencies' => ['primary_address_postalcode'],
      'process' => 'zip-2-city',
-->      'params' => ['field' => 'name_of_the_field_to_update'], <--??
      'activeOnFields' => [...

Otherwise, I need to create for each field a dedicated handler. This will result into 8 handlers (2 for each module).

Cheers,
Carsten

I think we can place our logic in extensions\defaultExt\backend\Process\Service if it is not module specific. Can you try placing your class file in a folder in this folder and use the PROCESS_TYPE as a key in your vardefs.php or detailviewdefs.php and see it works. Thank you!

The levels and names of sub-directories under DefaultExt are irrelevant. You can check with Symfony console if auto-wiring knows about your class, after that it’s just a matter of getting the full reference (namespace and class name) right.

And yes, the declarations are PHP code running, so you should be able to use variables in there.

Remember also that in some places configurations have a module parameter to say where they apply, but you can use default (if I am not mistaken) to say they apply everywhere. I am going from memory here, can’t look for an example right now, but I am pretty sure this exists.

1 Like

Yes, level or folder structure is irrelevant. They are just created as a better practice of development. What matters is the namespace should be according to the folder hierarchy. Thanks @pgr for bringing this up here.

Thank you @Harshad and @pgr for your support.

Step one is done and works as expected. I copied the logic into the folder " extensions\defaultExt\backend\Process\Service".
At step two (passing the field name as a parameter), I am struggeling. I tried different ways:

$dictionary['Contact']['fields']['primary_address_city']['logic']=[
    'Zip2City01' => [
        'key' => 'updateValueBackend',
        'modes' => ['edit', 'create'],
        'params' => [
            'fieldDependencies' => ['primary_address_postalcode'],
            'process' => 'zip-2-city',
            'srcField' => 'primary_address_postalcode',
            'activeOnFields' => [
                'primary_address_postalcode' => [
                    'operator' => 'not-empty',
                ],
            ]
        ]
    ],
];

also:

$dictionary['Contact']['fields']['primary_address_city']['logic']=[
    'Zip2City01' => [
        'key' => 'updateValueBackend',
        'modes' => ['edit', 'create'],
        'params' => [
            'fieldDependencies' => ['primary_address_postalcode'],
            'process' => 'zip-2-city',
            'activeOnFields' => [
                'primary_address_postalcode' => [
                    'operator' => 'not-empty',
                ],
            ],
            'refParams' => [
                'srcField' => 'primary_address_postalcode',
            ]
        ]
    ],
];

In the module I tryed to get the value with

  • try #1: $srcField = $options[‘srcField’] ?? ‘’;
  • try #2: $srcField = $options[‘refParams’][‘srcField’] ?? ‘’;

But both ways are not working. Also in the grphlq I can’t see the parameters passed to the function. Any idea how the key must be, to pass additional values to the function?

Sorry guys for the dump questions. As mentioned, I am quite new to the whole thing. :bowing_man:

I suggest you just write two or three $dictionary attributions like you want them, for the several fields, and then give that to ChatGPT and ask it to generalize the declarations and build an array with the names of the fields and code to iterate that array and issue the declarations.

hahaha… @pgr I had the same idea. Feedback from ChatGPT was my second try:

'params' => [
    'fieldDependencies' => ['primary_address_postalcode'],
    'process' => 'zip-2-city',
    'activeOnFields' => [
        'primary_address_postalcode' => [
            'operator' => 'not-empty',
        ],
    ],
    'additionalParams' => [
        'country' => 'DE',
        'useFallback' => true,
    ]
]

But maybe I need to clarify my prompt to GPT a bit more. I will give it a second try.

After arguing back and forth with ChatGPT for 2 hours, I give up and fallback to create for each field a dedicated logic. At least, the field names are in all modules the same. :smiley:

If someone has a better idea, feel free to update here your thoughts.

Cheers,
Carsten

Do you want to have this zip city logic common and use across all the modules where the zip field change would update the city. Can you please help understand the new change. If possible can you please raise a new question for the same and for the clarity. I will try this at my end and will share the code there. Thanks!

The idea is to automatically fill in the city field when adding or updating an account, a contact, a lead or a prospect. So far so good. The issue is, there are always two adresses a primary and an alternate. So when typing in the “primary_address_postalcode” the field “primary_address_city” needs to be populated with the value from the DB loockup. And vica-verca. If you typing in the “alt_address_postalcode” the field “alt_address_city” needs to be populated with the value from the DB loockup.

The first issue is: account doesn’t use primary_address_postalcode and primary_address_city. It uses “billing_address_postalcode” and “shipping_address_postalcode”.

So my idea was, instead of creating a logic for every field (primary_, alt_, billing_ and shipping_), just create one and pass the corresponding ZIP/Postalcode field as a parameter. Then dynamically create the options path like:

  • regular: “$zip = $options[‘record’][‘attributes’][‘primary_address_postalcode’] ?? ‘’;”
  • dynamic: " $zip = $options[‘record’][‘attributes’][$FieldNameCommingFromTheParameter] ?? ‘’;"

With this approache, I just need one logic and can call it from different modules for different address-types.

I hope you understand, what I try to achive. I am very close to it. The only challange is: passing the corresponding postalcode field name as a parameter to the logic. The rest is done already and works fine.

Here the codes (extensions\defaultExt\backend\Process\Service\Fields\Zip2CityAlt.php) → The logic for the primary address field, looks the same:

<?php

namespace App\Extension\defaultExt\backend\Process\Service\Fields;

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

class Zip2CityAlt implements ProcessHandlerInterface
{
    protected const MSG_OPTIONS_NOT_FOUND = 'Process options are not defined';
    public const PROCESS_TYPE = 'zip-2-city-alt';

    /**
     * Zip2CityAlt 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']['alt_address_postalcode'] ?? '';
        if (empty($type)) {
            throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
        }
    }

    /**
     * @inheritDoc
     */
    public function run(Process $process)
    {
        global $db;
        $options = $process->getOptions();
        $srcField = 'alt_address_postalcode';
        $zip = $options['record']['attributes'][$srcField] ?? '';
        
        $query = 'SELECT city FROM zip_to_city_c WHERE zip='.$zip.' AND deleted=0';
        $city = $db->getOne($query);

        if(!$city) {
            $city = '';
        }

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

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

And the _override_zipfield.php:

<?php

$dictionary['Contact']['fields']['primary_address_city']['logic']=[
    'Zip2CityPrimary' => [
        'key' => 'updateValueBackend',
        'modes' => ['edit', 'create'],
        'params' => [
            'fieldDependencies' => ['primary_address_postalcode'],
            'process' => 'zip-2-city-primary',
            'activeOnFields' => [
                'primary_address_postalcode' => [
                    'operator' => 'not-empty',
                ],
            ]
        ]
    ],
];

$dictionary['Contact']['fields']['alt_address_city']['logic']=[
    'Zip2CityAlt' => [
        'key' => 'updateValueBackend',
        'modes' => ['edit', 'create'],
        'params' => [
            'fieldDependencies' => ['alt_address_postalcode'],
            'process' => 'zip-2-city-alt',
            'activeOnFields' => [
                'alt_address_postalcode' => [
                    'operator' => 'not-empty',
                ],
            ]
        ]
    ],
];

 ?>

As you can see, I currently spited it into Primary- and Alt- function. But the idea is to have it in one logic.

Something like this?

$fieldList = ['primary_address_city',  'alt_address_city'];

foreach ($fieldList as $field) {
      $dictionary['Contact']['fields'][$field]['logic']=[
(etc)
}