This "framework" is an extension of the TriggerHandler
pattern which original shipped with Mavensmate
The goal of this project is to improve upon that concept, by adding additional configuration and capabilities, while at the same time retaining full backwards compatibility.
- Dynamic Binding of Trigger Handlers via Custom Metadata (similar to table based trigger)
- Ability to disable whole triggers or individual handlers
- Easy to upgrade, full backwards compatibility
- Build in Error Handling & Logging
- Ability to decouple your handlers from the system static
Trigger
context, which allows for better unit testability (no need to actually run DML in tests)
- built in recursion control
- support for other popular framework patterns (sfdc-trigger-framework)
-
Package URL (replace host as needed)
-
command line:
sfdx force:package:install -p 04t1C000000lIX3QAM -u {TARGET ALIAS OR USERNAME} -b 1000 -w 1000
At it's core, the concept is very simple:
- Create a class that
implements YATF.Handler
- Write a
void handle(){}
method to perform the trigger logic - Create a trigger (one trigger per object!) and bind your handlers to the appropriate events.
global class AccountGreeter implements YATF.Handler{
global void handle(){
for(Account acc : (Account[]) Trigger.new){
System.debug('Hello ' + acc.Name);
}
}
}
❓Why the 'global' access modifier❓
- In order to use the dynamic binding through custom metadata, you will need to set your access modifier for the handler classes to 'global'. This is because the trigger manager must actually create instances of your handler classes. Since this code lives inside of the YATF package, it will fail unless the class access is set to 'global'.
- The 'handle' method must be set to global for the same reason. The 'handle' method is being called within the manager code that lives in the YATF package, so without 'global' access the YATF package cannot see that method.
trigger AccountTrigger on Account(before insert){
YATF.Manager m = new YATF.Manager();
AccountGreeter greeter = new AccountGreeter();
m.bind(TriggerOperation.BEFORE_INSERT, greeter);
m.manage();
}
Dynamic binding (or dependency injection) has the advantage of being able to switch out handlers without needed to deploy updates to the Trigger itself.
To do this for the above example:
- update the
.trigger
to fire on all event:
trigger AccountMaster on Account(
after insert,
after update,
after delete,
after undelete,
before insert,
before update,
before delete
) {
new YATF.Manager().manage();
}
WARNING: this is technically not required. The performance impact of binding to all events is untested. If you are concerned, only bind the events you are using!
-
Change the
Handler
access modifier toglobal
-
Setup the Custom Metadata
- navigate to
setup -> Custom Metadata -> Trigger Object -> manage
- create a new Record.
- Set the
Object API Name
field to "Account" - Save
- Set the
- create a new child
Trigger Handler
- Set the
Handler Class
to "AccountGreeter" - Check the
Before Update
box
- Set the
NOTES:
- Dynamic Binding and Static binding can co-exist! Statically bound handlers will execute first.
- You can still configuration for a statically bound trigger. The handler will only execute one time per event, but this allows you to disable a handler or use built in exception handling without having to deploy code.
You can disable the trigger on the entire SObject
or just a single Handler
. Simply un-check the Enabled
field on the respective record.
NOTE: You can disable a static bound trigger by adding a Handler Configuration!
When a trigger throws an uncaught exception, you can choose how it should be handled via the On Exception
field:
Throw
: Just rethrows the error. Will cause the entire trigger to fail. This is the default.Rollback
: Creates aSavepoint
prior to running handler and rolls back the transaction on exception. NOTE: this does NOT rollback changes made directly to the trigger context onBEFORE
triggers.Suppress
: Will onlySystem.Debug
the exception.
Additionally, by setting Create_Exception_Event__c
the system will platform events named Trigger_Handler_Exception__e
any time an uncaught exception is thrown. This event contains details of the thrown exception, the handler that failed and the trigger context.
You can register a listener to automatically run when a Trigger_Handler_Exception__e
occurs by setting the On_Exception_Event_Handler__c
field. The value must be the name of a global
class which implements ExceptionEventHandler
.
You can optionally use On_Exception_Event_Handler_Props__c
to pass json
properties which will be used to initialize the class.
Sends an email with the details of the exception to one or more recipients. Requires the following On_Exception_Event_Handler_Props__c
properties
fromEmailAddress
: The ORG WIDE email address the email will be sent from
recipients
: String[]
of emails to send the exceptions to.
Example JSON:
{
"fromEmailAddress": "[email protected]",
"recipients": ["[email protected]", "[email protected]"]
}
The design is not finalized, but the basic idea is to pass in or inject a proxy to the static Trigger
variable (see TriggerContext
). This will allow you to define your own context during unit tests.
-
Replace all instances of
TriggerHandler.HandlerInterface
withYATF.Handler
-
Replace all instances of
TriggerHandler
class withYATF.Manager
. -
Update all references of
TriggerHandler.Evt
to eitherTriggerOperation
orManager.EVT
(easier to refactor but will be deprecated at some point). -
Delete the
TriggerHandler
class from org
That's it! There should be no functional changes, but it's a good idea to run all unit tests before and after to confirm.
Please do! We will try to incorporate all reasonable ideas.
MIT