forked from apex-enterprise-patterns/fflib-apex-common
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request apex-enterprise-patterns#5 from wimvelzeboer/featu…
…re/EventHandler Custom event handling
- Loading branch information
Showing
41 changed files
with
1,972 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
# Events | ||
|
||
### Emitter and listeners | ||
|
||
With the event emitter and listener interfaces events can be thrown and captured in Apex. | ||
The events feature can be used to handle custom events and SObject trigger events. | ||
``` | ||
+ - - - - - - - - + | ||
+ - - - - - - - + + - - - - - - - + + - - - - - - - - + | | ||
| Event occurs | - - - > | Event Emitter | - - - > | Event Listeners | + | ||
+ - - - - - - - + + - - - - - - - + + - - - - - - - - + | ||
``` | ||
An event has a name and can have a payload with data. | ||
Listeners can be added or removed in run-time. | ||
They can be configured to be executed; | ||
- in a certain order of priority, | ||
- as Queueable Apex. | ||
|
||
### Application event Emitter | ||
|
||
The purpose of the emitter is to publish an event by calling all the listeners for that event. | ||
|
||
The Application class contains an application wide emitter, | ||
which will automatically load the configured listeners | ||
and will call the corresponding implementation of that event listener. | ||
|
||
``` | ||
| Apex class | App Event Emitter | Event Listener Selector | Binding Resolver | | ||
+ - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + | ||
| | | | | | ||
| Event occurs -|-> emit('MyEvent') -|-> selectByName('MyEvent') | | | ||
| | | | | | | ||
| | eventListeners < - -| - - List - - + | | | ||
| | ^ | | | | | ||
| | | iterate | | | | ||
| | | | | | | | ||
| | | | - - - - - - - - - - - - - - - - - - -|-> newInstance(eventListener) | | ||
| | | | | | | | | ||
| | | | < - - - - - - - - - - - - - - - - - -|- - - - + | | ||
| | | | | | | | ||
| | | listener.handle()| | | | ||
| | | | | | | | ||
| | + - - - + | | | | ||
| | | | | | ||
+ - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + | ||
``` | ||
The selector is in control of which type event the application can listen for and where the configuration is stored. | ||
Be careful with including all listeners as multiple managed packages might emit the same event, | ||
by default you only want to listen to your own (namespace) events. | ||
|
||
The Application Event Emitter is using lazy loading of the listeners to avoid memory overload. | ||
|
||
### Trigger handling | ||
|
||
This event feature is an ideal replacement for the old style trigger handler (fflib_SObjectDomain). | ||
|
||
``` | ||
| Apex Trigger | fflib_SObjectEvent | Application Event | fflib_SObjectEventListener | | ||
+ - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + | ||
| | | | | | ||
| execution - - -|-> new instance | | | | ||
| | | | | | | ||
| sObjectEvent <-|- - - - + | | | | ||
| | | | | | ||
| - - - - - - - - - - - - - - - - - - - - -|-> emit(sObjectEvent) | | | ||
| | | | | | | ||
| | getName <- - - - - - -|- - - - - + | | | ||
| | | | | | | ||
| | + - - - - - - - - - -|-> eventName | | | ||
| | | | | | ||
| | | [select Listeners] | | | ||
| | | | | | ||
| | | call each listener - - -|-> handle() | | ||
| | getData <- - - - - - - -|- - - - - - - - - - - - - - - - - | | ||
| | | | | | | ||
| | + - - - - - - - - - - - - (Trigger.new) - - - - -|-> eventData | | ||
| | | | | | ||
| | getOperationType <- - -|- - - - - - - - - - - - - -|- - - | | ||
| | | | | | | ||
| | + - - - - - - - - (System.TriggerOperation) - - -|-> operationType | | ||
| | | | | | ||
| | | | depending on operationType: | | ||
| | | | - onBeforeInsert() | | ||
| | | | - onAfterInsert() | | ||
| | | | - onBeforeUpdate() | | ||
| | | | - onAfterUpdate() | | ||
| | | | - onBeforeDelete() | | ||
| | | | - onAfterDelete() | | ||
| | | | - onAfterUndelete() | | ||
| | | | | | ||
+ - - - - - - - - + - - - - - - - - - - - - + - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + | ||
``` | ||
The event is emitter from the Apex Trigger. | ||
It utilises the fflib_SObjectEvent to generate a name for the event, based on the SObjectType and the TriggerOperation. | ||
|
||
|
||
|
||
## Examples | ||
|
||
## Execution in realtime | ||
|
||
Let's create a very basic example of an event listener and emit an event. | ||
The example below will output the eventData to the debug-log. | ||
```apex | ||
public with sharing class MyEventListener implements fflib_EventListener | ||
{ | ||
public void handle(fflib_Event event) | ||
{ | ||
String eventData = (String) event.getContext().getEventData(); | ||
System.debug(eventData); | ||
} | ||
} | ||
``` | ||
|
||
To publish an event and call the listener, the listener must first be registered in the event emitter. | ||
Then the event is published, and the listener will be invoked. | ||
|
||
```apex | ||
public with sharing class MyController | ||
{ | ||
private fflib_EventEmitter eventEmitter; | ||
public MyController() | ||
{ | ||
eventEmitter = new fflib_EventEmitterImp(); | ||
eventEmitter.addListener('MyEvent', MyEventListener.class); | ||
} | ||
public void callEvent() | ||
{ | ||
eventEmitter.emit('MyEvent', 'Hello World'); | ||
} | ||
} | ||
``` | ||
|
||
This should output 'Hello World' in the debug-log. | ||
|
||
|
||
## Execution in near-time (via Queueable) | ||
|
||
Running many event listeners in realtime can cause issues with limits. | ||
Therefore, it can be useful to have listeners running in their own execution context. | ||
|
||
The same listener can be used as shown in the previous example. | ||
```apex | ||
public with sharing class MyEventListener implements fflib_QueueableEventListener | ||
{ | ||
public void handle(fflib_Event event) | ||
{ | ||
String eventData = String.valueOf(event.getData()); | ||
System.debug(eventData); | ||
} | ||
} | ||
``` | ||
|
||
Then in the controller the event listener is registered as queueable and the event is emitted. | ||
|
||
```apex | ||
public with sharing class MyController | ||
{ | ||
private fflib_EventEmitter eventEmitter; | ||
public MyController() | ||
{ | ||
eventEmitter = new fflib_EventEmitterImp(); | ||
eventEmitter.addQueueableListener('MyEvent', MyEventListener.class); | ||
} | ||
public void callEvent() | ||
{ | ||
eventEmitter.emit('MyEvent', 'Hello World'); | ||
} | ||
} | ||
``` | ||
After execution there should be a second debug-log containing the message 'Hello World'. | ||
|
||
|
||
## Call listeners in a particular order | ||
|
||
In some case there is a need to call listeners in a particular order. | ||
|
||
-- Todo -- | ||
|
||
## Trigger handling with events. | ||
|
||
In the **Application class** we define the Application Event Emitter, | ||
link the selector to the event listeners | ||
and define the bindings of the event listener interface to its implementation. | ||
```apex | ||
public class Application | ||
{ | ||
// This will bind the defined event listener interfaces to it implementation | ||
public static final fflib_Application.ServiceFactory EventListenerBindings = | ||
new fflib_Application.ServiceFactory( | ||
new Map<Type, Type> | ||
{ | ||
OnChangedAccountSanitizer.class => OnChangedAccountSanitizerImp.class | ||
} | ||
); | ||
public static final fflib_ApplicationEventEmitter eventEmitter = | ||
new fflib_ApplicationEventEmitterImp( | ||
// The Namespace of the application to emit the event. | ||
'MyNameSpace', | ||
// The selector that queries the event listeners (List<fflib_EventListenerConfig>), | ||
// The default is shown here but can be replaced with another selector | ||
// to retrieve the listeners from wherever they are stored | ||
fflib_MetadataEventListenerSelector.class, | ||
// The reference to the bindings to link interface and implementation | ||
fflib_Application.EventListenerBindings | ||
); | ||
} | ||
``` | ||
|
||
On the fflib_EventListener__mdt custom metadata object we need to define the listener and to which event it listens. | ||
|
||
**fflib_EventListeners.OnChangeAccountSanitize.md-meta.xml** | ||
```xml | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> | ||
<label>OnChangeAccount Sanitize</label> | ||
<protected>false</protected> | ||
<values><field>EventName__c</field> <value xsi:type="xsd:string">Account.BEFORE_UPDATE</value> </values> | ||
<values><field>InterfaceType__c</field><value xsi:type="xsd:string">OnChangedAccountSanitizer</value></values> | ||
<values><field>Priority__c</field> <value xsi:type="xsd:double">0.0</value> </values> | ||
<values><field>QueuedAction__c</field> <value xsi:type="xsd:boolean">false</value> </values> | ||
</CustomMetadata> | ||
``` | ||
|
||
An event is published in the trigger. | ||
The event name is a combination of the SObjectType and the operationType, e.g. 'Account.AFTER_INSERT'. | ||
```apex | ||
trigger AccountEvent on Account | ||
(after delete, after insert, after undelete, after update, before delete, before insert, before update) | ||
{ | ||
Application.eventEmitter.emit(new ffib_SObjectEvent()); | ||
} | ||
``` | ||
|
109 changes: 109 additions & 0 deletions
109
...ource/apex-common/events/application-factory/classes/fflib_ApplicationEventEmitterImp.cls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/** | ||
* File Name: fflib_ApplicationEventsEmitterImp | ||
* Description: Implementation of the Application Events Emitter | ||
* | ||
* @author: architect ir. Wilhelmus G.J. Velzeboer | ||
* | ||
* Redistribution and use in source and binary forms, with or without modification, | ||
* are permitted provided that the following conditions are met: | ||
* | ||
* - Redistributions of source code must retain the above author notice, | ||
* this list of conditions and the following disclaimer. | ||
* - Redistributions in binary form must reproduce the above copyright notice, | ||
* this list of conditions and the following disclaimer in the documentation | ||
* and/or other materials provided with the distribution. | ||
* - Neither the name of the author nor the names of its contributors | ||
* may be used to endorse or promote products derived from this software without | ||
* specific prior written permission. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | ||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL | ||
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | ||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | ||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | ||
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
public with sharing class fflib_ApplicationEventEmitterImp | ||
extends fflib_EventEmitterImp | ||
implements fflib_ApplicationEventEmitter | ||
{ | ||
private fflib_Application.ServiceFactory bindingResolver; // Todo - refactor to fflib_BindingResolver | ||
private Boolean disabled = false; | ||
private String namespace; | ||
private System.Type selectorType; | ||
|
||
private fflib_EventListenerSelector selector | ||
{ | ||
get | ||
{ | ||
if (selector == null) | ||
{ | ||
Object selectorInstance = selectorType.newInstance(); | ||
if (!(selectorInstance instanceof fflib_EventListenerSelector)) | ||
{ | ||
throw new fflib_ApplicationEventEmitterException( | ||
'Event Listener Selector should be an implementation of fflib_EventListenerSelector' | ||
); | ||
} | ||
selector = (fflib_EventListenerSelector) selectorInstance; | ||
} | ||
return selector; | ||
} | ||
private set; | ||
} | ||
|
||
/** | ||
* Class constructor | ||
*/ | ||
public fflib_ApplicationEventEmitterImp(String namespace, System.Type selectorClassType) | ||
{ | ||
this(namespace, selectorClassType, null); | ||
} | ||
|
||
public fflib_ApplicationEventEmitterImp( | ||
String namespace, | ||
System.Type selectorClassType, | ||
fflib_Application.ServiceFactory bindingResolver) | ||
{ | ||
this.namespace = namespace; | ||
this.selectorType = selectorClassType; | ||
this.bindingResolver = bindingResolver; | ||
} | ||
|
||
public override void emit(fflib_Event event) | ||
{ | ||
if (disabled) return; | ||
|
||
String eventName = event.getName(); | ||
if (isNonExistingEvent(eventName)) | ||
{ | ||
loadEventListeners(eventName); | ||
} | ||
|
||
super.emit(event); | ||
} | ||
|
||
protected override Object getListenerInstance(fflib_EventListenerConfig listener) | ||
{ | ||
return bindingResolver.newInstance(listener.getListenerType()); | ||
} | ||
|
||
private void loadEventListeners(String eventName) | ||
{ | ||
List<fflib_EventListenerConfig> eventListenerConfigs = selector.getEventListeners(namespace, eventName); | ||
addListeners(eventName, eventListenerConfigs); | ||
} | ||
|
||
/** | ||
* Disables the event resolver, | ||
* particular useful in the context of unit-test to avoid execution of the event listeners | ||
*/ | ||
public void disableAll() | ||
{ | ||
this.disabled = true; | ||
} | ||
|
||
public class fflib_ApplicationEventEmitterException extends Exception {} | ||
} |
5 changes: 5 additions & 0 deletions
5
...x-common/events/application-factory/classes/fflib_ApplicationEventEmitterImp.cls-meta.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
<apiVersion>48.0</apiVersion> | ||
<status>Active</status> | ||
</ApexClass> |
Oops, something went wrong.