MyTriggers
Lightweight Custom Metadata driven Trigger Framework that scales to your needs. Extended from TriggerX by Seb Wagner, provided with <3 by appero.com
ChangeLog
09-2019
- added
SObjectApiName__c
toMyTrigger Settings
to accomodate for sObjects not available in thesObject
picklist - added
IsByPassAllowed__c
toMyTrigger Settings
and a custom permissionbypassMyTriggers
. This allows for excluding certain users from trigger execution (e.g. Integration User).
10-2018
- initial release
Installation
Developer Controlled Package
- Production Instances: https://login.salesforce.com/packaging/installPackage.apexp?p0=04t1i000000gZ4HAAU
- Sandbox Instances: https://test.salesforce.com/packaging/installPackage.apexp?p0=04t1i000000gZ4HAAU
From Source
Clone this repo
git clone https://github.com/appero-com/MyTriggers
Create a scratch org and push source
sfdx force:org:create -a MyTriggers -s -f config/project-scratch-def.json && sfdx force:source:push -r MyTriggers/framework
or deploy to your org
sfdx force:source:convert -r MyTriggers/framework -d src && sfdx force:mdapi:deploy -u <username> -d src
Resources
Issues / Known Limitations
Found something? Use the issues page
Contribution
PRs are welcome
MyTriggers HowTo
MyTriggers is a lightweight Custom Metadata driven Trigger Framework that scales to your needs. It is based upon the foundation of TriggerX that Sebastian Wagner wrote in 2013, which was a perfect starting point since it covered all one could wish for in a trigger handler except Custom Metadata Types.
The general approach behind MyTriggers is
- run all triggers through one central handler, even across namespaces.
- design the orchestration of logic in a way that allows you to declarative wire things differently
- think about Triggers in new ways: configurable, closer to business needs/processes than database changes
Dreamforce 2018: "Route Your Triggers Like a Pro"
MyTriggers was released by appero GmbH and publicly presented by Christian Szandor Knapp and Daniel Stange.
If you want to recap the session, a recording will be available a few weeks after Dreamforce. The session discussion and assets will be stored at the session's tralblazer commuinty page.
You can follow Szandor on Twitter at @ch_sz_knapp, Daniel is @stangomat.
A change in perspective - a Business Process Centric Approach
When you reflect upon what your triggers actually do, you may hardly ever say that they are pieces of code that react to changes in data whenever they happen. You'd rather describe them as entry points for your business processes that, for example, create an onboarding case for new customers whenever an opportunity closes, but only for accounts that never had a closed opportunity before.
Now, with that description in mind, we should build our trigger handlers in a way that they can react to change in business requirements, and that they handle a lot more than just the records that initially started the process.
This is why MyTriggers has a records property that contains any sObject type (but all the records that are handled currently), and this is why there are Custom Metadata Type records that can be activated, grouped by names, put in a sequential orders (and re-ordered if need be).
You decide what happens when a trigger fires - the constant is that it will always open one central instance of MyTriggers that orchestrates your business processes according to your Metadata config.
General Design
- Trigger execution will be started by instantion of MyTriggers and calling the run() method from a Trigger
- records contains all objects currently handled by the trigger context
- recordsNotYetProcessed can be inspected through their getter method
- Ids of updated records can be accessed through their getter method
- handled trigger contexts can be accessed through their setter method
- you can enable() or disable() specific handler steps or trigger contexts at runtime
- for each handler step, MyTriggers has to be extended
- each handler step orchestrates its logic through overriding methods specific for the trigger contexts. _ onBeforeInsert() _ onAfterInsert() _ onBeforeUpdate() _ onAfterUpdate() _ onBeforeDelete() _ onAfterDelete() * onAfterUndelete()
Registering MyTriggers for all Trigger contexts
For MyTriggers to handle your triggers, create a Trigger for all contexts. Create a new instance of MyTriggers and call run()
.
Trigger AccountTrigger on Account (
before insert,
before update,
before delete,
after insert,
after update,
after delete,
after undelete) {
MyTriggers.run();
}
Building Trigger Handler Steps
Each handler step should extend the MyTriggers class and can override any of the methods.
public class newAccounts_ValidateType extends MyTriggers {
public override void onBeforeInsert() {
List<Account> newAccounts = (List<Account>)records;
}
private void stepLogic() {
// some method to handle the logic
}
}
Working With the "records" Propery
MyTriggers exposes a public instance variable records that contains all records that are handled by MyTriggers. When working with the records property, you should cast it to a specific type.
(List<Account>)records
MyTriggers has a helper property for you to control your the process flow: recordsNotYetProcessed. You can access it through its getter method, Same goes for updated records - you can access their Ids through a getter
Registering Steps For Execution
The execution flow is controlled by custom metadata records of the MyTriggerSetting type. You have to specify
- an sObject Type of a standard or custom object or a platform event
- a class that contains your logic for this step
- a trigger context
Additionally, you should set
- the activation flag
- a sequence number
Optionally, set
- a namespace prefix for the class that you are going to call if you want to call (or build) namespaced trigger handler steps.
One Word About Sequence Numbering
It doesn't really matter which sequence numbering you choose as long as it can be sorted. To avoid renumbering a whole set of steps, choose a numbering method that allows for gaps.
Classic ERP numbering styles and sequence might make you smile - but if your initial numbering sequence was 100, 200, 300, 400, 500 ..., you can add 190 and 210 later, and 195 and 215... without renumbering the whole list if you add one step inbetween.
Deactivating Triggers
MyTriggers allows you to deactivate or re-wire Trigger steps in productive environments. But just because it is possible does not mean that it is a good idea, necessarily.
Be extra careful when you deactivate or modify productive Trigger handler steps and keep a reminder that works for you (sticky notes, an alarm clock) so that you don't forget to activate your triggers again.
Enable Steps Or Trigger Contexts at Runtime
MyTriggers allows total control over all trigger operation at runtime.
public override void onAfterInsert() {
// records property provided by myTriggers
List<Account> newAccounts = new Map<Id,Map>(records);
// disable after insert trigger on Opp so it doesn't interfere
myTriggers.disable(myCaseTrigger.class,
new List<System.TriggerOperation>{
System.TriggerOperation.AFTER_UPDATE});
CaseService.updateCustomerCareCases(newAccounts);
}
Finding Out If (And Which) Data Has Changed
// use string field names
List<String> fieldNamesToCheck = new String[]{'Multiplier__c','Revenue__c','OwnerId','Type__c'};
if (MyTriggers.hasChangedFields(fieldNamesToCheck,record,recordOld)){
// logic executed when condition is true
}
// or sObjectFields
sObjectField[] fieldsToCheck = new sObjectField[]{Share__c.Multiplier__c, Share__c.Revenue__c, Share__c.OwnerId, Share__c.Type__c};
for (sObjectField field : MyTriggers.getChangedFields(fieldsToCheck,record,recordOld)){
// process field
}
Recursion control
// add all records in the current update context
MyTriggers.addUpdatedIds(triggerOldMap.keySet());
// and use this to return only records which havent been processed before
List<Sobject> untouchedRecords = MyTriggers.getRecordsNotYetProcessed();
Documentation
MyTriggers
global virtual class MyTriggers
Leightweight Custom Metadata driven Trigger Framework that scales to your needs
Properties
records
cast records to appropriate sObjectType in implementations
Methods
-
add set of ids to updatedIds
-
disable disables all events for System.Type MyClass
-
disable disables all events for the trigger handler with given namespace and classname Method also works in subscriber org with hidden (public) trigger handlers from managed package
-
disable disable all specificed events for the System.Type MyClass
-
disable all specificed events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
disable a single event for System.Type MyClass
-
disable a single event for ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
used instead of constructor since handlers are instanciated with an empty contructor
-
removes all disabled events for the System.Type MyClass
-
removes all disabled events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
enable all specificed events for the System.Type MyClass
-
enable all specificed events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
enable a single event for System.Type MyClass
-
enable a single event for ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
getAfterEvents list of all AFTER System.TriggerOperation enums
-
getBeforeEvents list of all BEFORE System.TriggerOperation enums
-
returns a list of changed fields based on provided fieldList list
-
returns a list of changed fields based on provided fieldList list
-
getDeleteEvents all delete events
-
returns set of disabled events
-
returns set of disabled events Method also works in subscriber org with hidden (public) trigger handlers from managed package
-
getInsertEvents all insert events
-
returns a list of objects that have not been processed yet
-
return all updated ids
-
getUpdateEvents all update events
-
returns true if a value of one of the specified fields has changed
-
returns true if a value of one of the specified fields has changed
-
isDisabled returns true if the specified event is disabled
-
isDisabled returns true if the specified event is disabled Method also works in subscriber org with hidden (public) trigger handlers from managed package
-
Global Constructor reserved for future use
-
executed to perform AFTER_DELETE operations
-
executed to perform AFTER_INSERT operations
-
executed to perform AFTER_UNDELETE operations
-
executed to perform AFTER_UPDATE operations
-
executed to perform BEFORE_DELETE operations
-
executed to perform BEFORE_INSERT operations
-
executed to perform BEFORE_UPDATE operations
-
Entry point of myTriggers framework - called from implementations
-
loads trigger event settings MyTriggerSetting__mdt
-
loads trigger event settings MyTriggerSetting__mdt Method also works in subscriber org with hidden (public) trigger handlers from managed package
-
converts a Set of Event enums into Strings
addUpdatedIds
global static void addUpdatedIds(Set idSet)
add set of ids to updatedIds
Parameters
- idSet - Set, usally Trigger.newMap.keyset()
disable
global static void disable(Type MyClass)
disable disables all events for System.Type MyClass
disable
global static void disable(String namespacePrefix, String className)
disable disables all events for the trigger handler with given namespace and classname Method also works in subscriber org with hidden (public) trigger handlers from managed package
disable
global static void disable(Type MyClass, System.TriggerOperation[] events)
disable all specificed events for the System.Type MyClass
disable
disable all specificed events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
disable
global static void disable(Type MyClass, System.TriggerOperation event)
disable a single event for System.Type MyClass
disable
global static void disable(String namespacePrefix, String className, System.TriggerOperation event)
disable a single event for ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
doConstruct
global virtual myTriggers doConstruct(sObject[] records)
used instead of constructor since handlers are instanciated with an empty contructor
Parameters
- records - Array of current sObjects. For INSERT & UPDATE Trigger.new otherwise Trigger.old
enable
global static void enable(Type MyClass)
removes all disabled events for the System.Type MyClass
enable
global static void enable(String namespacePrefix, String className)
removes all disabled events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
enable
global static void enable(Type MyClass, System.TriggerOperation[] events)
enable all specificed events for the System.Type MyClass
enable
enable all specificed events for given ClassName and Namespace. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
enable
global static void enable(Type MyClass, System.TriggerOperation event)
enable a single event for System.Type MyClass
enable
global static void enable(String namespacePrefix, String className, System.TriggerOperation event)
enable a single event for ClassName and Namespace. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
getAfterEvents
global static System.TriggerOperation[] getAfterEvents()
getAfterEvents list of all AFTER System.TriggerOperation enums
getBeforeEvents
global static System.TriggerOperation[] getBeforeEvents()
getBeforeEvents list of all BEFORE System.TriggerOperation enums
getChangedFields
global static String[] getChangedFields(String[] fieldList, sObject record, sObject recordOld)
returns a list of changed fields based on provided fieldList list
getChangedFields
returns a list of changed fields based on provided fieldList list
getDeleteEvents
global static System.TriggerOperation[] getDeleteEvents()
getDeleteEvents all delete events
getDisabledEvents
global static Set getDisabledEvents(Type MyClass)
returns set of disabled events
Return Value
- Set of disabled Event Namens (e.g. 'AFTER_UPDATE')
getDisabledEvents
global static Set getDisabledEvents(String namespacePrefix, String className)
returns set of disabled events. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
getInsertEvents
global static System.TriggerOperation[] getInsertEvents()
get all insert events
getRecordsNotYetProcessed
global protected sObject[] getRecordsNotYetProcessed()
returns a list of objects that have not been processed yet
getUpdatedIds
global static Set getUpdatedIds()
return all updated ids
Return Value
- set of updated/already touched Ids
getUpdateEvents
global static System.TriggerOperation[] getUpdateEvents()
getUpdateEvents all update events
Return Value
System.TriggerOperation[]
hasChangedFields
global static Boolean hasChangedFields(String[] fieldList, sObject record, sObject recordOld)
returns true if a value of one of the specified fields has changed
hasChangedFields
global static Boolean hasChangedFields(sObjectField[] fieldList, sObject record, sObject recordOld)
returns true if a value of one of the specified fields has changed
isDisabled
global static Boolean isDisabled(Type MyClass, System.TriggerOperation event)
returns true if the specified event is disabled
isDisabled
returns true if the specified event is disabled Method. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
myTriggers
Global Constructor reserved for future use
onAfterDelete
global virtual void onAfterDelete()
executed to perform AFTER_DELETE operations
onAfterInsert
global virtual void onAfterInsert()
executed to perform AFTER_INSERT operations
onAfterUndelete
global virtual void onAfterUndelete()
executed to perform AFTER_UNDELETE operations
onAfterUpdate
global virtual void onAfterUpdate(Map<Id,sObject> triggerOldMap)
executed to perform AFTER_UPDATE operations
onBeforeDelete
global virtual void onBeforeDelete()
executed to perform BEFORE_DELETE operations
onBeforeInsert
global virtual void onBeforeInsert()
executed to perform BEFORE_INSERT operations
onBeforeUpdate
global virtual void onBeforeUpdate(Map<Id,sObject> triggerOldMap)
executed to perform BEFORE_UPDATE operations
run
Entry point of myTriggers framework - called from implementations
setAllowedTriggerEvents
global static void setAllowedTriggerEvents(Type triggerHandlerType, Boolean forceInit)
loads trigger event settings MyTriggerSetting__mdt
Parameters
- triggerHandlerType
- forceInit - force reload of event settings
setAllowedTriggerEvents
loads trigger event settings MyTriggerSetting__mdt records. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
Parameters
- namespacePrefix
- className
- forceInit - force reload of event settings
toStringEvents
global static Set toStringEvents(System.TriggerOperation[] events)
converts a Set of Event enums into Strings