How do I create and configure a save handler to make it work?

I am a freelance Linux technical service provider, and I am currently reconsidering my CRM system choices. As a regular individual user and technical professional, I know I need to customize my CRM settings. I have hidden most of the core modules, keeping only a few tables that seem potentially useful, and have started making custom modifications to some table constraints and fields. Now, I need to define a field and use a program to automatically generate a unique identifier for the contact and account tables to identify customers. I believe this is a common and reasonable requirement. Therefore, with the help of AI (as a technical professional, I admit that the help provided by AI is almost useless, but due to its powerful retrieval capabilities, it can often complete some simple functions, which can then be better implemented with human intervention. Unfortunately, powerful software like SuiteCRM lacks clear documentation; it doesn’t even have complete and clear interface definitions),

I wrote the following code:

<?php
namespace App\Extension\defaultExt\backend\handler;

use App\Data\Entity\Record;
use App\FieldDefinitions\Entity\FieldDefinition;
use App\Data\Service\Record\RecordSaveHandlers\RecordSaveHandlerInterface;

class UniqueCodeRecordSaveHandler implements RecordSaveHandlerInterface {
        public function run(?Record $previousVersion, Record $inputRecord, ?Record $savedRecord, FieldDefinition $fieldDefinition): void {
                throw new \Exception("Bingo! 处理器确实被触发了");
                $currentCode = $inputRecord->getAttribute('unique_code_c');
                $GLOBALS['log']->info("into");
                if (!empty($currentCode)) {
                        return;
                }

                $n8n_url = 'http://192.168.4.20/webhook/StreamCounter?type=SuiteCRM-CID';
                $ch = curl_init($n8n_url);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_TIMEOUT, 3);
                $response = curl_exec($ch);
                $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);

                if ($http_code === 200 && is_numeric(trim($response))) {
                        $counter = (int)trim($response);
                        $final_code = date('YmdHis') . rand(1000, 9999) . str_pad($counter, 4, '0', STR_PAD_LEFT);
                        $inputRecord->setAttribute('unique_code_c', $final_code);
                }

        }

        public function getOrder(): int {
                return 10;
        }

        public function getKey(): string {
                return 'unique_code_generator_handler';
        }

        public function getModule(): string {
                return 'contacts';
        }

        public function getModes(): array {
                return ['create', 'edit'];
        }
}
?>

Typically, I would expect him to at least be able to access my n8n webhook counter interface and create a unique identification code based on a 4-bit year-reset counter value combined with a random code and timestamp. However, he was completely unable to execute this successfully, and SuiteCRM detected a problem with the saving process, as shown in the image.

Are there any official developers or community members who can help me, a new user, better understand and use SuiteCRM?

I assume the throw \Exception wasn’t there when this was run?

Have you actually created the unique_code_c field?

Also you are likely to need to extend the LegacyHandler for this. I would however suggest reading up on save handlers here:

They are probably what you are looking for.

Regards

Mark

For reference I used a load type handler:

<?php

namespace App\Extension\defaultExt\modules\Contacts\Data\Service\Record\Field;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use App\FieldDefinitions\Entity\FieldDefinition;
use App\Engine\LegacyHandler\LegacyHandler;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use App\Data\Entity\Record;
use App\Data\Service\Record\EntityRecordMappers\EntityRecordFieldMapperInterface;
use App\Data\Service\Record\Mappers\BaseFieldMapperInterface;

class ContactsCalculateJobTitle extends LegacyHandler implements EntityRecordFieldMapperInterface, LoggerAwareInterface
{
    protected const MSG_OPTIONS_NOT_FOUND = 'Process options are not defined';
    protected const MSG_INVALID_TYPE = 'Invalid type';
    public const PROCESS_TYPE = 'contacts-calculate-job-title';
    private $logger;

    public function replaceDefaultTypeMapper(): bool
    {
        return false;
    }

    public function getField(): string
    {
        return 'title';
    }

    public function getKey(): string
    {
        return self::PROCESS_TYPE;
    }

    public function getHandlerKey(): string
    {
        return self::PROCESS_TYPE;
        // TODO: Implement getHandlerKey() method.
    }

    public function getOrder(): int
    {
        return 0;
    }

    public function getModes(): array
    {
        return ['retrieve', 'list'];
    }

    public function getModule(): string
    {
        return 'contacts';
    }


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

    private function getRoleTitle($id)
    {
        $this->init();
        $contact = \BeanFactory::getBean('Contacts', $id);

        $roles = $contact->get_linked_beans(
            'ismnw_jobroles_contacts',
            'ISMNW_JobRoles',
            'ismnw_jobroles.name ASC',
            0,
            1,
            0,
            'ismnw_jobroles.primaryrole=1'
        );
        $this->logger->error('Found Roles', ['rolecount' => count($roles)]);
        $title = '';
        if (count($roles) > 0 && trim($roles[0]->name) !== '') {
            $title = $roles[0]->name;
        }
        $this->close();
        return $title;
    }

    /**
     * @inheritDoc
     */

    public function toExternal(Record $record, FieldDefinition $fieldDefinitions): void
    {
        $attrs = $record->getAttributes();

        $title = $this->getRoleTitle($attrs['id']);

        if ($title !== '') {
            $attrs['title'] = $title;
            $record->setAttributes($attrs);
        }

        return;
    }

    public function toInternal(Record $record, FieldDefinition $fieldDefinitions): void
    {
        return;
    }

    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
}

obviously not a save but it works in a similar way.

Regards

Mark