diff --git a/README.md b/README.md index 7da18e5..85e715c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This trigger framework bundles a single **TriggerHandler** base class that you c The base class also provides a secondary role as a supervisor for Trigger execution. It acts like a watchdog, monitoring trigger activity and providing an api for controlling certain aspects of execution and control flow. -But the most important part of this framework is that it's minimal and simple to use. +But the most important part of this framework is that it's minimal and simple to use. **Deploy to SFDX Scratch Org:** [![Deploy](https://deploy-to-sfdx.com/dist/assets/images/DeployToSFDX.svg)](https://deploy-to-sfdx.com) @@ -33,7 +33,7 @@ In your trigger handler, to add logic to any of the trigger contexts, you only n ```java public class OpportunityTriggerHandler extends TriggerHandler { - + public override void beforeUpdate() { for(Opportunity o : (List) Trigger.new) { // do something @@ -45,7 +45,7 @@ public class OpportunityTriggerHandler extends TriggerHandler { } ``` -**Note:** When referencing the Trigger statics within a class, SObjects are returned versus SObject subclasses like Opportunity, Account, etc. This means that you must cast when you reference them in your trigger handler. You could do this in your constructor if you wanted. +**Note:** When referencing the Trigger statics within a class, SObjects are returned versus SObject subclasses like Opportunity, Account, etc. This means that you must cast when you reference them in your trigger handler. You could do this in your constructor if you wanted. ```java public class OpportunityTriggerHandler extends TriggerHandler { @@ -55,7 +55,7 @@ public class OpportunityTriggerHandler extends TriggerHandler { public OpportunityTriggerHandler() { this.newOppMap = (Map) Trigger.newMap; } - + public override void afterUpdate() { // } @@ -83,7 +83,7 @@ public class OpportunityTriggerHandler extends TriggerHandler { public OpportunityTriggerHandler() { this.setMaxLoopCount(1); } - + public override void afterUpdate() { List opps = [SELECT Id FROM Opportunity WHERE Id IN :Trigger.newMap.keySet()]; update opps; // this will throw after this update @@ -94,32 +94,34 @@ public class OpportunityTriggerHandler extends TriggerHandler { ### Bypass API -What if you want to tell other trigger handlers to halt execution? That's easy with the bypass api: +What if you want to tell other trigger handlers to halt execution? That's easy with the bypass api. Trigger handlers can be bypassed individually, multiple at a time, or globally. ```java -public class OpportunityTriggerHandler extends TriggerHandler { - - public override void afterUpdate() { - List opps = [SELECT Id, AccountId FROM Opportunity WHERE Id IN :Trigger.newMap.keySet()]; - - Account acc = [SELECT Id, Name FROM Account WHERE Id = :opps.get(0).AccountId]; - - TriggerHandler.bypass('AccountTriggerHandler'); +List opps = [SELECT Id, AccountId FROM Opportunity WHERE ExpectedRevenue < 100000]; - acc.Name = 'No Trigger'; - update acc; // won't invoke the AccountTriggerHandler +Account acc = [SELECT Id, Name FROM Account WHERE Id = :opps.get(0).AccountId]; - TriggerHandler.clearBypass('AccountTriggerHandler'); +// individually +TriggerHandler.bypass('AccountTriggerHandler'); +// multiple +//List handlers = new List{'AccountTriggerHandler', 'OpportunityTriggerHandler'} +//TriggerHandler.bypass(handlers); +// globally +//TriggerHandler.bypassAll() - acc.Name = 'With Trigger'; - update acc; // will invoke the AccountTriggerHandler +acc.Name = 'No Trigger'; +update acc; // won't invoke the AccountTriggerHandler - } +// they can be cleared in the same ways +TriggerHandler.clearBypass('AccountTriggerHandler'); // clear a single one (even if multiple bypassed) +//TriggerHandler.clearBypass(handlers); // clear a list (can be a subset of those bypassed) +//TriggerHandler.clearAllBypasses(); // clear all bypasses, or a global one -} +acc.Name = 'With Trigger'; +update acc; // will invoke the AccountTriggerHandler ``` -If you need to check if a handler is bypassed, use the `isBypassed` method: +If you need to check if a handler is bypassed, use the `isBypassed` method. This will return true if the indicated handler is being bypassed, or if globalBypass() was called: ```java if (TriggerHandler.isBypassed('AccountTriggerHandler')) { @@ -127,10 +129,10 @@ if (TriggerHandler.isBypassed('AccountTriggerHandler')) { } ``` -If you want to clear all bypasses for the transaction, simple use the `clearAllBypasses` method, as in: +If you want to clear all bypasses for the transaction, simply use the `clearAllBypasses` method: ```java -// ... done with bypasses! +// ... done with bypasses (including global ones)! TriggerHandler.clearAllBypasses(); diff --git a/src/classes/TriggerHandler.cls b/src/classes/TriggerHandler.cls index 7ba6d54..6c3dcaf 100644 --- a/src/classes/TriggerHandler.cls +++ b/src/classes/TriggerHandler.cls @@ -3,6 +3,8 @@ public virtual class TriggerHandler { // static map of handlername, times run() was invoked private static Map loopCountMap; private static Set bypassedHandlers; + @TestVisible private static Boolean globalBypass; + @TestVisible private static Boolean showLimits; // the current context of the trigger, overridable in tests @TestVisible @@ -16,8 +18,10 @@ public virtual class TriggerHandler { static { loopCountMap = new Map(); bypassedHandlers = new Set(); + globalBypass = false; + showLimits = false; } - + // constructor public TriggerHandler() { this.setTriggerContext(); @@ -60,6 +64,15 @@ public virtual class TriggerHandler { this.afterUndelete(); } } + + if(showLimits) { + System.debug(LoggingLevel.DEBUG, String.format('{0} on {1} ({2}/{3})', new List{ + this.context+'', + getHandlerName(), + Limits.getQueries()+'', + Limits.getLimitQueries()+'' + })); + } } public void setMaxLoopCount(Integer max) { @@ -79,20 +92,81 @@ public virtual class TriggerHandler { * public static methods ***************************************/ + // bypass by string, e.g. TriggerHandler.bypass('AccountTriggerHandler') public static void bypass(String handlerName) { TriggerHandler.bypassedHandlers.add(handlerName); } + // bypass by list, e.g. TriggerHandler.bypass(listOfHandlerStrings) + public static void bypass(List handlersNames) { + TriggerHandler.bypassedHandlers.addAll(handlersNames); + } + + // bypass by type, e.g. TriggerHandler.bypass(AccountTriggerHandler.class) + public static void bypass(Type handlerType) { + TriggerHandler.bypass(handlerType.getName()); + } + + // bypass all handlers (clear bypassedHandlers to prevent confusion) + public static void bypassAll() { + TriggerHandler.bypassedHandlers.clear(); + globalBypass = true; + } + public static void clearBypass(String handlerName) { TriggerHandler.bypassedHandlers.remove(handlerName); } + public static void clearBypass(List handlersNames) { + TriggerHandler.bypassedHandlers.removeAll(handlersNames); + } + + public static void clearBypass(Type handlerType) { + TriggerHandler.clearBypass(handlerType.getName()); + } + + // a handler is considered bypassed if it was bypassed, or all handlers have been public static Boolean isBypassed(String handlerName) { - return TriggerHandler.bypassedHandlers.contains(handlerName); + return (globalBypass || TriggerHandler.bypassedHandlers.contains(handlerName)); + } + + public static Boolean isBypassed(Type handlerType) { + return (globalBypass || TriggerHandler.bypassedHandlers.contains(handlerType.getName())); + } + + // return a list of the bypassed handlers + public static List bypassList() { + List bypasses = new List(TriggerHandler.bypassedHandlers); + + // bypassAll clears bypassedHandlers, so bypasses is empty here + if(globalBypass) { + bypasses.add('bypassAll'); + } + + return bypasses; } public static void clearAllBypasses() { - TriggerHandler.bypassedHandlers.clear(); + if(globalBypass) { + globalBypass = false; + } else { + TriggerHandler.bypassedHandlers.clear(); + } + } + + public static void showLimits() { + showLimits = true; + } + + public static void showLimits(Boolean enabled) { + showLimits = enabled; + } + + public static Integer getLoopCount(String handlerName) { + if(TriggerHandler.loopCountMap.containsKey(handlerName)) { + return TriggerHandler.loopCountMap.get(handlerName).getCount(); + } + return 0; } /*************************************** @@ -112,28 +186,27 @@ public virtual class TriggerHandler { } else { this.isTriggerExecuting = true; } - - if((Trigger.isExecuting && Trigger.isBefore && Trigger.isInsert) || - (ctx != null && ctx == 'before insert')) { - this.context = TriggerContext.BEFORE_INSERT; - } else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isUpdate) || - (ctx != null && ctx == 'before update')){ - this.context = TriggerContext.BEFORE_UPDATE; - } else if((Trigger.isExecuting && Trigger.isBefore && Trigger.isDelete) || - (ctx != null && ctx == 'before delete')) { - this.context = TriggerContext.BEFORE_DELETE; - } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isInsert) || - (ctx != null && ctx == 'after insert')) { - this.context = TriggerContext.AFTER_INSERT; - } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUpdate) || - (ctx != null && ctx == 'after update')) { - this.context = TriggerContext.AFTER_UPDATE; - } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isDelete) || - (ctx != null && ctx == 'after delete')) { - this.context = TriggerContext.AFTER_DELETE; - } else if((Trigger.isExecuting && Trigger.isAfter && Trigger.isUndelete) || - (ctx != null && ctx == 'after undelete')) { - this.context = TriggerContext.AFTER_UNDELETE; + + if(Trigger.isExecuting) { + if(Trigger.isBefore) { + if (Trigger.IsInsert || (ctx != null && ctx == 'before insert')) { + this.context = TriggerContext.BEFORE_INSERT; + } else if (Trigger.IsUpdate || (ctx != null && ctx == 'before update')) { + this.context = TriggerContext.BEFORE_UPDATE; + } else if (Trigger.IsDelete || (ctx != null && ctx == 'before delete')) { + this.context = TriggerContext.BEFORE_DELETE; + } + } else if(Trigger.isAfter) { + if (Trigger.IsInsert || (ctx != null && ctx == 'after insert')) { + this.context = TriggerContext.AFTER_INSERT; + } else if (Trigger.IsUpdate || (ctx != null && ctx == 'after update')) { + this.context = TriggerContext.AFTER_UPDATE; + } else if (Trigger.IsDelete || (ctx != null && ctx == 'after delete')) { + this.context = TriggerContext.AFTER_DELETE; + } else if (Trigger.IsUndelete || (ctx != null && ctx == 'after undelete')) { + this.context = TriggerContext.AFTER_UNDELETE; + } + } } } @@ -156,7 +229,7 @@ public virtual class TriggerHandler { if(!this.isTriggerExecuting || this.context == null) { throw new TriggerHandlerException('Trigger handler called outside of Trigger execution'); } - return !TriggerHandler.bypassedHandlers.contains(getHandlerName()); + return (!globalBypass && !TriggerHandler.bypassedHandlers.contains(getHandlerName())); } @TestVisible @@ -194,6 +267,7 @@ public virtual class TriggerHandler { private Integer max; private Integer count; + // constructor public LoopCount() { this.max = 5; this.count = 0;