PHP Design Patterns 2: Events

This is the second post in a series aimed at applying design patterns in PHP Web Applications. You can go here to read the first post on Command Bus/Commands. In this post we will be talking about using a Event Dispatcher and Events to message seperate components of our web application.

Events

Events represent things that HAVE happened in our system. Typically you find in practice that a command might emit one or more than one event. This is important as different Modules of your program might care about certain events. Events are essentially messages, they include information relating to the state change that occured.

In general, I believe that any command should emit an event; however sometimes it's not required like when there are no listeners for that event.

Event Dispatcher

The Event Dispatcher is where Subscribers are registered. In Domain Driven Design, the only things that are suppose to cross Domain Boundries are events. In simple terms, something in my Subscription Domain should not be directly acccessing my Payment Gateway domain. This might not be immeditately clear, but consider how does a Subscription know about something like Taxes, Invoice, Payment Method, etc... A subscription represents something that needs to be paid for; but it's only one small piece of the puzzle.

Event Design

Building on the previous example; A subscription can have different "time-periods". It will need to notify the rest of our system when it "comes due". This is a perfect example of an event SubscriptionIsDue. This event is fired when the subscription needs to be billed. It informs other parts of our system that something has happened. Invoice Domain Generates an Invoice, our Tax Domain figures out the tax, then we charge a client's credit card... Etc. Each "Step" is a combiniation of Commands and Events.

Seem's simple enough right ? :trollface:

Example Code

// ProcessSubscriptions.php - COMMAND

class ProcessSubscriptions implements Command
{
    const COMMAND_NAME = 'subscriptions.process';

    private $day;

    /**
     * ProcessSubscriptions constructor.
     *
     * @param int $day
     */
    public function __construct($day)
    {
        $this->day = (int)$day;
    }

    /**
     * @return int
     */
    public function getDay()
    {
        return $this->day;
    }

    public function getCommandName()
    {
        return self::COMMAND_NAME;
    }

    public static function deserialize(array $data)
    {
        return new self($data['day']);
    }

    public function jsonSerialize()
    {
        return ['day' => $this->day];
    }

}

// HandlesProcessSubscriptionForDay.php - Command Handler
class HandlesProcessSubscriptionForDay
{
    /**
     * @var SubscriptionRepo
     */
    private $repo;

    /**
     * @var EventDispatcher
     */
    private $dispatcher;

    public function __construct(SubscriptionRepo $repo, EventDispatcher $dispatcher)
    {
        $this->repo       = $repo;
        $this->dispatcher = $dispatcher;
    }

    public function handle(ProcessSubscriptions $command)
    {
        $subscriptions = $this->repo->findActiveSubscriptionsForDay($command->getDay());

        foreach ($subscriptions as $subscription) {
            $this->dispatcher->dispatch(new SubscriptionIsDue($subscription->getUuid()));
        }

        return true;
    }
}


//SubscriptionIsDue.php - EVENT

class SubscriptionIsDue implements DomainEvent
{
    const EVENT_NAME = 'subscription.is_due';
    /**
     * @var Uuid
     */
    private $uuid;

    public function __construct(Uuid $sourceId)
    {
        $this->uuid = $sourceId;
    }

    /**
     * Given the body of a domain event that's been serialized, this method should reconstitute the event into
     * a proper DomainEvent object of the same type.
     *
     * @param array $body
     *
     * @return DomainEvent
     */
    public static function deserialize(array $body)
    {
        return new self(new Uuid($body['subscriptionId']));
    }

    /**
     * @return string the event name in dot notation
     */
    public function getEventName()
    {
        return self::EVENT_NAME;
    }

    /**
     * Specify data which should be serialized to JSON
     * @link  http://php.net/manual/en/jsonserializable.jsonserialize.php
     * @return mixed data which can be serialized by <b>json_encode</b>,
     * which is a value of any type other than a resource.
     * @since 5.4.0
     */
    public function jsonSerialize()
    {
        return ['subscriptionId' => $this->uuid->toString()];
    }

    /**
     * @return Uuid
     */
    public function getUuid()
    {
        return $this->uuid;
    }
}



//SubscriptionsSubscriber.php - SUBSCRIBER

class SubscriptionsSubscriber
{
    /**
     * @var SubscriptionService
     */
    private $subscriptionService;
    /**
     * @var CommandBus
     */
    private $bus;

    public function __construct(SubscriptionService $subscriptionService, CommandBus $bus)
    {
        $this->subscriptionService = $subscriptionService;
        $this->bus                 = $bus;
    }

    public function onSubscriptionIsDue(SubscriptionIsDue $isDue)
    {
        //Get Subscription
        $subscription = $this->subscriptionService->find($isDue->getUuid());

        //What are we billing
        $items = $subscription->getSubscriptionItems();

        //Create Ledger Transactions
        foreach ($items as $item) {
            $duration = $item->getDuration()->getRepeats();
            if ($duration > 0 || $duration === -1) {
                $uuid    = new Uuid();
                $command = CreateTransactionFromSubscriptionItem::fromSubscriptionItem($uuid, $item);
                $this->bus->handle($command);
            }
        }

    }
}

This is long... and complicated. But to be fair this is part of the billing system that I am writing.

After you digest this huge code block I hope you come away noticing that each method has ONLY one purpose. This is the idea of decoupling and good design. Naming things is hard, so I hope I didn't do a horrible job there ;)

Written by Glenn Eggleton on Tuesday August 22, 2017
Permalink - Chapter: php