meruff / ApexTriggerHandler

Another library implements Apex trigger handler design pattern.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Apex Trigger Handler

 

There are already many trigger handler libraries out there, but this one has some different approaches or advantanges such as state sharing, built in helper methods etc.. Just one class Triggers.cls with its corresponding test class TriggersTest.cls, and its minimal and simple.


Release 1.1.3

Breaking changes to Triggers.Props Methods: All filterChanged() methods now return List<Id>, instead of Set<Id>. This is because List<Id> has wider use cases, it can not only be used in a where condition, but can also be used in an iteration.


Features

  1. Share common query results via context.state with the following handlers in the current trigger execution context.
  2. Built-in helpers to perform common operations on trigger properties, such as detect field changes.
  3. Control flow of handler execution with context.next(), context.stop(), and context.skips.

Usage

To create a trigger handler, you will need to create a class that implements the Triggers.Handler interface and its criteria method, and the corresponding trigger event method interfaces, such as the Triggers.BeforeUpdate interface and its beforeUpdate method.

public class MyAccountHandler implements Triggers.Handler, Triggers.BeforeUpdate {
    public Boolean criteria(Triggers.Context context) {
        return true;
    }

    public void beforeUpdate(Triggers.Context context) {
        // do stuff
    }
}

Trigger

As you have noticed, why we are creating same handlers for different trigger events? This is because handlers may need to execute in different orders for different trigger events, we need to provide developers great controls over the order of executions.

trigger AccountTrigger on Account (before update, after update) {
    Triggers.prepare()
        .beforeUpdate()
            .bind(new MyAccountHandler())
            .bind(new AnotherAccountHandler())
        .afterUpdate()
            .bind(new AnotherAccountHandler())
            .bind(new MyAccountHandler())
        .execute();
}

Trigger Handler

Please check the comments below for detailed explanations and tricks to customize a trigger handler.

// 1. Use interfaces instead of a base class to extend a custom handler. With interface
// approach we can declare only the needed interfaces explicitly, which is much cleaner
// and clearer.
public class MyAccountHandler implements Triggers.Handler,
                                         Triggers.BeforeUpdate,
                                         Triggers.AfterUpdate {

    // 2. There is a "criteria" stage before any handler execution. This gives
    // developers chances to turn on and off the handlers according to
    // configurations at run time.
    public Boolean criteria(Triggers.Context context) {
        return Triggers.WHEN_ALWAYS;

        // 3. There are also helper methods to check if certain fields have changes
        // return context.props.isChangedAny(Account.Name, Account.Description);
        // return context.props.isChangedAll(Account.Name, Account.Description);
    }

    public void beforeUpdate(Triggers.Context context) {
        then(context);
    }

    public void afterUpdate(Triggers.Context context) {
        then(context);
    }

    private void then(Triggers.Context context) {
        // 4. All properties on Trigger have been exposed to context.props.
      	// Direct reference of Trigger.old and Trigger.new can be avoided,
        // instead use context.props.oldList and context.props.newList.
        if (context.props.isUpdate) {

            // 5. Use context.state to pass query or computation results down to all
            // following handlers within the current trigger context, i.e. before update.
            Integer counter = (Integer)context.state.get('counter');
            if (counter == null) {
                context.state.put('counter', 0);
            } else {
                context.state.put('counter', counter + 1);
            }

            // 6. Use context.skips or Triggers.skips to prevent specific handlers from
            // execution. Please do remember restore the handler when appropriate.
            context.skips.add(ContactHandler.class);
            List<Contact> contacts = ...;
            Database.insert(contacts);
            context.skips.remove(ContactHandler.class);

            // 7-1. Call context.next() to execute the next handler. It is optional to use,
            // unless some following up logics need to be performed after all following
            // handlers finished.
            context.next();

            // 7-2. If context.stop() is called instead of context.next(), any following
            // handlers won't be executed, just like the STOP in process builder.
            context.stop();
        }
    }
}

More on Skips

context.skips references the same global static variable Triggers.skips. If you want to skip handlers in contexts rather than a trigger handler. Please use Triggers.skips instead. For example, when you want to skip a trigger handler in a batch class:

global class AccountUpdateBatch implements Database.Batchable<sObject> {
    ...
    global void execute(Database.BatchableContext BC, List<sObject> scope){
        Triggers.skips.add(MyAccountHandler.class);
        // Update accounts...
        Triggers.skips.remove(MyAccountHandler.class);
    }
    ...
}

Or you can skip the handler during batch execution in the criteria phase:

public class MyAccountHandler implements Triggers.Handler, Triggers.BeforeUpdate {
    public Boolean criteria(Triggers.Context context) {
        return !System.isBatch();
    }
    ...
}

Unit Test How-To

The following method is private but @TestVisible, it can be used in test methods to supply mock recoreds for old and new lists. So we don't need to perform DMLs to trigger the real triggers.

List<SObject> oldList = new List<SObject> {
    new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'Old Name 1'),
    new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'Old Name 2'),
    new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'Old Name 3')}

List<SObject> newList = new List<SObject> {
    new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 1), Name = 'New Name 1'),
    new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 2), Name = 'New Name 2'),
    new Account(Id = TriggersTest.getFakeId(Account.SObjectType, 3), Name = 'New Name 3')}

