Skip to content
This repository was archived by the owner on Sep 27, 2023. It is now read-only.

Commit 33a6fc1

Browse files
authored
Added a custom setting to control trigger execution & debug statements (#8)
Renamed the protected methods so they fit the verb naming convention - existing classes need to be updated Added a new hierarchy custom setting called TriggerSettings__c * Triggers can be toggled globally using the field TriggerSettings__c.ExecuteTriggers__c * Individual triggers can be disabled using the field TriggerSettings__c.HandlerClassesToSkip__c. The name of each handler class to skip (ex: 'LeadTriggerHandler') should be put on a separate line * Debug statements inside TriggerHandler can be toggled using the field TriggerSettings__c.EnableDebugging__c
1 parent afd6e63 commit 33a6fc1

9 files changed

Lines changed: 129 additions & 40 deletions

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Apex Trigger Framework
2+
<a target="_blank" href="https://githubsfdeploy.herokuapp.com?owner=jongpie&repo=ApexTriggerFramework">
3+
<img alt="Deploy to Salesforce" src="https://raw.githubusercontent.com/afawcett/githubsfdeploy/master/src/main/webapp/resources/img/deploy.png">
4+
</a>
5+
6+
## Features
7+
* Implements Salesforce best practices of 1 trigger per object & logicless triggers
8+
* The abstract class TriggerHandler.cls handles determining the current context and calling 1 of 7 protected methods - triggers only have to call the public execute() method
9+
* Provides recursion detection/prevention by checking the list of trigger records have already been processed
10+
* Allows triggers to be enabled/disabled both globally and individually at the org, profile and user levels (hierarchy custom setting)
11+
* Allows framework debug statements to be enabled/disabled
12+
* Recursion prevention: in the event that there is a recursive loop, each handler detects that it has already processed the records and skips duplicated execution
13+
14+
15+
### Example Implementation: LeadTriggerHandler.cls

src/classes/Exceptions.cls

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/classes/Exceptions.cls-meta.xml

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/classes/LeadTriggerHandler.cls

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
public without sharing class LeadTriggerHandler extends TriggerHandler {
22

3-
protected override void beforeInsert(List<SObject> newRecordList) {
3+
protected override void executeBeforeInsert(List<SObject> newRecordList) {
44
List<Lead> newLeadList = (List<Lead>)newRecordList;
55

66
for(Lead newLead : newLeadList) {
77
this.setStatus(newLead);
88
}
99
}
1010

11-
protected override void beforeUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordListMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {
11+
protected override void executeBeforeUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordListMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {
1212
List<Lead> updatedLeadList = (List<Lead>)updatedRecordList;
1313
Map<Id, Lead> oldLeadMap = (Map<Id, Lead>)oldRecordMap;
1414

@@ -21,7 +21,7 @@ public without sharing class LeadTriggerHandler extends TriggerHandler {
2121

2222
private void setStatus(Lead updatedLead, Lead oldLead) {
2323
// Add logic here. Methods can be overloaded to handle updates & inserts
24-
if(updatedLead.Source != oldLead.Source) {
24+
if(updatedLead.LeadSource != oldLead.LeadSource) {
2525
this.setStatus(updatedLead);
2626
}
2727
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@isTest
2-
private class LeadTriggerHandler_Test {
2+
private class LeadTriggerHandler_Tests {
33

44
@isTest
55
static void setStatus_Test() {
File renamed without changes.

src/classes/TriggerHandler.cls

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,82 @@ public abstract class TriggerHandler {
88
}
99
public TriggerContext context; // The current context of the trigger
1010

11+
private String className;
1112
private Integer hashCode; // The hash code for the current records
1213
private Boolean isTriggerExecuting; // Checks if the code was called by a trigger
14+
private TriggerSettings__c triggerSettings;
1315

1416
protected TriggerHandler() {
17+
this.getClassName();
18+
this.getTriggerSettings();
19+
20+
this.addDebugStatement('Initializing ' + this.className);
1521
this.setTriggerContext();
1622
this.validateTriggerContext();
1723
this.setHashCode();
1824
}
1925

2026
public void execute() {
21-
String sobjectType = Trigger.new == null ? Trigger.old.getSObjectType() : Trigger.new.getSObjectType();
22-
System.debug('Starting execute method for: ' + sobjectType);
23-
System.debug('Hash codes already processed: ' + TriggerHandler.hashCodesForProcessedRecords);
24-
System.debug('Hash code for current records: ' + this.hashCode);
25-
System.debug('Trigger context for current records: ' + this.context);
26-
System.debug('Number of current records: ' + Trigger.size);
27+
this.addDebugStatement('Execute method called for ' + this.className);
28+
// Check the custom setting. If it's disabled, stop everything, show's over
29+
// You don't have to go home but you can't stay here
30+
if(!shouldExecuteTriggers()) {
31+
this.addDebugStatement('Skipping execution of class ' + this.className);
32+
return;
33+
}
34+
35+
this.addDebugStatement(this.className + ' is enabled, proceeding with execution');
36+
37+
String sobjectType = Trigger.new == null ? String.valueOf(Trigger.old.getSObjectType()) : String.valueOf(Trigger.new.getSObjectType());
38+
this.addDebugStatement('Starting execute method for: ' + sobjectType);
39+
this.addDebugStatement('Hash codes already processed: ' + TriggerHandler.hashCodesForProcessedRecords);
40+
this.addDebugStatement('Hash code for current records: ' + this.hashCode);
41+
this.addDebugStatement('Trigger context for current records: ' + this.context);
42+
this.addDebugStatement('Number of current records: ' + Trigger.size);
2743

2844
if(this.haveRecordsAlreadyBeenProcessed()) {
29-
System.debug('Records already processed for this context, skipping');
45+
this.addDebugStatement('Records already processed for this context, skipping');
3046
return;
31-
} else System.debug('Records have not been processed for this context, continuing');
47+
} else this.addDebugStatement('Records have not been processed for this context, continuing');
48+
49+
if(this.context == TriggerContext.BEFORE_INSERT) this.executeBeforeInsert(Trigger.new);
50+
else if(this.context == TriggerContext.BEFORE_UPDATE) this.executeBeforeUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
51+
else if(this.context == TriggerContext.BEFORE_DELETE) this.executeBeforeDelete(Trigger.old, Trigger.oldMap);
52+
else if(this.context == TriggerContext.AFTER_INSERT) this.executeAfterInsert(Trigger.new, Trigger.newMap);
53+
else if(this.context == TriggerContext.AFTER_UPDATE) this.executeAfterUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
54+
else if(this.context == TriggerContext.AFTER_DELETE) this.executeAfterDelete(Trigger.old, Trigger.oldMap);
55+
else if(this.context == TriggerContext.AFTER_UNDELETE) this.executeAfterUndelete(Trigger.new, Trigger.newMap);
56+
}
3257

33-
if(this.context == TriggerContext.BEFORE_INSERT) this.beforeInsert(Trigger.new);
34-
else if(this.context == TriggerContext.BEFORE_UPDATE) this.beforeUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
35-
else if(this.context == TriggerContext.BEFORE_DELETE) this.beforeDelete(Trigger.old, Trigger.oldMap);
36-
else if(this.context == TriggerContext.AFTER_INSERT) this.afterInsert(Trigger.new, Trigger.newMap);
37-
else if(this.context == TriggerContext.AFTER_UPDATE) this.afterUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
38-
else if(this.context == TriggerContext.AFTER_DELETE) this.afterDelete(Trigger.old, Trigger.oldMap);
39-
else if(this.context == TriggerContext.AFTER_UNDELETE) this.afterUndelete(Trigger.new, Trigger.newMap);
58+
protected virtual void executeBeforeInsert(List<SObject> newRecordList) {}
59+
protected virtual void executeBeforeUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {}
60+
protected virtual void executeBeforeDelete(List<SObject> deletedRecordList, Map<Id, SObject> deletedRecordMap) {}
61+
protected virtual void executeAfterInsert(List<SObject> newRecordList, Map<Id, SObject> newRecordMap) {}
62+
protected virtual void executeAfterUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {}
63+
protected virtual void executeAfterDelete(List<SObject> deletedRecordList, Map<Id, SObject> deletedRecordMap) {}
64+
protected virtual void executeAfterUndelete(List<SObject> undeletedRecordList, Map<Id, SObject> undeletedRecordMap) {}
4065

41-
this.runDmlForRelatedRecords();
66+
private void getClassName() {
67+
this.className = String.valueOf(this).split(':')[0];
4268
}
4369

44-
protected virtual void beforeInsert(List<SObject> newRecordList) {}
45-
protected virtual void beforeUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {}
46-
protected virtual void beforeDelete(List<SObject> deletedRecordList, Map<Id, SObject> deletedRecordMap) {}
47-
protected virtual void afterInsert(List<SObject> newRecordList, Map<Id, SObject> newRecordMap) {}
48-
protected virtual void afterUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {}
49-
protected virtual void afterDelete(List<SObject> deletedRecordList, Map<Id, SObject> deletedRecordMap) {}
50-
protected virtual void afterUndelete(List<SObject> undeletedRecordList, Map<Id, SObject> undeletedRecordMap) {}
70+
private void getTriggerSettings() {
71+
this.triggerSettings = TriggerSettings__c.getInstance();
72+
73+
if(this.triggerSettings.Id == null) {
74+
// If there's no ID, then there are settings setup for the current user at the user, profile or org level
75+
// Upsert the org defaults - the default field values will be used
76+
upsert TriggerSettings__c.getOrgDefaults();
77+
// Call getInstance() again to get the settings with the field defaults
78+
this.triggerSettings = TriggerSettings__c.getInstance();
79+
}
80+
}
81+
82+
private void addDebugStatement(String debugStatement) {
83+
if(!this.triggerSettings.EnableDebugging__c) return;
84+
85+
System.debug(debugStatement);
86+
}
5187

5288
private void setTriggerContext() {
5389
this.isTriggerExecuting = Trigger.isExecuting;
@@ -64,7 +100,19 @@ public abstract class TriggerHandler {
64100

65101
private void validateTriggerContext() {
66102
String errorMessage = 'Trigger handler called outside of trigger execution';
67-
if(!this.isTriggerExecuting || this.context == null) throw new Exceptions.TriggerHandlerException(errorMessage);
103+
if(!this.isTriggerExecuting || this.context == null) throw new TriggerHandlerException(errorMessage);
104+
}
105+
106+
private Boolean shouldExecuteTriggers() {
107+
this.addDebugStatement('triggerSettings.ExecuteTriggers__c=' + this.triggerSettings.ExecuteTriggers__c);
108+
109+
String handlerClassesToSkipString = this.triggerSettings.HandlerClassesToSkip__c;
110+
if(handlerClassesToSkipString == null) handlerClassesToSkipString = '';
111+
Set<String> handlerClassesToSkip = new Set<String>(handlerClassesToSkipString.toLowerCase().split('\n'));
112+
this.addDebugStatement('triggerSettings.HandlerClassesToSkip__c=' + this.triggerSettings.HandlerClassesToSkip__c);
113+
114+
// If ExecuteTriggers == true and the current class isn't in the list of handlers to skip, then execute
115+
return this.triggerSettings.ExecuteTriggers__c && !handlerClassesToSkip.contains(this.className.toLowerCase());
68116
}
69117

70118
private void setHashCode() {
@@ -111,4 +159,6 @@ public abstract class TriggerHandler {
111159
}
112160
}
113161

162+
private class TriggerHandlerException extends Exception {}
163+
114164
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3-
<apiVersion>37.0</apiVersion>
3+
<apiVersion>38.0</apiVersion>
44
<status>Active</status>
55
</ApexClass>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<customSettingsType>Hierarchy</customSettingsType>
4+
<enableFeeds>false</enableFeeds>
5+
<fields>
6+
<fullName>EnableDebugging__c</fullName>
7+
<defaultValue>true</defaultValue>
8+
<externalId>false</externalId>
9+
<inlineHelpText>Controls if debug statements from TriggerHandler are shown in the logs</inlineHelpText>
10+
<label>Enable Debugging</label>
11+
<trackTrending>false</trackTrending>
12+
<type>Checkbox</type>
13+
</fields>
14+
<fields>
15+
<fullName>ExecuteTriggers__c</fullName>
16+
<defaultValue>true</defaultValue>
17+
<externalId>false</externalId>
18+
<label>Execute Triggers</label>
19+
<trackTrending>false</trackTrending>
20+
<type>Checkbox</type>
21+
</fields>
22+
<fields>
23+
<fullName>HandlerClassesToSkip__c</fullName>
24+
<externalId>false</externalId>
25+
<inlineHelpText>Enter any handler classes that should be skipped (1 per line). For example:
26+
LeadTriggerHandler
27+
TaskTriggerHandler</inlineHelpText>
28+
<label>Handler Classes to Skip</label>
29+
<required>false</required>
30+
<trackTrending>false</trackTrending>
31+
<type>TextArea</type>
32+
</fields>
33+
<label>Trigger Settings</label>
34+
<visibility>Protected</visibility>
35+
</CustomObject>

0 commit comments

Comments
 (0)