1

Recently I was tasked with a project to dynamically call a class from a packaged trigger. This will also clients to hook into their custom logic instead of what is supplied as part of the package.

I started old school way by creating interface and abstract methods and it got ugly pretty quickly. It would not scale and hence looked further into what native options are available within the platform. To my surprise, there was actually one - Callable Interface

Documentation: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_Callable.htm

Here is a snippet of fully functional and dynamic execution of Apex classes, methods, and arguments. Other advantages of using this pattern

  • Control order of execution
  • Toggle what features/functionality to run

Step 1: Create a metadata, let's say Trigger Settings (API name: TriggerSettings__mdt)

enter image description here

Step 2: Create a Utils class to pull the metadata records based on sObject and Active flag.

public with sharing class Utils {

public static Map<String, List<TriggerSettings__mdt>> getTriggerSettings(String sObj) {

if(String.isEmpty(sObj))
    return null;

Map<String, List<TriggerSettings__mdt>> mapTriggerSettings = new Map<String, List<TriggerSettings__mdt>>();

for(TriggerSettings__mdt setting : [SELECT ClassName__c, MethodName__c, Event__c 
                                    FROM TriggerSettings__mdt 
                                    WHERE IsActive__c=true AND SObjectType__c=:sObj 
                                    ORDER BY Order__c]) {

    if(mapTriggerSettings.containsKey(setting.Event__c))
        mapTriggerSettings.get(setting.Event__c).add(setting);
    else
        mapTriggerSettings.put(setting.Event__c, new List<TriggerSettings__mdt>{setting});
}

return mapTriggerSettings;

 }
  public class ExtensionMalformedException extends Exception {}

}

Step 3: Create Trigger Helper class that implements Callable interface. This class will map the method name in the metadata and dispatch actual method(s). If you are creating a package, this class will be part of the package. But clients can provide their own implementation by creating new class that will implement Callable Interface that will dispatch custom methods/logic. Make sure to update the TriggerSettings metadata with correct class name and method name if the logic needs to be over-written

public class CaseTriggerHelper implements Callable {

public static void method1(List<Case> caseList) {
    //Do Something
}

public static void method2(List<Case> caseList, Map<Id, Case> newMap, Map<Id, Case> oldMap) {
    //Do Something
}

public static void method3(List<Case> caseList, Map<Id, Case> oldMap) {
    //Do Something
}

public static void method4(List<Case> caseList) {
    //Do Something
}

public static Object call(String action, Map<String, Object> args) {

    //To get event type, use => args.get('eventType') 

    switch on action {

        when 'method1' {
            method1(
                Trigger.new
            );
        }
        when 'method2' {
            method2(
                Trigger.new,
                (Map<Id, Case>)Trigger.newMap,
                (Map<Id, Case>)Trigger.oldMap
            );
        }
        when 'method3' { 
            method3(
                Trigger.new,
                (Map<Id, Case>)Trigger.oldMap
            );
        } 
        when 'method4' {
            method4(
                Trigger.old
            );
        }
        when else {
            throw new Utils.ExtensionMalformedException(action + ' not implemented');
        }
    }

    return action + ' dispatched successfully. Event: ' + args.get('eventType');
}
}

Step 4: From here on it is business as usual. Create TriggerHandler and the sObject Trigger

public class CaseTriggerHandler {

Map<String, List<TriggerSettings__mdt>> triggerSettings;

public CaseTriggerHandler() {
    triggerSettings = Utils.getTriggerSettings('Case');
}

public static Boolean caseRecursiveCheck = false;

public void handleEvent(String eventType) {

    if(caseRecursiveCheck) {
        return;
    }

    if(triggerSettings == null || !triggerSettings.containsKey(eventType)) {
        return;
    }

    for(TriggerSettings__mdt setting : triggerSettings.get(eventType)) {

        String className = setting.ClassName__c;
        String methodName = setting.MethodName__c;

        Callable extension = (Callable) Type.forName(className).newInstance();
        Object result = extension.call(methodName, new Map<String, Object> {
            'eventType' => eventType
        });

        system.debug('callable result ' + result);
    }
}
}

Step 5: Create sObject Trigger

trigger CaseTrigger on Case (before insert, before update, before delete, after insert, after update, after delete) {

CaseTriggerHandler handler = new CaseTriggerHandler();

if(Trigger.isInsert) {
    if(Trigger.isBefore) 
        handler.handleEvent('BeforeInsert');
    else if(Trigger.isAfter)
        handler.handleEvent('AfterInsert');
} 

else if(Trigger.isUpdate) {
    if(Trigger.isBefore)
        handler.handleEvent('BeforeUpdate');
    else if(Trigger.isAfter)
        handler.handleEvent('AfterUpdate');
}

else if(Trigger.isDelete) {
    if(Trigger.isBefore)
        handler.handleEvent('BeforeDelete');
    else if(Trigger.isAfter)
        handler.handleEvent('AfterDelete');
}
}

Finally the magic.

Create a new case enter image description here

Update a case enter image description here

Delete a case enter image description here

Hope this helps someone looking to build dynamic apex execution. Open to ideas on how it can be further improved.

Thanks to below StackExchange articles

5
  • 3
    Hi Mitesh, can I suggest you split this into a question and an answer because right now you have an answer without a question as a question! – Phil W Mar 12 '20 at 21:04
  • You can read more about what we expect in Can I answer my own question?. Without this, you can probably expect your question to be deleted, as it is "not a question." – sfdcfox Mar 12 '20 at 22:56
  • This pattern could be done without Salesforce providing the Callable interface - that they provide it is only to give them a way to surface this concept and to allow a single interface to be available for exposing such code across package boundaries rather than having separate versions exposed by different packages. Personally, I feel they would be better off providing true reflection capabilities, like you have in Java, rather than push the effort down onto developers to tunnel calls through this (to me) ugly API. – Phil W Mar 13 '20 at 8:15
  • Thanks for the suggestion. Create new Q/A. Basically did ctrl-c and ctrl-v. Would deleting this (original) post, delete the references to the images? salesforce.stackexchange.com/questions/298156/… – Mitesh Sura Mar 16 '20 at 19:55
  • @PhilW , Thanks for calling that out. Reflection by textbook definition is to call class, method and parameters dynamically. Which the interface does. It would be nice to have it like Java, but this is not that bad. We can continue the discussion in the new thread. – Mitesh Sura Mar 16 '20 at 20:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.