Sep 07, 2015
Table of contents:
All applications have business rules that define how the application can and cannot be used.
For example, only admins can edit posts, only subscribers have access to certain features, or only empty folders can be deleted.
Implementing these rules is usually not particular difficult once you have a clear understanding of how the business operates.
However, what can be complicated is where do you implement the rule?
Generally speaking, you should probably only have a single source of truth for each business rule.
If a business rule were to change or need modification, you wouldn’t want to be changing it in multiple places. This would make keeping the business rules up-to-date difficult.
And you definitely don’t want the rules of the application to be circumvented by malicious users.
In today’s tutorial we will be looking at implementing business rules as guards.
“Guard” is a bit of a loaded term in computer science, and so I’m going to explain what I mean by the term in this specific example.
A guard should be a class that encapsulates a single business rule.
The guard accepts what it requires in order to check the rule.
If the rule is satisfied, true
will be returned.
If the rule is not satisfied, an Exception will be thrown.
So we can create very specific guard classes that encapsulate a single rule of the application.
When that guard is invoked, the rule will be checked.
If the rule is not satisfied, an Exception will be thrown.
If you remember back to the Exceptions post (Dealing with Exceptions in a Laravel API application), in most instances we can just let this Exception bubble up to the surface and be returned as the correct HTTP Response. The consumer of the API can then take the correct course of action based upon the very specific error message we have returned stating that the business rule was not satisfied.
Each individual Guard should extend an abstract class because each implementation is a type of Guard:
<?php namespace Cribbb\Foundation\Guards;
abstract class Guard
{
}
First I will define an abstract handle method that accepts an array of arguments:
/**
* Handle the Guard
*
* @param array $args
* @return bool
*/
abstract public function check(array $args);
I want these guard classes to be mixed and matched, and so we can’t be strict on the method arguments.
Next I will define a method that will pick out the individual arguments:
/**
* Get an argument from the `args` array
*
* @param string $key
* @param array $args
* @return mixed
*/
protected function get($key, $args)
{
$arg = array_get($args, $key);
if ($arg) return $arg;
throw new Exception(sprintf('%s was not found in the `args` array'));
}
If one of the arguments is not specified, an Exception will be thrown. I’m using a regular Exception here because this Exception will probably arise during development. If this Exception is thrown during production it should be treated as a 500
error.
Next we will create a Guard implementation to ensure that the given user is a member of a group.
First we can define the new Guard class:
<?php namespace Cribbb\Users\Guards;
use Cribbb\Foundation\Guards\Guard;
class UserIsMemberOfGroup extends Guard
{
/**
* Handle the Guard
*
* @param array $args
* @return bool
*/
public function check(array $args)
{
}
}
Next we can implement the rule inside the handle()
method:
/**
* Handle the Guard
*
* @param array $args
* @return bool
*/
public function check(array $args)
{
$user = $this->get('user', $args);
$group = $this->get('group', $args);
if ($user->groups->contains($group)) return true;
throw new UserDoesNotBelongToGroup(
'user_does_not_belong_to_group', $user->uuid, $group->uuid);
}
If the user is a member of the group, we can return true
.
If not, we can throw an Exception. As you can see, I’ve defined a UserDoesNotBelongToGroup
Exception, which extends the BadRequestException
class.
Testing this guard class is also really simple. Here is the basic test file:
<?php namespace Cribbb\Tests\Users\Guards;
use Cribbb\Users\Guards\UserIsMemberOfGroup;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class UserIsMemberOfGroupTest extends \TestCase
{
use DatabaseMigrations;
}
First I can assert that the correct Exception is thrown when the rule is not satisfied:
/** @test */
public function should_throw_exception_when_user_does_not_belong_to_group()
{
$this->setExpectedException('Cribbb\Users\Exceptions\UserDoesNotBelongToGroup');
$user = factory('Cribbb\Users\User')->create();
$group = factory('Cribbb\Groups\Group')->create();
$guard = new UserIsMemberOfGroup;
$guard->handle(compact('user', 'group'));
}
Next I can assert that true
is returned when the rule is satisfied:
/** @test */
public function should_return_true_when_user_does_belong_to_group()
{
$user = factory('Cribbb\Users\User')->create();
$group = factory('Cribbb\Groups\Group')->create();
$user->groups()->save($group);
$guard = new UserIsMemberOfGroup;
$this->assertTrue($guard->handle(compact('user', 'group')));
}
The whole point of this technique is that it should be really easy to mix and match your business rules and use them throughout your application as a single source of truth.
Each guard enforces a single business rule, and multiple guards should be combined for given scenarios.
In order to do this we can create a Guards
trait that will accept an array of guards and an array of arguments:
<?php namespace Cribbb\Foundation\Guards;
trait Guards
{
/**
* Run the Guards
*
* @param array $guards
* @param array $args
* @return void
*/
public function guard(array $guards, array $args)
{
array_map(function ($guard) use ($args) {
app($guard)->handle($args);
}, $guards);
}
}
We can resolve each guard from the IoC so we can automatically deal with dependency injection.
This trait can then be added to your Controllers, Jobs or Command Line tools, or wherever you need to enforce your business rules.
It’s important to have single sources of truth for your business rules. Having multiple places define business rules will make it difficult to maintain and a nightmare in ensuring the application does not have any leaks.
Defining business rules as guards makes it really easy to define rules. These classes are easy to understand and test, and they can be combined to form more complicated business rules.
Having easy to understand rules around the business logic of your application is critical. You don’t want accidentally bypass your business rules and it’s important that you can evolve them as your application grows.