Apex Trigger Framework Based on Custom Metadata

If you are using best practice, you should have up to 1 trigger per object and most of the code itself is not inside the trigger. The trigger only called apex classes that hold the logic.
However, even when using those guidelines sometimes apex triggers can have too much logic and many processes which increase the regression when adding changes in 1 of the processes.

Apex Trigger Framework can assist on this area.

The following framework provide some useful benefits:
-minimize the code inside trigger
-separation between processes
-easy debugging and error reporting
-stop/start execution from setup

It contains 3 simple components:
1.Apex class - IRunTrigger
Interface class that every process that should be running from trigger must implement.


public interface IRunTrigger {

    TriggerHandler.TriggerRunResult  run();
    
}
2.Apex class - TriggerHandler
Each trigger call the main function in this handler in order to execute the processes.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public with sharing class TriggerHandler {
    
    private static Set<String> bypassProcess = new Set<String>();
    
    private static String userName;
    private static String userProfileName;
    
    static {
        String profileId = userInfo.getProfileId();
        userProfileName = [select Id,Name from Profile where Id = :profileId limit 1].Name;
        
        userName = UserInfo.getUserName();
    }
   
    public static void runTrigger (String objectType) {
   
       String triggerType = trigger.isBefore ? 'Before ' : 'After '; 
       triggerType += trigger.isInsert ? 'Insert' : (trigger.isUpdate ? 'Update' : (trigger.isDelete ? 'Delete' : ''));
       
       System.debug('Start trigger for ' + objectType + '. Context: ' + triggerType);
       
       list<Trigger_Execution__mdt> l_mdTrigger = [select Type__c, Execution_Order__c, Class_Runner__c, Half_for_Users__c, Halt_for_Profiles__c
                                                    from Trigger_Execution__mdt 
                                                    where Object_Type__c = :objectType and Halt_Process__c = false and Type__c = :triggerType
                                                    order by Execution_Order__c];
                                                    
        list<TriggerRunResult> l_results = new list<TriggerRunResult>();
                                  
        for(Trigger_Execution__mdt mdRecord : l_mdTrigger){
            try{
                if(! isSkipHandler(mdRecord.Class_Runner__c, mdRecord.Halt_for_Profiles__c, mdRecord.Half_for_Users__c)) {
                    Type typeClass = Type.forName(mdRecord.Class_Runner__c);
                    IRunTrigger runTrigger = (IRunTrigger) typeClass.newInstance();
                    TriggerRunResult res = runTrigger.run();
                    l_results.add(res);
                }
            }
            catch(Exception ex){
                //Add general error to the error list 'Unhandle exception in class 'class name'
                System.debug('Exception during execution: ' + ex.getMessage());
            }
        }
        
        //flush all errors to DB
        for(TriggerRunResult runResult : l_results){
            if(runResult.status != 'success'){
                //report error to log list
            }
        }
    }
   
   
    public static boolean isSkipHandler(
        String handlerName,
        String excludeProfiles,
        String excludeUsers){
    
        return bypassProcess.contains(handlerName)
            || (! String.isBlank(excludeUsers) && excludeUsers.contains(userName))
            || (! String.isBlank(excludeProfiles) && excludeProfiles.contains(userProfileName));
    
    }
   
    
    
    public static void addBypass(String handlerName) {
        bypassProcess.add(handlerName);
    }
    public static void clearBypass(String handlerName) {
        bypassProcess.remove(handlerName);
    }
    
    private String getHandlerName() {
        return String.valueOf(this).substring(0,String.valueOf(this).indexOf(':'));
    }
    private String getObjectType(){
        return trigger.isDelete ? trigger.old[0].getsObjectType().getDescribe().getName() : 
                        trigger.new[0].getsObjectType().getDescribe().getName();
    }

    
    
    public class TriggerRunResult {
    
        public String status;
        public String message;

    }
}
3.Custom Metadata - Trigger Execution
Each Custom MD record represent process that should be run from trigger in specific context.




Assume that you want to implement trigger on Contact object that not allow to save if some fields are empty (this can be done also with validation rule, but I add it in code for the demo).

Then 2-3 things need to be done:
1.If there is already trigger on Contact object - skip this step, if not implement the following trigger:


trigger ContactTrigger on Contact (before insert, before update, before delete,
                                    after insert, after update, after delete) {

    TriggerHandler.runTrigger ('Contact');

}

2.Write apex class that implement the interface IRunTrigger and add your logic


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public with sharing class SetContactDefaultValues implements IRunTrigger {


    public TriggerHandler.TriggerRunResult run(){
    
        TriggerHandler.TriggerRunResult result = new TriggerHandler.TriggerRunResult();
        
        try{
            for(Contact contactRecord : (list<Contact>) trigger.new){
        
                if(contactRecord.Email == null){
                    contactRecord.addError('Please setup the contact email.');
                }
            }
            
            result.status = 'ok';
            result.message = 'No Errors';
        }
        catch(Exception ex){
            result.status = 'failed';
            result.message = ex.getMessage();
        }
        
        return result;
    
    }
}

3.Last thing - new custom metadata records that instruct the handler when this process should run
If the process need to run in several contexts - for example: before update and before insert - add 2 metadata records.

And that's all.
The great benefits is that your trigger code is minimal and you don't need any changes it in the trigger in future when adding/changing processes.

In addition there are few more option from the custom metadata to halt specific process.
-Halt Process checkbox to stop process completely (can also delete the metadata record in such case).
-Halt for Profiles field - stop the process for specific profiles (can add several profiles and use any separator).
-Halt for Users - to stop the process for specific users.

Last additional useful feature is to skip specific processes by adding then to the bypassprocess list in the trigger handler.
This can done using the command:
TriggerHandler.addBypass(processToSkip);










No comments:

Post a Comment

Retire of Permission on Profiles

If you are working as a Salesforce admin/developer you've probably heard somewhere that Salesforce is planning to make a significant cha...