6

I'm trying to make work a trigger architecture pattern that was designed by Adam Purkiss, I believe around 2012. I know that there are other more recent designs but I just want to try this one from Adam. I have gone trough every line of the triggers and classes that are part of this design and can't figure out why I received the follow error;

Save Error: Invalid Type:TriggerDispatcher.TriggerParameters

Ok, so this is an overview of the idea behind this pattern. Adam's design suggest to build a trigger that will control other triggers in a kind of dispatch and execution controlled approach. The main trigger is what he calls the "TriggerDispatcher", code bellow:

public with sharing class TriggerDispatcher 
{

    private static final String SACCOUNT = 'Account'; 
    private static final String SCONTACT = 'Contact';
    private static final String SLEAD = 'Lead';
    private static final String SOPPORTUNITY = 'Opportunity';
    private static final String SUSER = 'User'; 


    public static ITriggerEntry activeInstance = null; 
    public static Map<Id, SObject> sObjectsToUpdate = new Map<Id, SObject>(); 


    public interface ITriggerEntry
    {
        void mainEntry(TriggerParameters tp);
        void inProgressEntry(TriggerParameters tp);     

    }

    public class TriggerParameters
    {
        public String triggerObject {get; private set;}
        public Boolean isBefore {get; private set;}
        public Boolean isAfter {get; private set;}
        public Boolean isDelete {get; private set;}
        public Boolean isInsert {get; private set;}
        public Boolean isUpdate {get; private set;}
        public Boolean isExecuting {get; private set;}
        public List<SObject> oldList {get; private set;}
        public List<SObject> newList {get; set;}
        public Map<Id, SObject> oldMap {get; private set;}
        public Map<Id, SObject> newMap {get; set;}

        public TriggerParameters(Boolean ib, Boolean ia, Boolean id, Boolean ii, Boolean iu, Boolean ie,
            List<SObject> ol, List<SObject> nl, Map<Id, SObject> om, Map<Id, SObject> nm )
            {
                this.isBefore = ib;
                this.isAfter = ia;
                this.isDelete = id;
                this.isInsert = ii;
                this.isUpdate = iu;
                this.isExecuting = ie;
                this.oldList = ol; 
                this.newList = nl;
                this.oldMap = om;
                this.newMap = nm;
                this.triggerObject = getSObjType((this.oldList != null && this.oldList.size() > 0) ? 
                this.oldList[0] : this.newList[0]);
            } 

            private String getSObjType(SObject so)
            {
                String retVal; 
                if(so instanceof Account) retVal = SACCOUNT; 
                else if( so instanceof Contact) retVal = SCONTACT; 
                else if( so instanceof Lead) retVal = SLEAD; 
                else if( so instanceof Opportunity) retVal = SOPPORTUNITY;
                else if( so instanceof User) retVal =SUSER;
                return retVal; 

            }
    }

    //Central dispatch entry
    private static void entry(TriggerParameters tp)
    {
        if(activeInstance == null)
            processWork(tp);
            else
            activeInstance.inProgressEntry(tp);

    }

    //Order of execution is controlled

    private static void processWork(TriggerParameters sp)
    {
        if(tp.triggerObject == SCONTACT && tp.isAfter && tp.isUpdate)
        {

            execute(new SyncContactToUser(), tp);
        }
        else if(tp.triggerObject == SUSER && tp.isAfter && tp.isUpdate)
            execute(new SyncUserToContact(), tp);

            if(sObjectToUpdate.size() >0)
                update sObjectToUpdate.values();
    }

    private static void execute(ITriggerEntry ite, TriggerParameters tp)
    {

        activeInstance = ite;
        activeInstance.mainEntry(tp);
    }

}

The TriggerDispatcher contains the logic that will help us to control the execution order among other things. There is an interface inside this trigger; ITriggerEntry. And there is a subclass as well TriggerParameters.

In Adam's example for this design he wants to make sure that if there is a contact and user connected to the same person, that when an update is executed to a field, that is part of a field group in the SyncUserToContact and SyncContactToUser classes , that this update will be done to both records, User record and Contact record. The connection between these objects is done with a User__c relationship field on the contacts page layout that point the User object.

The entry point for User and Contact records to the TriggerDispatcher is done with an independent trigger for each object. These triggers are all the same, see bellow:

trigger MainContactEntry on Contact (after delete, after insert, after update, 
before delete, before insert, before update)
{

TriggerDispatcher.entry(new TriggerDispatcher.TriggerParameters (Trigger.isBefore, Trigger.isAfter, Trigger.isDelete, Trigger.isInsert, Trigger.isUpdate, Trigger.isExecuting, Trigger.old, Trigger.new,Trigger.oldMap, Trigger.newMap));
}

There is another trigger exactly the same but for the User object. In both triggers I get the error I mentioned before;

Save Error: Invalid Type:TriggerDispatcher.TriggerParameters , on line 4.

There are also two classes that implement the TriggerDispatcher.ITriggerEntry. They contain the logic for our triggers. In Adam's example he used two classes that implement the interface. SyncContactToUser and SyncUserToContact.

Here is the code for the SyncContactToUser:

