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

Commit

Permalink
Added a custom setting to control trigger execution & debug statements (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
jongpie committed Feb 22, 2017
1 parent afd6e63 commit 33a6fc1
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 40 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Apex Trigger Framework
<a target="_blank" href="https://githubsfdeploy.herokuapp.com?owner=jongpie&repo=ApexTriggerFramework">
<img alt="Deploy to Salesforce" src="https://github.com/raw/afawcett/githubsfdeploy/master/src/main/webapp/resources/img/deploy.png">
</a>

## Features
* Implements Salesforce best practices of 1 trigger per object & logicless triggers
* 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
* Provides recursion detection/prevention by checking the list of trigger records have already been processed
* Allows triggers to be enabled/disabled both globally and individually at the org, profile and user levels (hierarchy custom setting)
* Allows framework debug statements to be enabled/disabled
* 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


### Example Implementation: LeadTriggerHandler.cls
6 changes: 0 additions & 6 deletions src/classes/Exceptions.cls

This file was deleted.

5 changes: 0 additions & 5 deletions src/classes/Exceptions.cls-meta.xml

This file was deleted.

6 changes: 3 additions & 3 deletions src/classes/LeadTriggerHandler.cls
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
public without sharing class LeadTriggerHandler extends TriggerHandler {

protected override void beforeInsert(List<SObject> newRecordList) {
protected override void executeBeforeInsert(List<SObject> newRecordList) {
List<Lead> newLeadList = (List<Lead>)newRecordList;

for(Lead newLead : newLeadList) {
this.setStatus(newLead);
}
}

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

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

private void setStatus(Lead updatedLead, Lead oldLead) {
// Add logic here. Methods can be overloaded to handle updates & inserts
if(updatedLead.Source != oldLead.Source) {
if(updatedLead.LeadSource != oldLead.LeadSource) {
this.setStatus(updatedLead);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@isTest
private class LeadTriggerHandler_Test {
private class LeadTriggerHandler_Tests {

@isTest
static void setStatus_Test() {
Expand Down
98 changes: 74 additions & 24 deletions src/classes/TriggerHandler.cls
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,82 @@ public abstract class TriggerHandler {
}
public TriggerContext context; // The current context of the trigger

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

protected TriggerHandler() {
this.getClassName();
this.getTriggerSettings();

this.addDebugStatement('Initializing ' + this.className);
this.setTriggerContext();
this.validateTriggerContext();
this.setHashCode();
}

public void execute() {
String sobjectType = Trigger.new == null ? Trigger.old.getSObjectType() : Trigger.new.getSObjectType();
System.debug('Starting execute method for: ' + sobjectType);
System.debug('Hash codes already processed: ' + TriggerHandler.hashCodesForProcessedRecords);
System.debug('Hash code for current records: ' + this.hashCode);
System.debug('Trigger context for current records: ' + this.context);
System.debug('Number of current records: ' + Trigger.size);
this.addDebugStatement('Execute method called for ' + this.className);
// Check the custom setting. If it's disabled, stop everything, show's over
// You don't have to go home but you can't stay here
if(!shouldExecuteTriggers()) {
this.addDebugStatement('Skipping execution of class ' + this.className);
return;
}

this.addDebugStatement(this.className + ' is enabled, proceeding with execution');

String sobjectType = Trigger.new == null ? String.valueOf(Trigger.old.getSObjectType()) : String.valueOf(Trigger.new.getSObjectType());
this.addDebugStatement('Starting execute method for: ' + sobjectType);
this.addDebugStatement('Hash codes already processed: ' + TriggerHandler.hashCodesForProcessedRecords);
this.addDebugStatement('Hash code for current records: ' + this.hashCode);
this.addDebugStatement('Trigger context for current records: ' + this.context);
this.addDebugStatement('Number of current records: ' + Trigger.size);

if(this.haveRecordsAlreadyBeenProcessed()) {
System.debug('Records already processed for this context, skipping');
this.addDebugStatement('Records already processed for this context, skipping');
return;
} else System.debug('Records have not been processed for this context, continuing');
} else this.addDebugStatement('Records have not been processed for this context, continuing');

if(this.context == TriggerContext.BEFORE_INSERT) this.executeBeforeInsert(Trigger.new);
else if(this.context == TriggerContext.BEFORE_UPDATE) this.executeBeforeUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.BEFORE_DELETE) this.executeBeforeDelete(Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.AFTER_INSERT) this.executeAfterInsert(Trigger.new, Trigger.newMap);
else if(this.context == TriggerContext.AFTER_UPDATE) this.executeAfterUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.AFTER_DELETE) this.executeAfterDelete(Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.AFTER_UNDELETE) this.executeAfterUndelete(Trigger.new, Trigger.newMap);
}

if(this.context == TriggerContext.BEFORE_INSERT) this.beforeInsert(Trigger.new);
else if(this.context == TriggerContext.BEFORE_UPDATE) this.beforeUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.BEFORE_DELETE) this.beforeDelete(Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.AFTER_INSERT) this.afterInsert(Trigger.new, Trigger.newMap);
else if(this.context == TriggerContext.AFTER_UPDATE) this.afterUpdate(Trigger.new, Trigger.newMap, Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.AFTER_DELETE) this.afterDelete(Trigger.old, Trigger.oldMap);
else if(this.context == TriggerContext.AFTER_UNDELETE) this.afterUndelete(Trigger.new, Trigger.newMap);
protected virtual void executeBeforeInsert(List<SObject> newRecordList) {}
protected virtual void executeBeforeUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {}
protected virtual void executeBeforeDelete(List<SObject> deletedRecordList, Map<Id, SObject> deletedRecordMap) {}
protected virtual void executeAfterInsert(List<SObject> newRecordList, Map<Id, SObject> newRecordMap) {}
protected virtual void executeAfterUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {}
protected virtual void executeAfterDelete(List<SObject> deletedRecordList, Map<Id, SObject> deletedRecordMap) {}
protected virtual void executeAfterUndelete(List<SObject> undeletedRecordList, Map<Id, SObject> undeletedRecordMap) {}

this.runDmlForRelatedRecords();
private void getClassName() {
this.className = String.valueOf(this).split(':')[0];
}

protected virtual void beforeInsert(List<SObject> newRecordList) {}
protected virtual void beforeUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {}
protected virtual void beforeDelete(List<SObject> deletedRecordList, Map<Id, SObject> deletedRecordMap) {}
protected virtual void afterInsert(List<SObject> newRecordList, Map<Id, SObject> newRecordMap) {}
protected virtual void afterUpdate(List<SObject> updatedRecordList, Map<Id, SObject> updatedRecordMap, List<SObject> oldRecordList, Map<Id, SObject> oldRecordMap) {}
protected virtual void afterDelete(List<SObject> deletedRecordList, Map<Id, SObject> deletedRecordMap) {}
protected virtual void afterUndelete(List<SObject> undeletedRecordList, Map<Id, SObject> undeletedRecordMap) {}
private void getTriggerSettings() {
this.triggerSettings = TriggerSettings__c.getInstance();

if(this.triggerSettings.Id == null) {
// If there's no ID, then there are settings setup for the current user at the user, profile or org level
// Upsert the org defaults - the default field values will be used
upsert TriggerSettings__c.getOrgDefaults();
// Call getInstance() again to get the settings with the field defaults
this.triggerSettings = TriggerSettings__c.getInstance();
}
}

private void addDebugStatement(String debugStatement) {
if(!this.triggerSettings.EnableDebugging__c) return;

System.debug(debugStatement);
}

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

private void validateTriggerContext() {
String errorMessage = 'Trigger handler called outside of trigger execution';
if(!this.isTriggerExecuting || this.context == null) throw new Exceptions.TriggerHandlerException(errorMessage);
if(!this.isTriggerExecuting || this.context == null) throw new TriggerHandlerException(errorMessage);
}

private Boolean shouldExecuteTriggers() {
this.addDebugStatement('triggerSettings.ExecuteTriggers__c=' + this.triggerSettings.ExecuteTriggers__c);

String handlerClassesToSkipString = this.triggerSettings.HandlerClassesToSkip__c;
if(handlerClassesToSkipString == null) handlerClassesToSkipString = '';
Set<String> handlerClassesToSkip = new Set<String>(handlerClassesToSkipString.toLowerCase().split('\n'));
this.addDebugStatement('triggerSettings.HandlerClassesToSkip__c=' + this.triggerSettings.HandlerClassesToSkip__c);

// If ExecuteTriggers == true and the current class isn't in the list of handlers to skip, then execute
return this.triggerSettings.ExecuteTriggers__c && !handlerClassesToSkip.contains(this.className.toLowerCase());
}

private void setHashCode() {
Expand Down Expand Up @@ -111,4 +159,6 @@ public abstract class TriggerHandler {
}
}

private class TriggerHandlerException extends Exception {}

}
2 changes: 1 addition & 1 deletion src/classes/TriggerHandler.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>37.0</apiVersion>
<apiVersion>38.0</apiVersion>
<status>Active</status>
</ApexClass>
35 changes: 35 additions & 0 deletions src/objects/TriggerSettings__c.object
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<customSettingsType>Hierarchy</customSettingsType>
<enableFeeds>false</enableFeeds>
<fields>
<fullName>EnableDebugging__c</fullName>
<defaultValue>true</defaultValue>
<externalId>false</externalId>
<inlineHelpText>Controls if debug statements from TriggerHandler are shown in the logs</inlineHelpText>
<label>Enable Debugging</label>
<trackTrending>false</trackTrending>
<type>Checkbox</type>
</fields>
<fields>
<fullName>ExecuteTriggers__c</fullName>
<defaultValue>true</defaultValue>
<externalId>false</externalId>
<label>Execute Triggers</label>
<trackTrending>false</trackTrending>
<type>Checkbox</type>
</fields>
<fields>
<fullName>HandlerClassesToSkip__c</fullName>
<externalId>false</externalId>
<inlineHelpText>Enter any handler classes that should be skipped (1 per line). For example:
LeadTriggerHandler
TaskTriggerHandler</inlineHelpText>
<label>Handler Classes to Skip</label>
<required>false</required>
<trackTrending>false</trackTrending>
<type>TextArea</type>
</fields>
<label>Trigger Settings</label>
<visibility>Protected</visibility>
</CustomObject>

0 comments on commit 33a6fc1

Please sign in to comment.