# Introduction

Events are a criminally overlooked part of back end engineering. It is so crucial that Suphle would've restricted all write database operations to be only executable within event handlers. But for the sole purpose of commands whose producers require the ID of sub entities, such imposition was left to the developer's discretion.

One of the recommended steps at adhering to this guideline is warning against the use of relational models in service oriented architectures. A less drastic opinion suggests making use of GUIDs (generated by the caller) instead of auto-incremented IDs. But one consideration to bear in mind is that cases where commands rely on responses from sub-commands can be considered a query, and should probably receive the same treatment as fetch queries i.e. being imported into the consumer and called directly.

Whatever design decision is made based on these suggestions, events are expected to be an important part of it. This is because they are a strong symbol of high cohesion, enabling us escape writing an infamous Transactional Script. Developers ought to pay attention at seams for operations whose results are of no importance to the caller, and execute them in response to an event, for them to evolve and be tested without incurring the initiator.

A developer more accustomed to the age-old Transactional Script can initially warrant an ideological revolution to think in terms of events. Features or functionality will no longer be read as a sequential, fixed procedure of activities, but as isolated reactions to relevant events, executed in event handlers.

# Setting an event manager

Event managers act as platform for all signal emission and reception pertaining to the module containing them. Modules aiming to perform either functionality are required to supply a sub-class of Suphle\Events\EventManager as the binding for the parent interface, Suphle\Contracts\Events.


use Suphle\Events\EventManager;

class AssignListeners extends EventManager {

	public function registerListeners ():void {

		parent::registerListeners();
		
		// custom event bindings
	}
}

# Emitting events

Events can be emitted by calling the Events::emit method.


class CheckoutCart extends UpdatefulService implements SystemModelEdit, VariableDependencies {

	use BaseErrorCatcherService;

	protected const EMPTIED_CART = "cart_empty";

	public function __construct (private readonly Events $eventManager) {

		//
	}

	public function updateModels () {

		$this->cartBuilder->products()->update(["sold" => true]);

		$this->eventManager->emit(

			self::class, self::EMPTIED_CART, $this->cartBuilder
		); // received by payment, order modules etc

		return $this->cartBuilder->delete();
	}
}

In the example above, the Events interface is used, as opposed to the class, AssignListeners, or any other implementation you have. The concrete should be dictated by where it was bound and used no where else. The Framework is responsible for managing the concrete's life-cycle. Doing so on emitter classes will result in new instances of the implementation being created that are not booted by Suphle.

Note: A more complete variation of CheckoutCart can be found in the Programmatic updates chapter.

Managers can be called from most scopes, although it's likely only service classes will be necessary to emit from. However, service coordinators are explicitly prohibited from importing event managers or emission. Doing so will throw an Suphle\Exception\Explosives\DevError\UnacceptableDependency exception at compile-time. The reason for this is to dissuade any form of logic or computation that would distract us from the primary assignment within coordinators. Coordinators are simply not classes to be relied on by anyone except the framework.

There is a trait recommended to be combined with the manager during emissions, known as Suphle\Events\EmitProxy. It is used as follows:


#[InterceptsCalls(SystemModelEdit::class)]
#[VariableDependencies([

	"setPayloadStorage", "setPlaceholderStorage"
])]
class CheckoutCart extends UpdatefulService implements SystemModelEdit {

	use BaseErrorCatcherService, EmitProxy;

	public const EMPTIED_CART = "cart_empty";

	public function __construct (private readonly Events $eventManager) {

		//
	}

	public function updateModels () {

		$this->cartBuilder->products()->decrement("quantity");

		$this->emitHelper(self::EMPTIED_CART, $this->cartBuilder);

		return $this->cartBuilder->delete();
	}
}

Aside from shortening the emission call, it acts as a safety net preventing emittors from falsely emitting events on behalf of other classes. Since it works with the class name where it's being used, it will preclude you from binding listeners to an interface, if you have the need to do so.

# Listening to events

Event listeners can be POPOs or anything you want them to be. All that needs to be done is to plant them in the module's event manager, pairing them to an emittor and the event they're expected to handle. Event handlers receive emitted payload as-is, without meta information such as the emitting instance, etc. For this reason, Suphle doesn't interfere by enforcing a payload type. The emitter must document what type its consumers are expected to adhere to.

