Trigger & Orchestrator
The trigger passes an orchestrator to TriggerOrchestrator.run(…). The orchestrator lists the handlers of each context in run order.
Trigger
trigger
trigger AccountTrigger on Account(before insert, after insert, before update, after update, before delete, after delete, after undelete) {
TriggerOrchestrator.run(new AccountTriggerOrchestrator());
}- List all seven events. A context the orchestrator does not implement does nothing. Add handlers later without touching the trigger.
- Put nothing else in the body. Code next to
run(…)ignores bypasses, and its errors never reach the Logger. - One trigger per object. Salesforce does not guarantee the order of several triggers on one object.
- Only inside a trigger. Called anywhere else,
run(…)throws aTriggerTypes.TriggerLibException.
Orchestrator
Implement one registration interface per context, such as TriggerOrchestrator.BeforeInsert. Its method, such as beforeInsertHandlers(), returns the handlers.
cls
public with sharing class AccountTriggerOrchestrator implements TriggerOrchestrator.BeforeInsert, TriggerOrchestrator.AfterInsert, TriggerOrchestrator.BeforeUpdate, TriggerOrchestrator.AfterUpdate {
public List<BeforeInsert.Handler> beforeInsertHandlers() {
return new List<BeforeInsert.Handler>{
new AccountParentDefaultsPopulator(),
new AccountRatingPopulator(),
new AccountShippingAddressPopulator(),
new AccountCustomerDataValidator(),
new AccountParentTypeValidator()
};
}
public List<BeforeUpdate.Handler> beforeUpdateHandlers() {
return new List<BeforeUpdate.Handler>{
new AccountRatingRefreshPopulator(),
new AccountShippingSyncPopulator(),
new AccountTypeAuditPopulator(),
new AccountDemotionValidator(),
new AccountBillingCountryValidator(),
new AccountParentMoveValidator()
};
}
public List<AfterInsert.Handler> afterInsertHandlers() {
return new List<AfterInsert.Handler>{ new AccountWelcomeTaskWriter() };
}
public List<AfterUpdate.Handler> afterUpdateHandlers() {
return new List<AfterUpdate.Handler>{ new AccountAddressCascadeWriter(), new AccountOwnerTransferWriter() };
}
}- List classes that implement a role. A class that implements only the context's
Handlerinterface compiles and never runs. - One role per class. A class that implements both roles of a context runs only as the first: Populator over Validator, Validator over Writer, Writer over Dispatcher.
Handler Order
- List order is run order, whatever the role. Each handler finishes before the next one starts.
- Later handlers see earlier changes. List Populators before Validators.
- Writers commit last. By default, the Writers' DML commits once, after the last handler.
One Class in Several Contexts
apex
public with sharing class ContactBirthdateRangeValidator implements BeforeInsert.Validator, BeforeUpdate.Validator {
private static final String MESSAGE = 'Birthdate must be in the past.';
public Boolean addErrorOnBeforeInsertWhen(TriggerTypes.InsertRecord record) {
return this.isInFuture((Contact) record.getNewSObject());
}
public void addErrorOnBeforeInsert(TriggerTypes.RejectableInsertRecord record) {
record.addError(Contact.Birthdate, ContactBirthdateRangeValidator.MESSAGE);
}
public Boolean addErrorOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) {
return record.isChanged(Contact.Birthdate) && this.isInFuture((Contact) record.getNewSObject());
}
public void addErrorOnBeforeUpdate(TriggerTypes.RejectableUpdateRecord record) {
record.addError(Contact.Birthdate, ContactBirthdateRangeValidator.MESSAGE);
}
private Boolean isInFuture(Contact contactRecord) {
return contactRecord.Birthdate != null && contactRecord.Birthdate > Date.today();
}
}- Method names carry the context, so they never clash.
- Register the class in each context's list.
- Add-ons are per context too, such as
BeforeInsert.ParentQueryandBeforeUpdate.ParentQuery. - Bypasses cover every context. A
TriggerHandler__mdtrecord switches the class off in every context of its object. To skip one context, use that context's Bypassable.
Instances per Chunk
The library calls the handler list method on every run: per context, per chunk of up to 200 records and per nested run.
- Instance fields reset on every run when the list returns
newhandlers. - Static fields last the whole transaction.
- A static "already ran" flag skips later chunks. Gate the predicate on a change such as
isChangedinstead, or use RecursionGuard in the update contexts. - Constructor arguments are allowed. They are evaluated on every run, so keep SOQL out of them.
Before to After Handoff
Stamp a decision on the record in a BeforeUpdate Populator. Let an AfterUpdate Writer or Dispatcher qualify on the stamp:
apex
public with sharing class AccountHotRatingUpdatePopulator implements BeforeUpdate.Populator {
public Boolean populateOnBeforeUpdateWhen(TriggerTypes.UpdateRecord record) {
return record.isChanged(Account.AnnualRevenue) && record.greaterThanOrEqualTo(Account.AnnualRevenue, 1000000);
}
public void populateOnBeforeUpdate(TriggerTypes.UpdateRecord record) {
record.put(Account.Rating, 'Hot');
}
}apex
public with sharing class AccountHotRatingTaskWriter implements AfterUpdate.Writer {
public Boolean writeOnAfterUpdateWhen(TriggerTypes.UpdateRecord record) {
return record.isChangedTo(Account.Rating, 'Hot');
}
public void writeOnAfterUpdate(TriggerTypes.UpdateRecord record, TriggerTypes.UnitOfWork unitOfWork) {
unitOfWork.toInsert(new Task(WhatId = record.getId(), Subject = 'Call the account: its rating is now Hot'));
}
}- The stamp is saved with the record, so
isChangedTosees it after the save. - Manual edits count too. When only the Populator's decision should count, stamp a field users cannot edit.
