Skip to content

Commit

Permalink
Merge pull request apex-enterprise-patterns#5 from wimvelzeboer/featu…
Browse files Browse the repository at this point in the history
…re/EventHandler

Custom event handling
  • Loading branch information
wimvelzeboer committed Dec 7, 2020
2 parents 46e2e79 + 46da9e9 commit b359d82
Show file tree
Hide file tree
Showing 41 changed files with 1,972 additions and 1 deletion.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FFLib Apex Common
FFLib Apex Common 2.0
=================

[![Build Status](https://travis-ci.org/apex-enterprise-patterns/fflib-apex-common.svg)](https://travis-ci.org/apex-enterprise-patterns/fflib-apex-common)
Expand All @@ -10,6 +10,14 @@ FFLib Apex Common
src="https://raw.githubusercontent.com/afawcett/githubsfdeploy/master/src/main/webapp/resources/img/deploy.png">
</a>

## What's new in v2.0
This fork of fflib-apex-commons has some major changes to its architecture;

- [Event driven arhitecture](#docs/events/README.md) <br/>
Ability to publish custom events in Apex.
Listeners will be invoked in realtime in will run as Queueable Apex.
This feature will also replace the existing trigger handler (fflib_SObjectDomain).

Updates
=======

Expand Down
242 changes: 242 additions & 0 deletions docs/events/README.md
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());
}
```

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 {}
}
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>
Loading

0 comments on commit b359d82

Please sign in to comment.