Event binding is one of the earliest events that occurs during the application's lifetime, shortly after descriptor booting.

# Binding to single events

namespace Suphle\Tests\Mocks\Modules\ModuleThree\Events;

use Suphle\Events\EventManager;

class AssignListeners extends EventManager {

	public function registerListeners ():void {

		/**
		 * Optional:
		 * @see /docs/v1/database#Testing-the-data-layer
		 */
		parent::registerListeners();
		
		$this->local(CheckoutCart::class, CartReactor::class)
			
		->on( CheckoutCart::EMPTIED_CART, "handleEmptied" );
	}
}

In the example above, CartReactor is used to handle all events emitted by CheckoutCart. The on method returns a fluent interface enabling us bind as many events as necessary to the emitter, CheckoutCart. We can make room in registerListeners by moving similar bindings into their own private methods and invoking that.

# Binding to multiple events

The on method is capable of taking multiple space-delimited event names, linking one reaction to multiple applicable events.


public function registerListeners ():void {
		
		$this->local(CheckoutCart::class, CartReactor::class)
			
		->on(
			CheckoutCart::EMPTY_PAYLOAD_EVENT . " " . CheckoutCart::CONCAT_EVENT,

			"unionHandler"
		);
	}

Local events are decoupled from the concrete that emits them. This makes it safe to listen to an interface or super class.


class LocalSender {

	use EmitProxy;

	public const SOME_EVENT = "event_name";

	public function __construct (protected readonly Events $eventManager) {

		//
	}

	public function sendLocalEvent ($payload):void {

		$this->emitHelper (self::SOME_EVENT, $payload);
	}
}

class SenderExtension extends LocalSender {

	//
}

class AssignListeners extends EventManager {

	public function registerListeners ():void {

		$this->local(LocalSender::class, SomeReactor::class)
			
		->on(
			LocalSender::SOME_EVENT, "unionHandler"
		);
	}
}

# Listening to foreign events

In the previous section, the local method was used to initialize a subscription scope between emittors and listeners within the same module. When an emittor wishes to broadcast an event to listener's outside its module, those modules ought not to concern themselves with the specific emitting classes services. All that should matter to them is the module's API.

The beauty of utilising events to exchange commands between modules is nearly tainted by the fact that they tend to limit the amount of information one can deduce by looking at an originating action. It's difficult to assess effect of the scrutinised action, thereby making reasoning about it somewhat of an uphill task. Fortunately, interfaces (your module's API being no exception) can have constants. This implies one can simply check for all usages of the event constant, as a guiding light to locate subscribers if need be.

To mount listeners on module-level events, we use the external method like so:

namespace Suphle\Tests\Mocks\Modules\ModuleTwo\Events;

use Suphle\Events\EventManager;

use Suphle\Tests\Mocks\Modules\ModuleTwo\Events\ExternalReactor;

use Suphle\Tests\Mocks\Interactions\ModuleOne;

class AssignListeners extends EventManager {

	public function registerListeners():void {
		
		$this->external(ModuleOne::class, ExternalReactor::class)
		
		->on(ModuleOne::DEFAULT_EVENT, "updatePayload");
	}
}

When Suphle encounters the external call, it anonymizes the actual emitter. This allows us transparently carry on development of other modules, providing implementations when ready without blocking.

Modules don't require importation before they can listen to events from their sibling modules. In comparison with the more direct modular communication pattern, modules should only be used when:

  • It's imperative that the caller establishes a non-negotiable invocation sequence between itself and collaborators of intended behavior.

  • A value in question must be derived from the source being triggered.

  • The reactors to a possible emission are foreknown and immutable. If not, or if they may vary over time, warranting tampering with the event originating scope, inter-module dependency is not recommended.

Every other execution flow outside these contexts should be delegated to the event manager.

# Event handling miscellania

We know how to emit and react to events, but there a few additional points to bear in mind, to get the most out of working with this component.

# Cascading events

