Your First Handler
In this tutorial you lowercase Contact emails and reject impossible birthdates on insert. You add a trigger, an orchestrator, a Populator and a Validator, then test them without DML.
First, install Trigger Lib in a scratch org or sandbox.
1. Add the Trigger
Each object gets one trigger with a one-line body:
trigger ContactTrigger on Contact(before insert, after insert, before update, after update, before delete, after delete, after undelete) {
TriggerOrchestrator.run(new ContactTriggerOrchestrator());
}List all seven events now. A context the orchestrator does not implement does nothing, so you can add handlers later without touching the trigger.
2. Add the Orchestrator
The orchestrator lists the handlers of each context in run order. For before insert, implement TriggerOrchestrator.BeforeInsert and return the handlers from beforeInsertHandlers():
public with sharing class ContactTriggerOrchestrator implements TriggerOrchestrator.BeforeInsert {
public List<BeforeInsert.Handler> beforeInsertHandlers() {
return new List<BeforeInsert.Handler>{ new ContactEmailNormalizationPopulator(), new ContactBirthdateValidator() };
}
}List Populators before Validators, so the Validators check the values the Populators set.
3. Write a Populator
A Populator changes the record being saved. Its predicate picks the records, and its action changes them:
public with sharing class ContactEmailNormalizationPopulator implements BeforeInsert.Populator {
public Boolean populateOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
return record.isNotBlank(Contact.Email);
}
public void populateOnBeforeInsert(TriggerHandler.InsertRecord record) {
Contact contactRecord = (Contact) record.getNewSObject();
record.put(Contact.Email, contactRecord.Email.trim().toLowerCase());
}
}populateOnBeforeInsertWhenruns for each record. When it returns true,populateOnBeforeInsertruns for that record.record.put(field, value)sets the field on the row being saved. No DML is needed.
See BeforeInsert.Populator and the Record API.
4. Write a Validator
A Validator rejects records. Its predicate finds the invalid records, and its error method attaches the error:
public with sharing class ContactBirthdateValidator implements BeforeInsert.Validator {
public Boolean errorShouldBeAttachedOnBeforeInsertWhen(TriggerHandler.InsertRecord record) {
if (record.isNull(Contact.Birthdate)) {
return false;
}
return record.greaterThan(Contact.Birthdate, Date.today()) || record.lessThan(Contact.Birthdate, Date.today().addYears(-120));
}
public void addErrorOnBeforeInsert(TriggerHandler.RejectableInsertRecord record) {
record.addError(Contact.Birthdate, 'Birthdate must be in the past and less than 120 years ago. Check the year.');
}
}record.addError(message)attaches a record error.record.addError(field, message)attaches it to a field.- When the predicate returns true,
addErrorOnBeforeInsertmust attach an error. Otherwise the library throws and the save fails.
5. Try It
Run this in anonymous Apex:
Contact jane = new Contact(LastName = 'Doe', Email = 'Jane.Doe@Example.COM');
insert jane;
System.debug([SELECT Email FROM Contact WHERE Id = :jane.Id].Email);The log shows jane.doe@example.com. Now insert a contact born tomorrow:
insert new Contact(LastName = 'Roe', Email = 'rick.roe@example.com', Birthdate = Date.today().addDays(1));The insert fails with a DmlException that carries the Validator's message.
6. Test It
Handlers are plain classes. Call their methods with rows built in memory: no trigger, no DML. Wrap a row in new TriggerHandler.TriggerRecord(newRow, oldRow), with null for the old row on insert.
@IsTest
private class ContactEmailNormalizationPopulatorTest {
@IsTest
static void populateOnBeforeInsertWithMixedCaseEmail() {
// Setup
Contact newContact = new Contact(LastName = 'Doe', Email = 'Jane.Doe@Example.COM');
// Test
new ContactEmailNormalizationPopulator().populateOnBeforeInsert(new TriggerHandler.TriggerRecord(newContact, null));
// Verify
Assert.areEqual('jane.doe@example.com', newContact.Email, 'The email should be lowercase.');
}
}@IsTest
private class ContactBirthdateValidatorTest {
@IsTest
static void addErrorOnBeforeInsertOnBirthdate() {
// Setup
Contact newContact = new Contact(LastName = 'Doe', Birthdate = Date.today().addDays(1));
// Test
new ContactBirthdateValidator().addErrorOnBeforeInsert(new TriggerHandler.TriggerRecord(newContact, null));
// Verify
Assert.areEqual(new List<String>{ 'Birthdate' }, newContact.getErrors()[0].getFields(), 'The error should be on Birthdate.');
}
}Test the predicates the same way and assert the Boolean. More in Testing.
Next Steps
- Read Account fields from the contact: BeforeInsert.ParentQuery.
- Change other records after the save: AfterInsert.Writer.