Triggers.prepare(TriggerOperation.Before_Update, oldList, newList)
    .beforeUpdate()
        .bind(new MyAccountHandler())
    .execute();

Trigger Switch

You can turn individual trigger handlers or triggers for entire SObjectTypes off completely with the use of a custom metadata type Trigger_Switch__mdt or programmatically through the TriggerSwitch class. The custom setting also includes a field for specifying on/off for a specific User's email address in a semi-colon delimited list. See User_Emails_to_Skip__c.

This is handy for integrations to skip automation that isn't necessary for basic data loads, or times when you need to deactivate specific functionality or the entire trigger easily.

Programmatically

// specific sobject
TriggerSwitch triggerSwitch = new TriggerSwitch(Custom_Object__c.getSObjectType());
triggerSwitch.turnOff('Custom_Object__c');
...
triggerSwitch.turnOn('Custom_Object__c');

// or specific handler
TriggerSwitch triggerSwitch = new TriggerSwitch(Account.getSObjectType());
triggerSwitch.turnOff('MyAccountHandler');
...
triggerSwitch.turnOn('MyAccountHandler');

Custom Metadata

You can create a custom metadata setting for either SObject or trigger handler and individually turn them on or off. If one does not exist for the SObject or trigger handler, that trigger will just continue to work.

Context Name Is Active User Emails to Skip
Contact false integrations.user@nomail.com
MyAccountHandler true
MyContactValidationHandler true integrations.user@nomail.com;my.admin.user@nomail.com

APIs

Trigger Handler Interfaces

Interface Method to Implement
Triggers.Handler Boolean criteria(Triggers.Context context);
Triggers.BeforeInsert void beforeInsert(Triggers.Context context);
Triggers.AfterInsert void afterInsert(Triggers.Context context);
Triggers.BeforeUpdate void beforeUpdate(Triggers.Context context);
Triggers.AfterUpdate void afterUpdate(Triggers.Context context);
Triggers.BeforeDelete void beforeDelete(Triggers.Context context);
Triggers.AfterDelete void afterDelete(Triggers.Context context);
Triggers.BeforeUndelete void afterUndelete(Triggers.Context context);

Triggers.Context

Property/Method Type Description
context.props Triggers.Props All properties on Trigger are exposed by this class. In addition there are frequently used helper methods and a convinient sObjectType property, in case reflection is needed .
context.state Map<Object, Object> A map provided for developers to pass any value down to other handlers.
context.skips Triggers.Skips A set to store handlers to be skipped. Call the following methods to manage skips: context.skips.add(), context.skips.remove(), context.skips.clear() context.skips.contains() etc.
context.next() void Call the next handler.
context.stop() void Stop execute any following handlers. A bit like the the stop in process builders.

Triggers.Props

Triggers.Props Properties

Property Type Description
sObjectType SObjectType The current SObjectType.
isExecuting Boolean Trigger.isExecuting
isBefore Boolean Trigger.isBefore
isAfter Boolean Trigger.isAfter
isInsert Boolean Trigger.isInsert
isUpdate Boolean Trigger.isUpdate
isDelete Boolean Trigger.isDelete
isUndelete Boolean Trigger.isUndelete
oldList List<SObject> Trigger.old
oldMap Map<Id, SObject> Trigger.oldMap
newList List<SObject> Trigger.new
newMap Map<Id, SObject> Trigger.newMap
operationType TriggerOperation Trigger.operationType
size Integer Trigger.size

Triggers.Props Methods

Note: the following isChanged method has the same behavior has the ISCHANGED formula:

  • This function returns false when evaluating any field on a newly created record.
  • If a text field was previously blank, this function returns true when it contains any value.
  • For number, percent, or currency fields, this function returns true when:
    • The field was blank and now contains any value
    • The field was zero and now is blank
    • The field was zero and now contains any other value
Method Type Description
- isChanged(SObjectField field1) Boolean Check if any record has a field changed during an update.
- isChangedAny(SObjectField field1, SObjectField field2)
- isChangedAny(SObjectField field1, SObjectField field2, SObjectField field3)
- isChangedAny(List<SObjectField> fields)
Boolean Check if any record has multiple fields changed during an update. Return true if any specified field is changed.
- isChangedAll(SObjectField field1, SObjectField field2)
- isChangedAll(SObjectField field1, SObjectField field2, SObjectField field3)
- isChangedAll(List<SObjectField> fields)
Boolean Check if any record has multiple fields changed during an update. Return true only if all specified fields are changed.
- filterChanged(SObjectField field1) List<Id> Filter IDs of records have a field changed during an update.
- filterChangedAny(SObjectField field1, SObjectField field2)
- filterChangedAny(SObjectField field1, SObjectField field2, SObjectField field3)
- filterChangedAny(List<SObjectField> fields)
List<Id> Filter IDs of records have mulantiple fields changed during an update. Return IDs if any specified field is changed.
- filterChangedAll(SObjectField field1, SObjectField field2)
- filterChangedAll(SObjectField field1, SObjectField field2, SObjectField field3)
- filterChangedAll(List<SObjectField> fields)
List<Id> Filter IDs of records have mulantiple fields changed during an update. Return IDs only if all specified fields are changed.

License

BSD 3-Clause License

About

Another library implements Apex trigger handler design pattern.

License:BSD 3-Clause "New" or "Revised" License


Languages

Language:Apex 100.0%