The handlers are fired in the order the modules are being stacked. This could result in actions dependent on completion of prior activities, having to rely on a brittle arrangement. In such cases, rather than all interested parties listening to an event from the originating emitter, it would be more robust for the preceding listener to emit its own event onto the stack for direct dependents to listen to. In practice, that would split the main event into sub-events representing each dependent step:


class SplitEventService extends UpdatelessService {

	use EmitProxy;

	public const CASCADE_BEGIN_EVENT = "cascading";

	public function __construct (protected readonly Events $eventManager) {

		//
	}

	public function cascadingEntry ($payload):void {

		// do some stuff

		$this->emitHelper (self::CASCADE_BEGIN_EVENT, $payload);
	}
}

class MediatingReceptor extends UpdatelessService {

	use EmitProxy;

	public const CASCADE_REBOUND_EVENT = "rebounding";

	public function __construct (protected readonly Events $eventManager) {

		//
	}

	public function reboundsNewEvent ($payload):void {

		$this->emitHelper( self::CASCADE_REBOUND_EVENT, $payload);
	}
}

class AssignListeners extends EventManager {

	public function registerListeners ():void {

		$this->local(SplitEventService::class, MediatingReceptor::class)

		->on(SplitEventService::CASCADE_BEGIN_EVENT, "reboundsNewEvent");
		
		$this->local(MediatingReceptor::class, ReboundReceiver::class)

		->on(MediatingReceptor::CASCADE_REBOUND_EVENT, "ricochetReactor");
	}
}

Now, a predictable sequence will commence when the initiator calls,


$this->splitEventService->cascadingEntry($cartBuilder);

# Handling events on its emittor

A method has no need to react to events emitted by the class containing it. Doing this will simply add plumbing overhead and should be avoided by calling the method directly. If Suphle encounters a situation such as this, an InvalidArgumentException will be thrown.


public function registerListeners ():void {
			
	$this->local(MediatingReceptor::class, MediatingReceptor::class)
	
	->on(MediatingReceptor::CASCADE_REBOUND_EVENT, "updatePayload");
}

# Updating the database within events

During the course of event emission, one or more of your listeners may modify the database using an UpdatefulService and any of its recommended decorators. As you may be aware, those decorators run your code within database transactions. The implication of this while using events is that if another service starts its own execution, it'll open another transaction independent of the original one. If an operation fails at some level, transactions already committed/completed handlers won't be rolled back like the outermost transaction.

It may seem as if the independently good practices of events and database mutating decorators conflict when combined together. To get them working in unison, remember the following:

  • It's only necessary for one collaborator/the outermost service to have those decorations. Sub-services reacting to events by the decorated one should be POPOs and if they fail, the exception will be treated as a regular failure of that decorated service; in addition, any database mutations will be rolled back.

  • The originating emittor should fire the event as the method's earliest activity. This will prevent any data from being committed until all listeners return successfully.

# Overriding listener bindings

The methods local and external return read-only subscription scopes locked to given emittor. Any subsequent calls to either local or external for the same emittor will override all preceding bindings to that emittor. Once a scope is opened for an emittor, bindings should be assigned to it using the on method.

# Testing events

Suphle provides the Suphle\Testing\Condiments\EmittedEventsCatcher trait for making some assertions regarding the state of a possible emission. This trait is only applicable on module-level tests. While using it, the underlying event bus will be replaced with duds preventing attached handlers from running.

# Verifying event was handled

We use the assertHandledEvent and its inverse assertNotHandledEvent method for verifying an expected entity emitted an event.


use Suphle\Hydration\Container;

use Suphle\Testing\{TestTypes\ModuleLevelTest, Condiments\EmittedEventsCatcher};

class EmissionTest extends ModuleLevelTest {

	use EmittedEventsCatcher;

	protected function getModules ():array {

		return [ new ModuleOneDescriptor (new Container)];
	}

	public function test_expected_listener_will_handle () {

		// when

		$this->assertHandledEvent(

			LocalSender::class, ModuleOne::EMPTY_PAYLOAD_EVENT
		); // then
	}
}

The 2nd argument to assertHandledEvent is optional, allowing assertion that an emitter fired any random event.