Using the framework

Because the methods in the TriggerHandler class are ultimately invoked during an Apex trigger context, they have access to the trigger context variables such as Trigger.new and Trigger.old. Additionally, if the context supports it, you'll have access to Trigger.newMap and Trigger.oldMap. While you have access to these collections in the TriggerHandler class, you must cast them to typed collections. This is required because Apex actually returns a collection of generic sObjects rather than collections of sObject subclasses, such as Account, Opportunity and so on. With this in mind, it's a good idea to use the constructor of your trigger handler to set a few class level variables that are precast to the object your handler works with, as follows:

public class AccountTriggerHandler extends triggerHandler {

  List<Account> triggerOld;
  Map<id, Account> triggerNewMap;
  Map<id, Account> triggerOldMap;

  public AccountTriggerHandler(){
    this.triggerNew = (List<Account>) trigger.new;
    this.triggerOld = (List<Account>) trigger.old;

    if(trigger.oldMap != null) {
      this.triggerOldMap = (Map<Id, Account>) trigger.oldMap;
    }
    if(trigger.newMap != null) {
      this.triggerNewMap = (Map<Id, Account>) trigger.NewMap;
    }
  }
}

Aside from encouraging you to clean up your logic and put it in the classes, the framework has a couple of other benefits that make writing triggers within the framework much easier and much more maintainable. Earlier, I talked about re-entrant or circular triggers, these are triggers fired by object 1 that does some sort of DML on object 2 which in turn does DML on object 1 causing the trigger on object 1 to reset and start the cycle all over again. For example, imagine a contact trigger that updates a case, which in turn updates an account, which in turn updates all the account's contacts. This of course would start the cycle over again. To prevent this, the framework has the ability to keep track of how many times the trigger has been fired. Once the threshold for execution has been exceeded, the framework will throw an exception. This is called the max loop count API. To use it, call the setMaxLoopCount(X) method. This method is provided by the framework and accepts one integer argument. When the trigger execution count hits that number, it will throw an exception:

public class ContactTriggerHandler extends TriggerHandler {
  public ContactTriggerHandler() {
    this.setMaxLoopCount(2);
  }
}

Perhaps most importantly, the framework provides a way for developers to selectively, and programmatically disable triggers. The one caveat to this is that the bypassed triggers must also use the framework. Essentially, this allows developers to prevent re-entrant or cyclical triggers by disabling triggers on demand. In other words, if you fire a trigger on an account and you know that that will also result in a contact trigger running, you can bypass the execution of the contact trigger programmatically from your account trigger. This is accomplished with a set of two methods:

TriggerHandler.bypass('AccountTriggerHandler');
TriggerHandler.clearBypass('AccountTriggerHandler');

Whenever you'd like to bypass a trigger's execution, call the TriggerHandler.bypass method with the name of the trigger handler you'd like to bypass. When you've completed your potentially re-entrant DML, call the TriggerHandler.clearBypass() method with the name of the trigger handler you previously bypassed. This feature is incredibly powerful and when combined with the maxLoopCount API, provides a robust way to prevent re-entrant triggers.

Let's take a look at what our example trigger would look like if we wrote it with the trigger framework.

First, this is what the actual trigger source would look like:

trigger AccountTrigger on Account (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
  new AccountTriggerHandler().run();
}

The AccountTriggerHandler class would look like this:

public class AccountTriggerHandler extends triggerHandler {

  List<Account> triggerNew;
  List<Account> triggerOld;
  Map<id, Account> triggerNewMap;
  Map<id, Account> triggerOldMap;

  Public AccountTriggerHandler(){
    this.triggerNew = (List<Account>) trigger.new;
    this.triggerOld = (List<Account>) trigger.old;

    if(trigger.oldMap != null) {
      this.triggerOldMap = (Map<Id, Account>) trigger.oldMap;
    }
    if(trigger.newMap != null) {
      this.triggerNewMap = (Map<Id, Account>) trigger.newMap;
    }
  }

  @testVisible
  Private AccountTriggerHandler(list<Account> newMockAccountsForTesting, list<Account> oldMockAccountsForTesting){
    this.triggerNew = newMockAccountsForTesting;
    this.triggerOld = oldMockAccountsForTesting;
  }

  public override void afterUpdate() {
    doUpdate(updateCasesWithNewContactPhoneIfChanged());
  }

  private void doUpdate(List<Account> toInsert){
    // Always wrap your DML in try/catch blocks.
    try {
      update toInsert;  
    } catch(Exception e) {
      System.debug(e.getMessage());
    }
  }

  @testVisible
  private List<Case> updateCasesWithNewContactPhoneIfChanged(){
   List<Case> affectedCases = [SELECT id, mainContactPhone__c 
          FROM Case 
          WHERE contactId in :triggerNewMap.keyset()];

  // This is the primary data structure we'll use to access our data.
    // A map of contact id's -> to list of cases.
    Map<id, List<Case>> affectedCasesByContactId = new Map<Id,List<Case>>();
    // Now to populate our map
    for(Case c: affectedCases) {
       if(affectedCasesByContactId.keyset().containsKey(c.contactId)){
      affectedCasesByContactId.get(c.contactId).add(c);
      } else {
      affectedCasesByContactId.put(c.contactId, new List<Case>{c});
        }
    }

    // create a new list to hold our newly updated cases 
    // this way we can insert them all at once at the end.
    List<Case> updatedCases = new List<Case>();
    for(Id contactId: trigger.newMap.keyset()) {
      // Trap to make sure that the contact id we're working with actually
      // changed information we care about ie: the phone number.
     if(trigger.old.get(contactId).phone != trigger.new.get(contactId).phone) {
     for(Case thisCase: affectedCasesByContactId.get(contactId)) {
        // Update all the cases to with the new phone number
         thisCase.mainContactPhone__c = trigger.new.get(contactId).phone;
       // add them to our list that we'll update.
          updatedCases.add(thisCase);
        }
      }
    }

    return updatedCases;

  }
}

Finally, our trigger tests can now test the logic of the updateCasesWithNewContactPhoneIfChanged() method without necessitating waiting for DML statements to finish. Our Test class looks like this:

@isTest
private class ProfitStarsUtils_Tests {
  
  @isTest static void test_updateCasesWithNewContactPhoneIfChanged() {
    List<Account> accountsToTestWith = TestUtils.generateListOfAccounts(5);
    List<Account> newAccountsToTestWith = new List<Account>();
    for (Account a: accountsToTestWith) {
     a.phone = '555 867 5300';
    }
    // this invokes the private constructor that's only visible to tests
    AccountTriggerHandler a = new AccountTriggerHandler(newAccountsToTestWith, accountsToTestWith);
    Test.startTest();
    List<Account> results = a.updateCasesWithNewContactPhoneIfChanged();
    Test.stopTest();
    system.assertEquals(results.size(), 5, 'expected to have 5 accounts in collection');
    for (Account a : results) {
      system.assertEquals(a.mainContactPhone__c, '555 867 5309', 'expected the new phone number to be set as the mainContactPhone__c');
    }
  }

}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.205.235