From 04346f48226cd9b8d744778413e2830209368579 Mon Sep 17 00:00:00 2001 From: vr8ce Date: Mon, 1 Jun 2020 16:30:53 -0500 Subject: [PATCH 1/7] Reformat renatoliveira's patch to allow global bypass and multiple bypasses at once --- src/classes/TriggerHandler.cls | 65 ++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/src/classes/TriggerHandler.cls b/src/classes/TriggerHandler.cls index 8fa9b4b..7352f28 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(); @@ -51,6 +55,14 @@ 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) { @@ -71,19 +83,58 @@ public virtual class TriggerHandler { ***************************************/ public static void bypass(String handlerName) { - TriggerHandler.bypassedHandlers.add(handlerName); + bypassedHandlers.add(handlerName); + } + + public static void bypass(List handlersNames) { + bypassedHandlers.addAll(handlersNames); + } + + public static void bypass(Type handlerType) { + bypass(handlerType.getName()); + } + + public static void bypassAll() { + globalBypass = true; } public static void clearBypass(String handlerName) { - TriggerHandler.bypassedHandlers.remove(handlerName); + bypassedHandlers.remove(handlerName); + } + + public static void clearBypass(Type handlerType) { + clearBypass(handlerType.getName()); + } + + public static void clearBypass(List handlersNames) { + bypassedHandlers.removeAll(handlersNames); + } + + public static void clearGlobalBypass() { + globalBypass = false; } public static Boolean isBypassed(String handlerName) { - return TriggerHandler.bypassedHandlers.contains(handlerName); + return bypassedHandlers.contains(handlerName); + } + + public static Boolean isBypassed(Type handlerType) { + return bypassedHandlers.contains(handlerType.getName()); } public static void clearAllBypasses() { - TriggerHandler.bypassedHandlers.clear(); + bypassedHandlers.clear(); + } + + public static void showLimits() { + showLimits = true; + } + + public static Integer getLoopCount(String handlerName) { + if(TriggerHandler.loopCountMap.containsKey(handlerName)) { + return TriggerHandler.loopCountMap.get(handlerName).getCount(); + } + return 0; } /*************************************** @@ -103,7 +154,7 @@ 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; @@ -147,7 +198,7 @@ public virtual class TriggerHandler { if(!this.isTriggerExecuting || this.context == null) { throw new TriggerHandlerException('Trigger handler called outside of Trigger execution'); } - if(TriggerHandler.bypassedHandlers.contains(getHandlerName())) { + if(globalBypass || TriggerHandler.bypassedHandlers.contains(getHandlerName())) { return false; } return true; From f56e41804e3b5c0d45870dd6d0a9f4c2877a277c Mon Sep 17 00:00:00 2001 From: vr8ce Date: Tue, 2 Jun 2020 15:06:46 -0500 Subject: [PATCH 2/7] Restore TriggerHandler. to bypass methods; rework context assignment for performance --- src/classes/TriggerHandler.cls | 71 +++++++++++++++++----------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/src/classes/TriggerHandler.cls b/src/classes/TriggerHandler.cls index 7352f28..7af42e5 100644 --- a/src/classes/TriggerHandler.cls +++ b/src/classes/TriggerHandler.cls @@ -34,7 +34,9 @@ public virtual class TriggerHandler { // main method that will be called during execution public void run() { - if(!validateRun()) return; + if(!validateRun()) { + return; + } addToLoopCount(); @@ -83,15 +85,15 @@ public virtual class TriggerHandler { ***************************************/ public static void bypass(String handlerName) { - bypassedHandlers.add(handlerName); + TriggerHandler.bypassedHandlers.add(handlerName); } public static void bypass(List handlersNames) { - bypassedHandlers.addAll(handlersNames); + TriggerHandler.bypassedHandlers.addAll(handlersNames); } public static void bypass(Type handlerType) { - bypass(handlerType.getName()); + TriggerHandler.bypass(handlerType.getName()); } public static void bypassAll() { @@ -99,31 +101,31 @@ public virtual class TriggerHandler { } public static void clearBypass(String handlerName) { - bypassedHandlers.remove(handlerName); + TriggerHandler.bypassedHandlers.remove(handlerName); } public static void clearBypass(Type handlerType) { - clearBypass(handlerType.getName()); + TriggerHandler.clearBypass(handlerType.getName()); } public static void clearBypass(List handlersNames) { - bypassedHandlers.removeAll(handlersNames); - } - - public static void clearGlobalBypass() { - globalBypass = false; + TriggerHandler.bypassedHandlers.removeAll(handlersNames); } public static Boolean isBypassed(String handlerName) { - return bypassedHandlers.contains(handlerName); + return (globalBypass || TriggerHandler.bypassedHandlers.contains(handlerName)); } public static Boolean isBypassed(Type handlerType) { - return bypassedHandlers.contains(handlerType.getName()); + return (globalBypass || TriggerHandler.bypassedHandlers.contains(handlerType.getName())); } public static void clearAllBypasses() { - bypassedHandlers.clear(); + if(globalBypass) { + globalBypass = false; + } else { + TriggerHandler.bypassedHandlers.clear(); + } } public static void showLimits() { @@ -155,27 +157,26 @@ public virtual class TriggerHandler { 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; + } + } } } From 1fda5463ac466b6b18b3e946ae3de162e7d0f207 Mon Sep 17 00:00:00 2001 From: vr8ce Date: Tue, 2 Jun 2020 15:07:21 -0500 Subject: [PATCH 3/7] Add text in bypass section for bypassing a list and global bypass --- README.md | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 7da18e5..adfc4b8 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.globalBypass() - 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(); From 54e71354fdb3602417267825b763ea6808b340b4 Mon Sep 17 00:00:00 2001 From: vr8ce Date: Thu, 4 Jun 2020 12:55:36 -0500 Subject: [PATCH 4/7] Add bypassList method to return list of bypassed handlers, add comments to bypass methods, rearrange slightly for consistency --- src/classes/TriggerHandler.cls | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/classes/TriggerHandler.cls b/src/classes/TriggerHandler.cls index 7af42e5..b9baf04 100644 --- a/src/classes/TriggerHandler.cls +++ b/src/classes/TriggerHandler.cls @@ -84,19 +84,24 @@ 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; } @@ -104,14 +109,15 @@ public virtual class TriggerHandler { TriggerHandler.bypassedHandlers.remove(handlerName); } - public static void clearBypass(Type handlerType) { - TriggerHandler.clearBypass(handlerType.getName()); - } - 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 (globalBypass || TriggerHandler.bypassedHandlers.contains(handlerName)); } @@ -120,6 +126,18 @@ public virtual class TriggerHandler { 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() { if(globalBypass) { globalBypass = false; @@ -199,10 +217,7 @@ public virtual class TriggerHandler { if(!this.isTriggerExecuting || this.context == null) { throw new TriggerHandlerException('Trigger handler called outside of Trigger execution'); } - if(globalBypass || TriggerHandler.bypassedHandlers.contains(getHandlerName())) { - return false; - } - return true; + return (!globalBypass && !TriggerHandler.bypassedHandlers.contains(getHandlerName())) { } @TestVisible @@ -240,6 +255,7 @@ public virtual class TriggerHandler { private Integer max; private Integer count; + // constructor public LoopCount() { this.max = 5; this.count = 0; From e4cdf539b2ee600192bfdf5cb18698b206b14094 Mon Sep 17 00:00:00 2001 From: vr8ce Date: Sat, 13 Jun 2020 01:45:52 -0500 Subject: [PATCH 5/7] Add overloaded showLimits to allow toggling off --- src/classes/TriggerHandler.cls | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/classes/TriggerHandler.cls b/src/classes/TriggerHandler.cls index b9baf04..af932e8 100644 --- a/src/classes/TriggerHandler.cls +++ b/src/classes/TriggerHandler.cls @@ -150,6 +150,10 @@ public virtual class TriggerHandler { 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(); From 2d991a4a1dfc3681accedf7696eaae10aafdb87e Mon Sep 17 00:00:00 2001 From: vr8ce Date: Sat, 13 Jun 2020 01:46:53 -0500 Subject: [PATCH 6/7] Correct global bypass call --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index adfc4b8..85e715c 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ TriggerHandler.bypass('AccountTriggerHandler'); //List handlers = new List{'AccountTriggerHandler', 'OpportunityTriggerHandler'} //TriggerHandler.bypass(handlers); // globally -//TriggerHandler.globalBypass() +//TriggerHandler.bypassAll() acc.Name = 'No Trigger'; update acc; // won't invoke the AccountTriggerHandler From 027ead257bef3b40fc99312a1b8f3e83fd7feb5b Mon Sep 17 00:00:00 2001 From: vr8ce Date: Tue, 14 Jul 2020 22:17:19 -0500 Subject: [PATCH 7/7] Update to match current master --- src/classes/TriggerHandler.cls | 44 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/classes/TriggerHandler.cls b/src/classes/TriggerHandler.cls index af932e8..6c3dcaf 100644 --- a/src/classes/TriggerHandler.cls +++ b/src/classes/TriggerHandler.cls @@ -41,20 +41,28 @@ public virtual class TriggerHandler { addToLoopCount(); // dispatch to the correct handler method - if(this.context == TriggerContext.BEFORE_INSERT) { - this.beforeInsert(); - } else if(this.context == TriggerContext.BEFORE_UPDATE) { - this.beforeUpdate(); - } else if(this.context == TriggerContext.BEFORE_DELETE) { - this.beforeDelete(); - } else if(this.context == TriggerContext.AFTER_INSERT) { - this.afterInsert(); - } else if(this.context == TriggerContext.AFTER_UPDATE) { - this.afterUpdate(); - } else if(this.context == TriggerContext.AFTER_DELETE) { - this.afterDelete(); - } else if(this.context == TriggerContext.AFTER_UNDELETE) { - this.afterUndelete(); + switch on this.context { + when BEFORE_INSERT { + this.beforeInsert(); + } + when BEFORE_UPDATE { + this.beforeUpdate(); + } + when BEFORE_DELETE { + this.beforeDelete(); + } + when AFTER_INSERT { + this.afterInsert(); + } + when AFTER_UPDATE { + this.afterUpdate(); + } + when AFTER_DELETE { + this.afterDelete(); + } + when AFTER_UNDELETE { + this.afterUndelete(); + } } if(showLimits) { @@ -221,7 +229,7 @@ public virtual class TriggerHandler { if(!this.isTriggerExecuting || this.context == null) { throw new TriggerHandlerException('Trigger handler called outside of Trigger execution'); } - return (!globalBypass && !TriggerHandler.bypassedHandlers.contains(getHandlerName())) { + return (!globalBypass && !TriggerHandler.bypassedHandlers.contains(getHandlerName())); } @TestVisible @@ -276,11 +284,7 @@ public virtual class TriggerHandler { } public Boolean exceeded() { - if(this.max < 0) return false; - if(this.count > this.max) { - return true; - } - return false; + return this.max >= 0 && this.count > this.max; } public Integer getMax() {