├── TriggerX ├── salesforce.schema ├── src │ ├── classes │ │ ├── TriggerX.cls-meta.xml │ │ ├── TriggerXTest.cls-meta.xml │ │ ├── AccountSampleHandler.cls-meta.xml │ │ ├── AccountSampleHandler.cls │ │ ├── TriggerXTest.cls │ │ └── TriggerX.cls │ ├── triggers │ │ ├── AccountSample.trigger-meta.xml │ │ └── AccountSample.trigger │ ├── package.xml │ └── objects │ │ └── TRIGGER_CONTROL__c.object └── .project ├── .gitignore └── README.md /TriggerX/salesforce.schema: -------------------------------------------------------------------------------- 1 | place holder -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .settings/ 3 | 4 | .DS_Store* 5 | ehthumbs.db 6 | Icon? 7 | Thumbs.db -------------------------------------------------------------------------------- /TriggerX/src/classes/TriggerX.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /TriggerX/src/classes/TriggerXTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /TriggerX/src/classes/AccountSampleHandler.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /TriggerX/src/triggers/AccountSample.trigger-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /TriggerX/src/triggers/AccountSample.trigger: -------------------------------------------------------------------------------- 1 | /** 2 | * TriggerX by Sebastian Wagner 2013 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted. 5 | * 6 | * Sample Trigger for TriggerX implementation 7 | * http://github.com/sebwagner/TriggerX 8 | */ 9 | trigger AccountSample on Account (after update, before update) { 10 | 11 | TriggerX.handleTrigger(AccountSampleHandler.class); 12 | 13 | } -------------------------------------------------------------------------------- /TriggerX/src/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AccountSampleHandler 5 | TriggerX 6 | TriggerXTest 7 | ApexClass 8 | 9 | 10 | AccountSample 11 | ApexTrigger 12 | 13 | 14 | * 15 | CustomObject 16 | 17 | 27.0 18 | 19 | -------------------------------------------------------------------------------- /TriggerX/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | TriggerX 4 | 5 | 6 | 7 | 8 | 9 | com.salesforce.ide.builder.default 10 | 11 | 12 | 13 | 14 | com.salesforce.ide.builder.online 15 | 16 | 17 | 18 | 19 | 20 | com.salesforce.ide.nature.default 21 | com.salesforce.ide.nature.online 22 | 23 | 24 | -------------------------------------------------------------------------------- /TriggerX/src/objects/TRIGGER_CONTROL__c.object: -------------------------------------------------------------------------------- 1 | 2 | 3 | List 4 | Public 5 | Control trigger event execution. Save settings by the name of the class you want to control. If now record exists, all trigger events are considered as enabled 6 | false 7 | false 8 | 9 | AFTER_DELETE__c 10 | true 11 | false 12 | Uncheck to turn off AFTER DELETE 13 | 14 | Checkbox 15 | 16 | 17 | AFTER_INSERT__c 18 | true 19 | false 20 | Uncheck to disable AFTER INSERT 21 | 22 | Checkbox 23 | 24 | 25 | AFTER_UNDELETE__c 26 | true 27 | false 28 | Uncheck to disable AFTER UNDELETE 29 | 30 | Checkbox 31 | 32 | 33 | AFTER_UPDATE__c 34 | true 35 | false 36 | Uncheck to disable AFTER UPDATE 37 | 38 | Checkbox 39 | 40 | 41 | BEFORE_DELETE__c 42 | true 43 | false 44 | Uncheck to disable BEFORE DELETE 45 | 46 | Checkbox 47 | 48 | 49 | BEFORE_INSERT__c 50 | true 51 | false 52 | Uncheck to disable BEFORE_INSERT 53 | 54 | Checkbox 55 | 56 | 57 | BEFORE_UPDATE__c 58 | true 59 | false 60 | Uncheck to disable BEFORE UPDATE 61 | 62 | Checkbox 63 | 64 | 65 | DESCRIPTION__c 66 | false 67 | 68 | false 69 | TextArea 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /TriggerX/src/classes/AccountSampleHandler.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * TriggerX by Sebastian Wagner 2013 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted. 5 | * 6 | * Sample Implementation of a TriggerHandler for TriggerX 7 | * http://github.com/sebwagner/TriggerX 8 | */ 9 | public class AccountSampleHandler extends TriggerX { 10 | 11 | 12 | public override void onBeforeUpdate(Map triggerOldMap){ 13 | 14 | Set updatedIds = TriggerX.getUpdatedIds(); 15 | 16 | for (Account record:(Account[])records) 17 | { 18 | // skip records which have been updated within the current context 19 | if (updatedIds.contains(record.Id)) continue; 20 | 21 | } 22 | // more logic goes here 23 | } 24 | 25 | 26 | // Method executed to perform AFTER_UPDATE operations, overwrite if applicable 27 | public override void onAfterUpdate(Map triggerOldMap){ 28 | 29 | Map withAddressUpdate = new Map(); 30 | 31 | sObjectField[] addressFields = new sObjectField[]{ 32 | Account.BillingStreet 33 | , Account.BillingPostalCode 34 | , Account.BillingCity 35 | , Account.BillingCountry}; 36 | 37 | // use prefiltered list 38 | for (Account record:(Account[])getNonRecursiveUpdates()) 39 | { 40 | // check if the address has change and keep record for further processing if so 41 | if (TriggerX.hasChangedFields(addressFields, record, triggerOldMap.get(record.Id))) 42 | { 43 | withAddressUpdate.put(record.Id,record); 44 | } 45 | } 46 | 47 | // track updated ids to prevent recursive updates 48 | TriggerX.addUpdatedIds(triggerOldMap.keySet()); 49 | 50 | if (withAddressUpdate.size() > 0) 51 | { 52 | updateContactAddressFromAccount(withAddressUpdate); 53 | } 54 | 55 | // more logic goes here 56 | } 57 | 58 | 59 | private void updateContactAddressFromAccount(Map accountMap){ 60 | 61 | Contact[] contacts = [ 62 | select Id 63 | , AccountId 64 | from Contact 65 | where AccountId IN: accountMap.keySet()]; 66 | 67 | for (Contact record:contacts) 68 | { 69 | 70 | Account acc = accountMap.get(record.AccountId); 71 | 72 | record.MailingCity = acc.BillingCity; 73 | record.MailingCountry = acc.BillingCountry; 74 | record.MailingPostalCode = acc.BillingPostalCode; 75 | record.MailingStreet = acc.BillingStreet; 76 | } 77 | 78 | // disable Account updates before saving the contacts, so we dont have to run it twice 79 | TriggerX.disable(AccountSampleHandler.class, TriggerX.getUpdateEvents()); 80 | 81 | update contacts; 82 | 83 | // reenable updates for futher processing 84 | TriggerX.enable(AccountSampleHandler.class, TriggerX.getUpdateEvents()); 85 | } 86 | 87 | 88 | @isTest 89 | private static void stupidTest(){ 90 | 91 | // just cover the trigger 92 | Account record = new Account(Name = 'TEST_ACCOUNT'); 93 | try 94 | { 95 | insert record; 96 | 97 | // create contact 98 | Contact contact = new Contact( 99 | AccountId = record.Id 100 | , LastName = 'LAST_NAME'); 101 | insert contact; 102 | 103 | record.BillingStreet = 'Street'; 104 | update record; 105 | 106 | Map accMap = new Map(); 107 | 108 | TriggerX handler = new AccountSampleHandler().doConstruct(new sObject[]{record}); 109 | handler.onBeforeUpdate(accMap); 110 | handler.onAfterUpdate(accMap); 111 | } 112 | catch(Exception ex) 113 | { 114 | 115 | } 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Deploy to Salesforce 4 | 5 | 6 | **Table of Contents** 7 | [Features](#features) 8 | _[One-Line Trigger Code](#one-line-trigger-code) 9 | _[Evaluate Field Value Changes](#evaluate-field-value-changes) 10 | _[Dynamic Event Control](#dynamic-event-control) 11 | _[Recursion Control](#recursion-control) 12 | 13 | [How To Use](#how-to-use) 14 | [Test Coverage](#test-coverage) 15 | [License](#license) 16 | 17 | # Features# 18 | 19 | 20 | ## One-Line Trigger Code ## 21 | 22 | Execute Triggers with a single line of code 23 | 24 | ```java 25 | TriggerX.handleTrigger(AccountSampleHandler.class) 26 | ``` 27 | 28 | ## Evaluate Field Value Changes ## 29 | Often triggers contain conditional logic that checks for changed field values 30 | 31 | ```java 32 | if (record.CloseDate != recordOld.CloseDate 33 | || record.OwnerId != recordOld.CloseDate 34 | || record.StageName != recordOld.StageName 35 | || record.Type != recordOld.Type){ 36 | 37 | // logic executed when condition is true 38 | } 39 | ``` 40 | . Using the hasChangedFields and getChangedFields methods you just pass in a list of fields (String or sObjectField) for which changes should be evaluated 41 | 42 | ```java 43 | // use string field names 44 | String[] stringFieldZ = new String[]{'StageName','CloseDate','OwnerId','Type'}; 45 | 46 | if (TriggerX.hasChangedFields(stringFieldZ,record,recordOld)){ 47 | // logic executed when condition is true 48 | } 49 | 50 | // or sObjectFields 51 | sObjectField[] fieldZ = new sObjectField[]{Opportunity.StageName, Opportunity.CloseDate, Opportunity.OwnerId, Opportunity.Type}; 52 | 53 | for (sObjectField field:TriggerX.getChangedFields(fieldZ,record,recordOld)){ 54 | // process field 55 | } 56 | ``` 57 | 58 | ## Dynamic Event Control ## 59 | 60 | Turn execution of events within the runtime context on and off. Use for instance when you perform operations that cause updates on multiple hierachy levels of the same ObjectType see also *Recursion Control* 61 | 62 | **control single events** 63 | 64 | ```java 65 | TriggerX.disable(AccountSampleHandler.class,TriggerX.EventType.AFTER_UPDATE) 66 | TriggerX.enable(AccountSampleHandler.class,TriggerX.EventType.AFTER_UPDATE) 67 | ``` 68 | 69 | **control multiple events** 70 | 71 | ```java 72 | TriggerX.disable(AccountSampleHandler.class 73 | ,new TriggerX.EventType[]{ 74 | TriggerX.EventType.AFTER_INSERT 75 | , TriggerX.EventType.BEFORE_UPDATE 76 | , TriggerX.EventType.AFTER_UPDATE}) 77 | 78 | TriggerX.enable(AccountSampleHandler.class 79 | ,new TriggerX.EventType[]{ 80 | TriggerX.EventType.BEFORE_UPDATE 81 | , TriggerX.EventType.AFTER_UPDATE}) 82 | 83 | ``` 84 | **control entire trigger via code** 85 | 86 | ```java 87 | TriggerX.disable(AccountSampleHandler.class) 88 | TriggerX.enable(AccountSampleHandler.class) 89 | ``` 90 | **control trigger via custom setting** 91 | 92 | With the custom setting TRIGGER_CONTROL you can control the execution of your trigger via configuration, which is especially usefull when performing data migration and massive batchjobs. The following custom setting for the AccountSampleHandler.class 93 | 94 | ```java 95 | TRIGGER_CONTROL__c { 96 | Name = 'AccountSampleHandler' 97 | , AFTER_INSERT__c = true 98 | , AFTER_UPDATE__c = false 99 | , AFTER_DELETE__c = false 100 | , AFTER_UNDELETE__c = true 101 | , BEFORE_INSERT__c = false 102 | , BEFORE_UPDATE__c = false 103 | , BEFORE_DELETE__c = false} 104 | ``` 105 | 106 | will prevent the execution of all BEFORE events as well as AFTER UPDATE and AFTER DELETE events for AccountSampleHandler.class. If no TRIGGER_CONTROL__c record exists, all events are considered as enabled! 107 | 108 | 109 | ## Recursion Control ## 110 | The built-in recursion control allows you to keep track of updated records within the current runtime context and filter on those records which have already been processed. Use for instance for updates on multiple hierachy levels of the same ObjectType or for recursive updates. 111 | 112 | ```java 113 | // add all records in the current update context to the updatedIds and 114 | TriggerX.addUpdatedIds(triggerOldMap.keySet()); 115 | 116 | // and use this to return only records which havent been processed before 117 | #getNonRecursiveUpdates() 118 | ``` 119 | 120 | # How To Use # 121 | 122 | **Handler class** 123 | Create a Handler class that extends TriggerX, per Custom Object. Overwrite those methods you actually want to handle. Keep in mind, that you have to cast the record variables to the concrete sObjectType 124 | 125 | ```java 126 | public class AccountSampleHandler extends TriggerX { 127 | 128 | // handle after update 129 | public override void onBeforeInsert(){ 130 | for (Account record:(Account[])records){ 131 | // BEFORE INSERT LOGIC 132 | } 133 | } 134 | // handle after update 135 | public override void onAfterUpdate(Map triggerOldMap){ 136 | // just process Account records that haven't been updated in the same context 137 | for (Account record:(Account[])getNonRecursiveUpdates()){ 138 | // AFTER UPDATE LOGIC 139 | } 140 | // prevent recursion 141 | TriggerX.addUpdatedIds(triggerOldMap.keySet()); 142 | } 143 | } 144 | ``` 145 | 146 | Create then a Trigger for your Custom Object and call TriggerX.handleTrigger with the Type of the handler class you just created 147 | 148 | trigger AccountSample on Account (before insert, after update){ 149 | TriggerX.handleTrigger(AccountSampleHandler.class) 150 | } 151 | 152 | 153 | # Test Coverage # 154 | TriggerX.cls has a 100% test coverage. 155 | AccountSampleHandler.cls and AccountSample.trigger might have a lower coverage, depending on required fields and validation rules on Account and Contact. 156 | 157 | # License # 158 | Redistribution and use in source and binary forms, with or without modification, are permitted. 159 | -------------------------------------------------------------------------------- /TriggerX/src/classes/TriggerXTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * TriggerX by Sebastian Wagner 2013 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted. 5 | * 6 | * Unit Tests for TriggerX 7 | * http://github.com/sebwagner/TriggerX 8 | */ 9 | @isTest 10 | public class TriggerXTest { 11 | 12 | //-------------------------------------------------------------- 13 | // DataTypes 14 | //-------------------------------------------------------------- 15 | public class ConcreteHandlerWithoutOverride extends TriggerX {} 16 | 17 | // Test hanlder that sets the TriggerX.EventType of the executed method 18 | public class ConcreteHandler extends TriggerX { 19 | 20 | public TriggerX.EventType eventX; 21 | 22 | // set eventX to null 23 | public void reset(){ 24 | eventX = null; 25 | } 26 | 27 | public override void onBeforeInsert(){ 28 | eventX = TriggerX.EventType.BEFORE_INSERT; 29 | } 30 | 31 | public override void onAfterInsert(){ 32 | eventX = TriggerX.EventType.AFTER_INSERT; 33 | } 34 | 35 | public override void onBeforeUpdate(Map triggerOldMap){ 36 | eventX = TriggerX.EventType.BEFORE_UPDATE; 37 | } 38 | 39 | public override void onAfterUpdate(Map triggerOldMap){ 40 | eventX = TriggerX.EventType.AFTER_UPDATE; 41 | } 42 | 43 | public override void onBeforeDelete(){ 44 | eventX = TriggerX.EventType.BEFORE_DELETE; 45 | } 46 | 47 | public override void onAfterDelete(){ 48 | eventX = TriggerX.EventType.AFTER_DELETE; 49 | } 50 | 51 | public override void onAfterUndelete(){ 52 | eventX = TriggerX.EventType.AFTER_UNDELETE; 53 | } 54 | 55 | 56 | public sObject[] getNonRecursiveRecords(){ 57 | 58 | return super.getNonRecursiveUpdates(); 59 | } 60 | 61 | 62 | public Boolean hasChangedNameOrPhoneField(sObject record, sObject recordOld){ 63 | return TriggerX.hasChangedFields(new sObjectField[]{Account.Name,Account.Phone},record,recordOld); 64 | } 65 | 66 | 67 | public Boolean hasChangedNameOrPhoneString(sObject record, sObject recordOld){ 68 | return TriggerX.hasChangedFields(new String[]{'Name','Phone'},record,recordOld); 69 | } 70 | 71 | 72 | public sObjectField[] getNameAndPhoneChangesField(sObject record, sObject recordOld){ 73 | return TriggerX.getChangedFields(new sObjectField[]{Account.Name,Account.Phone},record,recordOld); 74 | } 75 | 76 | public String[] getNameAndPhoneChangesString(sObject record, sObject recordOld){ 77 | return TriggerX.getChangedFields(new String[]{'Name','Phone'},record,recordOld); 78 | } 79 | 80 | } 81 | 82 | 83 | // Holds context variables for #handleTriggerTest 84 | private class TestContext { 85 | 86 | public TriggerX.EventType eventX; 87 | public Boolean isBefore; 88 | public Boolean isInsert; 89 | public Boolean isUpdate; 90 | public Boolean isDelete; 91 | public Boolean isUndelete; 92 | 93 | public sObject[] records = new sObject[]{}; 94 | public Map triggerOldMap = new Map(); 95 | 96 | public TestContext(){} 97 | 98 | // sets context variables based on the EventType 99 | public void setEvent(TriggerX.EventType eventX){ 100 | 101 | this.eventX = eventX; 102 | String name = eventX.name(); 103 | this.isBefore = eventX.name().contains('BEFORE'); 104 | this.isInsert = name.contains('INSERT'); 105 | this.isUpdate = name.contains('UPDATE'); 106 | this.isDelete = name.contains('_DELETE'); 107 | this.isUndelete = name.contains('UNDELETE'); 108 | } 109 | } 110 | 111 | 112 | //-------------------------------------------------------------- 113 | // Handler Features 114 | //-------------------------------------------------------------- 115 | // test #hasChangedFields and #getChangedFields for TriggerX 116 | private static testMethod void fieldChangesTest(){ 117 | 118 | String name = 'SAME_NAME'; 119 | Account record = new Account(Name = name, Phone = '123456'); 120 | Account recordOld = new Account(Name = name, Phone = record.Phone); 121 | 122 | Test.startTest(); 123 | 124 | ConcreteHandler hndl = new ConcreteHandler(); 125 | 126 | system.assertEquals(false,hndl.hasChangedNameOrPhoneField(record, recordOld),'should be not true because nothing has changed'); 127 | system.assertEquals(false,hndl.hasChangedNameOrPhoneString(record, recordOld),'should be not true because nothing has changed'); 128 | recordOld.Phone += '1'; 129 | system.assertEquals(true,hndl.hasChangedNameOrPhoneField(record, recordOld),'should be true because Phone has changed'); 130 | system.assertEquals(true,hndl.hasChangedNameOrPhoneString(record, recordOld),'should be true because Phone has changed'); 131 | 132 | system.assertEquals(Account.Phone,hndl.getNameAndPhoneChangesField(record, recordOld).get(0),'should return changed sObjectField Account.Phone'); 133 | system.assertEquals('Phone',hndl.getNameAndPhoneChangesString(record, recordOld).get(0),'should return changed Fieldname Phone'); 134 | 135 | Test.stopTest(); 136 | } 137 | 138 | 139 | // test for recusion control while updating 140 | private static testMethod void recursionControlTest(){ 141 | 142 | Test.startTest(); 143 | 144 | ConcreteHandler hndl = new ConcreteHandler(); 145 | sObject[] records = new sObject[]{ 146 | new User(Id = UserInfo.getUserId()) 147 | , new User(Id = null)}; 148 | 149 | hndl.doConstruct(records); 150 | TriggerX.addUpdatedIds(new Set{UserInfo.getUserId()}); 151 | 152 | system.assertEquals(true,TriggerX.getUpdatedIds().contains(UserInfo.getUserId())); 153 | system.assertEquals(null,hndl.getNonRecursiveRecords().get(0).Id,'should not return any record which id is returned by TriggerX.getUpdatedIds()'); 154 | 155 | Test.stopTest(); 156 | } 157 | 158 | //-------------------------------------------------------------- 159 | // Event Control 160 | //-------------------------------------------------------------- 161 | 162 | // test support with trigger control 163 | private static testMethod void fromControlTest(){ 164 | 165 | Type typ = ConcreteHandler.class; 166 | 167 | // list of Event strings to disable 168 | TriggerX.EventType[] dsbld = new TriggerX.EventType[]{ 169 | TriggerX.EventType.AFTER_INSERT 170 | , TriggerX.EventType.AFTER_DELETE 171 | , TriggerX.EventType.AFTER_UNDELETE 172 | , TriggerX.EventType.BEFORE_UPDATE 173 | , TriggerX.EventType.BEFORE_DELETE}; 174 | 175 | // create the CONTROL record 176 | TRIGGER_CONTROL__c record = new TRIGGER_CONTROL__c( 177 | Name = typ.toString()); 178 | 179 | // set flags on corresponding fields 180 | for (TriggerX.EventType eventX:dsbld) 181 | { 182 | record.put(eventX.name() + '__c',false); 183 | } 184 | 185 | upsert record; 186 | 187 | 188 | Test.startTest(); 189 | 190 | // load Controls 191 | TriggerX.initControls(typ,true); 192 | 193 | // make sure all events are disabled 194 | for (TriggerX.EventType eventX:dsbld) 195 | { 196 | system.assert(TriggerX.isDisabled(typ, eventX),eventX.name() + ' should be disabled via TRIGGER_CONTROL__c'); 197 | } 198 | 199 | Test.stopTest(); 200 | } 201 | 202 | 203 | // Test for TriggerX.handleTrigger methods, with event control 204 | private static testMethod void handleTriggerTest(){ 205 | 206 | Test.startTest(); 207 | 208 | ConcreteHandler hndl = new ConcreteHandler(); 209 | Type typ = ConcreteHandler.class; 210 | TestContext tx = new TestContext(); 211 | 212 | // run test for each EventType 213 | for (TriggerX.EventType eventX:TriggerX.EventType.values()) 214 | { 215 | 216 | // update the context 217 | tx.setEvent(eventX); 218 | hndl.reset(); 219 | 220 | // disabled event 221 | TriggerX.disable(typ,tx.eventX); 222 | TriggerX.handleTrigger(hndl, typ, tx.isBefore, tx.isInsert, tx.isUpdate, tx.isDelete, tx.isUndelete, tx.records, tx.triggerOldMap); 223 | system.assertEquals(null,hndl.eventX,tx.eventX.name() + ' should be disabled for ' + typ.toString()); 224 | 225 | // enabled event 226 | TriggerX.enable(typ,tx.eventX); 227 | TriggerX.handleTrigger(hndl, typ, tx.isBefore, tx.isInsert, tx.isUpdate, tx.isDelete, tx.isUndelete, tx.records, tx.triggerOldMap); 228 | system.assertEquals(tx.eventX,hndl.eventX,tx.eventX.name() + ' should be enabled for ' + typ.toString()); 229 | 230 | } 231 | 232 | Test.stopTest(); 233 | } 234 | 235 | 236 | // controls all events via TriggerX.disable(Type) and TriggerX.enable(Type) 237 | public static testMethod void dynamicControlAllTest(){ 238 | 239 | Test.startTest(); 240 | Type typ = ConcreteHandler.class; 241 | 242 | // disable all events 243 | TriggerX.disable(typ); 244 | 245 | // make sure all events are disabled 246 | for (TriggerX.EventType eventX:TriggerX.EventType.values()) 247 | { 248 | system.assert(TriggerX.isDisabled(typ, eventX),eventX.name() + ' shoud be disabled for ' + typ.toString()); 249 | } 250 | 251 | // enable em all 252 | TriggerX.enable(typ); 253 | 254 | for (TriggerX.EventType eventX:TriggerX.EventType.values()) 255 | { 256 | system.assert(!TriggerX.isDisabled(typ, eventX),eventX.name() + ' shoud be enabled for ' + typ.toString()); 257 | } 258 | 259 | Test.stopTest(); 260 | } 261 | 262 | 263 | // enables all events via TriggerX.enable(Type, EventType[]) 264 | public static testMethod void dynamicControlTest(){ 265 | 266 | Test.startTest(); 267 | Type typ = ConcreteHandler.class; 268 | 269 | // disable all events 270 | TriggerX.disable(typ); 271 | 272 | // make sure all events are disabled 273 | for (TriggerX.EventType eventX:TriggerX.EventType.values()) 274 | { 275 | system.assert(TriggerX.isDisabled(typ, eventX),eventX.name() + ' shoud be disabled for ' + typ.toString()); 276 | } 277 | 278 | 279 | // an enable all 280 | TriggerX.enable(typ, TriggerX.EventType.values()); 281 | 282 | for (TriggerX.EventType eventX:TriggerX.EventType.values()) 283 | { 284 | system.assert(!TriggerX.isDisabled(typ, eventX),eventX.name() + ' shoud be enabled for ' + typ.toString()); 285 | } 286 | 287 | Test.stopTest(); 288 | } 289 | 290 | 291 | // tests TriggerX.handleTrigger(Type) without Trigger context, coverage only 292 | private static testMethod void nonTriggerContextTest(){ 293 | 294 | Boolean success = true; 295 | try 296 | { 297 | TriggerX.handleTrigger(ConcreteHandler.class); 298 | } 299 | catch(Exception ex) 300 | { 301 | success = false; 302 | } 303 | system.assert(!success, 'TriggerX.handleTrigger(Type) should fail in Non-Trigger mode'); 304 | } 305 | 306 | 307 | // calls virtual TriggerX methods, just for TestCoverage 308 | private static testMethod void nonOverrideTest(){ 309 | 310 | Test.startTest(); 311 | 312 | ConcreteHandlerWithoutOverride hndl = new ConcreteHandlerWithoutOverride(); 313 | hndl.onBeforeInsert(); 314 | hndl.onAfterInsert(); 315 | hndl.onBeforeUpdate(null); 316 | hndl.onAfterUpdate(null); 317 | hndl.onBeforeDelete(); 318 | hndl.onAfterDelete(); 319 | hndl.onAfterUndelete(); 320 | 321 | Test.stopTest(); 322 | } 323 | 324 | // calls getInsertEvents, getUpdateEvents and getDeleteEvents() 325 | private static testMethod void getTypesTest(){ 326 | 327 | Test.startTest(); 328 | 329 | for (TriggerX.EventType eventX:TriggerX.getDeleteEvents()) 330 | { 331 | system.assert(eventX.name().contains('DELETE'),'DELETE events should contain DELETE'); 332 | } 333 | for (TriggerX.EventType eventX:TriggerX.getInsertEvents()) 334 | { 335 | system.assert(eventX.name().contains('INSERT'),'INSERT events should contain INSERT'); 336 | } 337 | for (TriggerX.EventType eventX:TriggerX.getUpdateEvents()) 338 | { 339 | system.assert(eventX.name().contains('UPDATE'),'UPDATE events should contain UPDATE'); 340 | } 341 | Test.stopTest(); 342 | } 343 | } -------------------------------------------------------------------------------- /TriggerX/src/classes/TriggerX.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * TriggerX by Sebastian Wagner 2013 3 | * 4 | * Redistribution and use in source and binary forms, with or without modification, are permitted. 5 | * 6 | * Force.com Trigger API for advanced trigger execution 7 | * http://github.com/sebwagner/TriggerX 8 | */ 9 | public virtual class TriggerX { 10 | 11 | // for INSERT && UPDATE Trigger.new otherwise Trigger.old 12 | public sObject[] records; 13 | 14 | public TriggerX(){} 15 | 16 | 17 | // used instead of constructor since handlers are instanciated with an emptry contructor 18 | public virtual TriggerX doConstruct(sObject[] records){ 19 | 20 | this.records = records; 21 | return this; 22 | } 23 | 24 | 25 | // Method executed to perform BEFORE_INSERT operations, overwrite if applicable 26 | public virtual void onBeforeInsert(){} 27 | 28 | 29 | // Method executed to perform AFTER_INSERT operations, overwrite if applicable 30 | public virtual void onAfterInsert(){} 31 | 32 | 33 | // Method executed to perform BEFORE_UPDATE operations, overwrite if applicable 34 | public virtual void onBeforeUpdate(Map triggerOldMap){} 35 | 36 | 37 | // Method executed to perform AFTER_UPDATE operations, overwrite if applicable 38 | public virtual void onAfterUpdate(Map triggerOldMap){} 39 | 40 | 41 | // Method executed to perform BEFORE_DELETE operations, overwrite if applicable 42 | public virtual void onBeforeDelete(){} 43 | 44 | 45 | // Method executed to perform AFTER_DELETE operations, overwrite if applicable 46 | public virtual void onAfterDelete(){} 47 | 48 | 49 | // Method executed to perform AFTER_UNDELETE operations, overwrite if applicable 50 | public virtual void onAfterUndelete(){} 51 | 52 | 53 | // returns a list that contains only records which ids are not returned by TriggerX.getUpdatedIds() 54 | // for improved performance use contains() directly on the set of updated ids 55 | // 56 | // Set updatedIds = TriggerX.getUpdatedIds(); 57 | // for (Account record:(Account)records){ 58 | // if (!updatedIds.contains(record.Id)) 59 | // } 60 | protected sObject[] getNonRecursiveUpdates(){ 61 | 62 | Set updatedIds = TriggerX.getUpdatedIds(); 63 | 64 | // make a copy of records 65 | sObject[] tmp = records.clone(); 66 | tmp.clear(); 67 | // loop through records and make check if is tracked as updated 68 | for (sObject record:records) 69 | { 70 | if (!updatedIds.contains((Id)record.get('Id'))) 71 | { 72 | tmp.add(record); 73 | } 74 | } 75 | 76 | return tmp; 77 | } 78 | 79 | 80 | //-------------------------------------------------------------- 81 | // Handling 82 | //-------------------------------------------------------------- 83 | // instanciates the applicable Trigger Handler object and passes it with Trigger context to handleTrigger 84 | public static void handleTrigger(Type handlerType){ 85 | 86 | handleTrigger( 87 | (TriggerX)handlerType.newInstance() 88 | , handlerType 89 | , Trigger.isBefore 90 | , Trigger.isInsert 91 | , Trigger.IsUpdate 92 | , Trigger.isDelete 93 | , Trigger.isUndelete 94 | , Trigger.new 95 | , Trigger.oldMap); 96 | } 97 | 98 | 99 | // executes the required methods based on the triggers context 100 | public static void handleTrigger(TriggerX handler, Type handlerType, Boolean isBefore, Boolean isInsert, Boolean isUpdate, Boolean isDelete, Boolean isUndelete, sObject[] triggerNew, Map triggerOldMap){ 101 | 102 | initControls(handlerType,false); 103 | 104 | //TriggerX handler = handlerClass.newInstance(); 105 | Set dsbld = getDisabledEvents(handlerType); 106 | 107 | // BEFORE events 108 | if (isBefore) 109 | { 110 | if (isInsert && !dsbld.contains(EventType.BEFORE_INSERT.name())) handler.doConstruct(triggerNew).onBeforeInsert(); 111 | else if (isUpdate && !dsbld.contains(EventType.BEFORE_UPDATE.name())) handler.doConstruct(triggerNew).onBeforeUpdate(triggerOldMap); 112 | else if (isDelete && !dsbld.contains(EventType.BEFORE_DELETE.name())) handler.doConstruct(triggerOldMap.values()).onBeforeDelete(); 113 | } 114 | // AFTER events 115 | else 116 | { 117 | if (isInsert && !dsbld.contains(EventType.AFTER_INSERT.name())) handler.doConstruct(triggerNew).onAfterInsert(); 118 | else if (isUpdate && !dsbld.contains(EventType.AFTER_UPDATE.name())) handler.doConstruct(triggerNew).onAfterUpdate(triggerOldMap); 119 | else if (isDelete && !dsbld.contains(EventType.AFTER_DELETE.name())) handler.doConstruct(triggerOldMap.values()).onAfterDelete(); 120 | else if (isUndelete && !dsbld.contains(EventType.AFTER_UNDELETE.name())) handler.doConstruct(triggerNew).onAfterUndelete(); 121 | } 122 | } 123 | 124 | 125 | //-------------------------------------------------------------- 126 | // Change Tracking 127 | //-------------------------------------------------------------- 128 | 129 | // returns true if a value of one of the specified fields has changed 130 | public static Boolean hasChangedFields(String[] fieldZ, sObject record, sObject recordOld){ 131 | 132 | for (String field:fieldZ) 133 | { 134 | if (record.get(field) != recordOld.get(field)) return true; 135 | } 136 | 137 | return false; 138 | } 139 | 140 | 141 | // returns true if a value of one of the specified fields has changed 142 | public static Boolean hasChangedFields(sObjectField[] fieldZ, sObject record, sObject recordOld){ 143 | 144 | for (sObjectField field:fieldZ) 145 | { 146 | if (record.get(field) != recordOld.get(field)) return true; 147 | } 148 | 149 | return false; 150 | } 151 | 152 | 153 | // returns a list of changed fields based on provided fieldZ list 154 | public static String[] getChangedFields(String[] fieldZ, sObject record, sObject recordOld){ 155 | 156 | String[] changes = new String[]{}; 157 | 158 | for (String field:fieldZ) 159 | { 160 | if (record.get(field) != recordOld.get(field)) changes.add(field); 161 | } 162 | 163 | return changes; 164 | } 165 | 166 | // returns a list of changed fields based on provided fieldZ list 167 | public static sObjectField[] getChangedFields(sObjectField[] fieldZ, sObject record, sObject recordOld){ 168 | 169 | sObjectField[] changes = new sObjectField[]{}; 170 | 171 | for (sObjectField field:fieldZ) 172 | { 173 | if (record.get(field) != recordOld.get(field)) changes.add(field); 174 | } 175 | 176 | return changes; 177 | } 178 | 179 | 180 | //-------------------------------------------------------------- 181 | // EVENT Control 182 | //-------------------------------------------------------------- 183 | // enum that represents all Trigger events 184 | public enum EventType { 185 | BEFORE_INSERT 186 | , AFTER_INSERT 187 | , BEFORE_UPDATE 188 | , AFTER_UPDATE 189 | , BEFORE_DELETE 190 | , AFTER_DELETE 191 | , AFTER_UNDELETE 192 | } 193 | 194 | // all disabled events by tyep 195 | static Map> disabledMap = new Map>(); 196 | 197 | // keep track for which TriggerX instances initControls has been executed 198 | static Set ctrlInits = new Set(); 199 | 200 | // keep track of the updated ids 201 | static Set ctrlUpdatedIds = new Set(); 202 | 203 | // add set of ids to updatedIds 204 | public static void addUpdatedIds(Set idSet){ 205 | ctrlUpdatedIds.addAll(idSet); 206 | } 207 | 208 | // return all updated ids 209 | public static Set getUpdatedIds(){ 210 | return ctrlUpdatedIds; 211 | } 212 | 213 | 214 | // init disabled events from TRIGGER_CONTROL__c settings 215 | public static void initControls(Type className, Boolean forceInit){ 216 | 217 | // only init the list for the given set 218 | if (!ctrlInits.contains(className) || forceInit) 219 | { 220 | 221 | TRIGGER_CONTROL__c record = TRIGGER_CONTROL__c.getInstance(className.toString()); 222 | Set events = getDisabledEvents(className); 223 | 224 | if (record != null) 225 | { 226 | // reduce statements 227 | events.addAll( 228 | new Set{ 229 | !record.AFTER_INSERT__c ? 'AFTER_INSERT' : null 230 | , !record.AFTER_UPDATE__c ? 'AFTER_UPDATE' : null 231 | , !record.AFTER_DELETE__c ? 'AFTER_DELETE' : null 232 | , !record.AFTER_UNDELETE__c ? 'AFTER_UNDELETE' : null 233 | , !record.BEFORE_INSERT__c ? 'BEFORE_INSERT' : null 234 | , !record.BEFORE_UPDATE__c ? 'BEFORE_UPDATE' : null 235 | , !record.BEFORE_DELETE__c ? 'BEFORE_DELETE' : null}); 236 | 237 | events.remove(null); 238 | } 239 | 240 | // remember that the given key has been index already 241 | ctrlInits.add(className); 242 | } 243 | 244 | } 245 | 246 | 247 | // returns set of disabled events 248 | public static Set getDisabledEvents(Type className){ 249 | 250 | if (!disabledMap.containsKey(className)) 251 | { 252 | disabledMap.put(className,new Set()); 253 | } 254 | return disabledMap.get(className); 255 | } 256 | 257 | 258 | // returns true if the specified event is disabled 259 | public static Boolean isDisabled(Type className, EventType event){ 260 | return getDisabledEvents(className).contains(event.name()); 261 | } 262 | 263 | 264 | // all insert events 265 | public static EventType[] getInsertEvents(){ 266 | return new EventType[]{ 267 | EventType.BEFORE_INSERT 268 | , EventType.AFTER_INSERT}; 269 | } 270 | 271 | 272 | // all update events 273 | public static EventType[] getUpdateEvents(){ 274 | return new EventType[]{ 275 | EventType.BEFORE_UPDATE 276 | , EventType.AFTER_UPDATE}; 277 | } 278 | 279 | 280 | // all update events 281 | public static EventType[] getDeleteEvents(){ 282 | return new EventType[]{ 283 | EventType.BEFORE_DELETE 284 | , EventType.AFTER_DELETE}; 285 | } 286 | 287 | // list of all BEFORE EventType enums 288 | public static EventType[] getBeforeEvents(){ 289 | return new EventType[]{ 290 | EventType.BEFORE_INSERT 291 | , EventType.BEFORE_UPDATE 292 | , EventType.BEFORE_DELETE}; 293 | } 294 | 295 | 296 | // list of all AFTER EventType enums 297 | public static EventType[] getAfterEvents(){ 298 | return new EventType[]{ 299 | EventType.AFTER_INSERT 300 | , EventType.AFTER_UPDATE 301 | , EventType.AFTER_DELETE 302 | , EventType.AFTER_UNDELETE}; 303 | } 304 | 305 | 306 | // disables all events for the type 307 | public static void disable(Type className){ 308 | 309 | EventType[] allEvents = new EventType[]{}; 310 | allEvents.addAll(getBeforeEvents()); 311 | allEvents.addAll(getAfterEvents()); 312 | 313 | disable(className,allEvents);//.addAll(toStringEvents(allEvents)); 314 | } 315 | 316 | 317 | // removes all disabled events for the class 318 | public static void enable(Type className){ 319 | 320 | getDisabledEvents(className).clear(); 321 | } 322 | 323 | 324 | // disable all specificed events for the type 325 | public static void disable(Type className, EventType[] events){ 326 | getDisabledEvents(className).addAll(toStringEvents(events)); 327 | } 328 | 329 | 330 | // enable all specificed events for the type 331 | public static void enable(Type className, EventType[] events){ 332 | getDisabledEvents(className).removeAll(toStringEvents(events)); 333 | } 334 | 335 | 336 | // disable a single event 337 | public static void disable(Type className, EventType event){ 338 | getDisabledEvents(className).add(event.name()); 339 | } 340 | 341 | 342 | // enable a single event 343 | public static void enable(Type className, EventType event){ 344 | getDisabledEvents(className).remove(event.name()); 345 | } 346 | 347 | 348 | // converts a Set of Event enums into Strings 349 | public static Set toStringEvents(EventType[] events){ 350 | 351 | Set output = new Set(); 352 | for (EventType e:events) 353 | { 354 | output.add(e.name()); 355 | } 356 | return output; 357 | } 358 | 359 | } 360 | --------------------------------------------------------------------------------