public with sharing class SyncContactToUser implements TriggerDispatcher.ITriggerEntry
{

public void inProgressEntry(TriggerDispatcher.TriggerParameters tp)
{
    //Confirm caller origin
    system.assert(tp.triggerObject == 'User');
}

public void mainEntry(TriggerDispatcher.TriggerParameters tp)
{
    Map<id, Contact> newMap = (Map<id, Contact>) tp.newMap; 
    List<Id> userIds = new List<Id>();
    for(Contact c: newMap.values())
        userIds.add(c.User__c);


    Map<Id, User> userMap = new Map<Id, User>([SELECT Id, Phone, MobilePhone, Fax FROM User WHERE Id IN :userIds]);

    for(Contact c : newMap.values())
    {
        User u = userMap.get(c.User__c);
        if(u.Phone != c.Phone || u.MobilePhone != c.MobilePhone || u.Fax != c.Fax)
        {
            u.Email = c.Email;
            u.Phone = c.Phone;
            u.MobilePhone = c.MobilePhone;
            u.Fax = c.Fax; 
            TriggerDispatcher.sObjectsToUpdate.put(u.id, u);

        }
      }
   }
 }

Now the SyncUserToContact:

public with sharing class SyncUserToContact implements TriggerDispatcher.ITriggerEntry
{

public void inProcessEntry(TriggerDispatcher.TriggerParameters tp)
{
    //Confirm caller origin
    system.assert(tp.triggerObject == 'Contact');

}

public void mainEntry(TriggerDispatcher.TriggerParameters tp)
{

    Map<Id, User> newMap = (Map<Id, User>) tp.newMap;
    List<Contact> contactList = [SELECT User__c, Phone, MobilePhone, Fax FROM Contact WHERE User__c IN :newMap.keyset()]; 

    for(Contact c: contactList)
    {

        User u = newMap.get(c.User__c);
        if(c.Phone !=u.Phone || c.MobilePhone != u.MobilePhone || c.Fax != u.Fax)
        {

            c.Phone =u.Phone;
            c.MobilePhone = u.MobilePhone;
            c.Fax = u.Fax;
            TriggerDispatcher.sObjectsToUpdate.put(c.Id, c);
        }
      }
    }
  }

In both classes SyncUserToContact and SyncContactToUser the error points out the TriggerDispatcher.TriggerParameters on line 4.

So my question is, how can I fix this? because the code is from 2012 it may be that there is some architecture changes needed in order for this design to work. I will really appreciated if anyone could give me some directions on how to get this structure to work.

Thanks!

  • Is it run time error or compile time error? – Mohith Shrivastava May 16 '15 at 16:09
  • Compile Error. I get this error on Eclipse or when I try to save the code directly on SFDC. So it is a compile error. – Carlos Naranjo May 16 '15 at 17:09
  • Is it when you save class or trigger? – Mohith Shrivastava May 16 '15 at 17:12
10

You are probably getting a compile error based on circular dependencies. Try commenting out all your references in TriggerDispatcher to both SyncUserToContact and SyncContactToUser. Once your TriggerDispatcher class saves, the types you need will be defined. and you should be able to save all your classes.

Even saving the file alone won't do the trick entirely though, because you've got a parameter in your processWork function named sp, which you assume in the function body is tp. There may be other errors, but just focus on getting TriggerDispatcher to save.

A few critiques about the code itself

Storing the map of sObjectsToUpdate statically seems like a huge design flaw. If you are using this in an org with complicated cross-object architecture, you don't want an update to User leaking into the context for an update to Contact (for example).

Your if/else chain in processWork is going to get monstrously unwieldy in a hurry as you add support for events/objects. If you want a central dispatcher like this, your life will still be easier to have more granular dispatchers that help on a per object basis. Then you can just have a map of sObjectType to dispatcher.

A simple interface

global interface ITriggerHandler
{
    void beforeInsert();
    void beforeUpdate();
    void beforeDelete();
    void afterInsert();
    void afterUpdate();
    void afterDelete();
    void afterUndelete();
}

Optionally an abstract class so you only have to implement what you want.

public abstract class TriggerHandler
{
    public abstract void beforeInsert();
    public abstract void beforeUpdate();
    public abstract void beforeDelete();
    public abstract void afterInsert();
    public abstract void afterUpdate();
    public abstract void afterDelete();
    public abstract void afterUndelete();
}

And a repository type file

public with sharing class TriggerHandlers
{
    public static Map<Schema.SObjectType, ITriggerHandler> bySObjectType =
        new Map<Schema.SObjectType, ITriggerHandler>
        {
            Account.sObjectType => new AccountTriggerHandler(),
            Contact.sObjectType => new ContactTriggerHandler()
        }
}

And then all your logic would be in your specific handler implementations.

public with sharing class AccountTriggerHandler
    extends TriggerHandler implements ITriggerHandler
{
    public override void afterInsert()
    {
        //put your logic here.
    }
}

Style

These are fairly nitpicky and won't hurt your code to ignore.

The following syntax

public static ITriggerEntry activeInstance = null;

is functionally identical to

public static ITriggerEntry activeInstance;

You're not really setting it so it is probably better to not assign it.

I also prefer syntax like:

public static final String SACCOUNT = String.valueOf(Account.sObjectType);

over

private static final String SACCOUNT = 'Account';

There's no reason to hide your constants. They are immutable. Also it won't matter with standard objects but if you add support for custom objects, adding such references in the code ensures they cannot be deleted or have their API Names modified.

  • 1
    i'm also not nuts about TriggerDispatcher.processWork() - this method will need to be endlessly modified as new trigger use cases for different sobjects arise. – cropredy May 16 '15 at 17:42
  • 1
    @crop1645 And horrifying to read through as it inevitably grows upwards of 500 lines. – Adrian Larson May 16 '15 at 17:43
  • Thanks guys, it is save now... still not working on dev :( , but they are save. I also found that the private static void entry(TriggerParameters tp) was giving me a different error because either MainContactEntry or MainUserEntry were able to access that method because was on private. Thanks for the tips, I will try to modify this code to get something more solid. But is good for a starting point, right? – Carlos Naranjo May 16 '15 at 18:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.