Commands, events, global functions and testing

Back-end

Commands, events, global functions and testing

Tony Messias

Tony Messias

The other day I was listening to the FullStackRadio episode 34 which is about dealing with dependencies in Active Record models. This is a very interesting topic and they suggest a few solutions for it. I liked the suggestions and I tried to implement it differently (first try and second try).

After that, I decided to talk to my colleagues about the design implementations. And they asked “why not implementing it as a command?”. At first sight I was a bit reluctant, because I’m starting to think applications are getting more complex then it really needs. Then I decided to implement it and the end result was the best one to us. Let’s discuss it a bit.

 

The Problem

The feature we are experimenting on is invitations. “As a user I want to invite friends”. I assumed via email, but this could be abstracted to allow other ways as well. The main change I made uppon the other designs was to decouple an invite from the invitation action. The invite is the data. We send invites, so it makes sense to me to make it an Eloquent model. Let’s see how it was implemented:

<?php

namespace App;

class User extends Eloquent
{
    public function invites()
    {
        return $this->hasMany(Invite::class);
    }
}

class Invite extends Eloquent
{
    protected $fillable = ['invitee'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

And the invitation will happen through a command (or job, to use Laravel’s default CommandBus implementation), as you can see:

<?php

namespace App\Jobs;

class InviteFriend extends Job
{
    public $user;
    public $invitee;

    public function __construct(User $inviter, $invitee)
    {
        $this->user = $inviter;
        $this->invitee = $invitee;
    }

    public function handle()
    {
        // magic goes here
    }
}

You could name this command however you want. We like to name them as actions to represent its intention. This command will create the invite that represents the invitation, something like this:

<?php

namespace App\Jobs;

use App\User;
use App\Invite;

class InviteFriend extends Job
{
    // ...
    public function handle()
    {
        $invite = new Invite(['invitee' => $this->invitee]);
        $this->user->invites()->save($invite);
    }
}

Nice. But… the friend wasn’t actually invited at this point. We only stored the invite. To invite the friend we could, for example, inject a mailer in the InviteFriend::handle() method and send the email at the bottom of this command. It would be fine. However, sending the invitation isn’t really part of our domain here, we only need to store the invite and somehow tell the application that the invite was created so we could send the email or anything else we wanted.

We could use a model observer here, but I’m trying to stay away from it, since it makes a bit hard to create models for testing purposes or during tests, but that might be a topic for another post. Instead, I like to use domain events when it comes to situations like this.

At this point, it makes sense to fire a domain event that represents that an invite was created. Then we could listen to this event somewhere else and trigger the email. Let’s see the easiest way to implement this:

<?php

namespace App\Jobs;

use App\User;
use App\Invite;
use App\Events\FriendWasInvited;

class InviteFriend extends Job
{
    public function handle()
    {
        $invite = new Invite(['invitee' => $this->invitee]);
        $this->user->invites()->save($invite);

        event(new FriendWasInvited($invite));
    }
}

And the event is simply a DTO, as you can see below:

<?php

namespace App\Events;

use App\Invite;

class FriendWasInvited extends Event
{
    public $invite;

    public function __construct(Invite $invite)
    {
        $this->invite = $invite;
    }
}

At this point, we can listen to this event somewhere else, let’s say in the EventsServiceProvider, like so:

<?php

namespace App\Providers;

use App\Events\FriendWasInvited;
use App\Mailers\InvitationMailer;

class EventsServiceProvider extends ServiceProvider
{
    public function register()
    {
        event()->listen(FriendWasInvited::class, InvitationMailer::class);
    }
}

Now, it’s time to implement the email invitation:

<?php

namespace App\Mailers;

use Mail;
use App\Events\FriendWasInvited;

class InvitationMailer
{
    public function handle(FriendWasInvited $event)
    {
        Mail::send(
            '_emails.invitation',
            ['invite' => $event->invite],
            function($message) use ($event) {
                $message->to($event->invite->invitee)
                    ->subject('Your friend invited you to this awesome app!');
            }
        );
    }
}

This code looks really nice to us. Like something we would have built. The only point that caused discussion was the use of the global function event() at the InviteFriend::handle() method. It’s not be that easy to test, you might think. Another approach is to use a trait there with a method that fires the event so we could mock it in the handler test.

Global functions and testing

Recently I’ve been trying to avoid mocks, since some tests are having more lines of mock arrangements for only a few act and assert lines.

Another less known option is that we could hijack the global function in the testcase, since the code is namespaced. As an example, inspect the code below:

<?php

namespace App\Jobs;

use TestCase;

function event($event)
{
    InviteFriendTest::$events[] = $event;
}

class InviteFriendTest extends TestCase
{
    public static $events;

    public function setUp()
    {
        parent::setUp();

        // clear the fired events list before each test
        static::$events = [];
    }

    public function testInvitesAFriend()
    {
        // test code goes here
    }
}

Now, this function defined in the testcase will be called instead of Laravel’s global function. This is possible because PHP looks for a function in the same namespace as the class is before looking for it in the global namespace. Cool, right?

This also allows us to inspect the event that was fired, as you can see:

<?php

// ...
class InviteFriendTest extends TestCase
{
    public function testInvitesAFriend()
    {
        $this->assertCount(1, static::$events);
        $this->assertInstanceOf(FriendWasInvited::class, static::$events[0]);
    }
}

We could even refactor this to a better-named assert method, like so:

<?php

// ...
class InviteFriendTest extends TestCase
{
    public function assertEventWasFired($event = null)
    {
        $this->assertTrue(count(static::$events) >= 1, 'No event was fired');

        if (!is_null($event)) {
            $eventFired = array_first(
                static::$events,
                function ($index, $eventClass) use ($event) {
                    return get_class($eventClass) == $event;
                }
            );

            $this->assertNotNull($eventFired, "No event {$event} was fired");
        }
    }

    public function testInvitesAFriend()
    {
        // test code goes here
        $this->assertEventWasFired(FriendWasInvited::class);
    }
}

Conclusion

It’s time to wrap it up. Before ending this blog post, I’d like to say that these are opinions, not rules. It only represents how I’m currently thinking about software and we are willing to discuss it with you.

My main point is that using a global helper function might be breaking some rules for the testing gurus out there, but let’s agree: it lets the code be really simple. And if you think this is not the best way to go because we are coupling our domain to the framework… well, you can always create your own globalevent() function that uses your own Container to find your own event dispatcher and fire your event.

Leave a comment below or ping us on Twitter, we love to talk about software development.

Simple is beautiful.

See you later.

Hire Tony as a speaker?

Hire Tony as a speaker?
Tony Messias

Tony Messias

Also known as ‘that cat man’ or ‘that guy working with Kurt Cobain looking over his shoulder all the time’, this Brazilian developer never stops learning. Because time is money, he never reads just one book at a time. Why take it easy, when difficult is an extra option? Tony is currently waiting for his wife to finish college. After that, they might hit the road for a couple of months and explore the world in search of paradise.

our blog Related blog articles

Do you have a question? Leave a comment