DEV Community

Cover image for Exception Factories for better code
Eduardo Guzmán
Eduardo Guzmán

Posted on

Exception Factories for better code

Yes, Exception factories... Although I think it has been shared many times over the web. Today, I was making a code review and found out a good use case for "Exception Factories", so I think it is worth it to share this programming tip with you.

The code I was reviewing had somewhere in a method this:

public function user(){
    ...

    $token = $this->getToken();
    $siteCredential = $this->getSiteCredential();

    throw_if(empty($token),
        AuthenticationException::class,
        __('auth.failed', ['code' => AuthenticationCodes::TOKEN_NOT_PROVIDED]), 401);

    throw_if(is_null($siteCredential),
        AuthenticationException::class,
        __('auth.failed', ['code' => AuthenticationCodes::CREDENTIALS_NOT_FOUND]), 401);

    throw_if($siteCredential->hasExpired(),
        AuthenticationException::class,
        __('auth.failed', ['code' => AuthenticationCodes::CREDENTIALS_EXPIRED]), 401);

    throw_if(! optional($siteCredential->site)->isEnabled(),
        AuthenticationException::class,
        __('auth.failed', ['code' => AuthenticationCodes::SITE_DISABLED]), 401);
    ...
}

The code works fine, don't get me wrong about this, the thing I am trying to achieve with this refactor is to make this code a bit more readable and clean. As you can see each Exception is conditionally thrown making use of a multiline expression in php, these long expressions might become not so eye-friendly when reading code.

Whenever I reach situations like this, I like to refactor them into "Exception Factory Methods". We can see that every thrown exception is an AuthenticationException so it makes sense to work inside the class itself, we will introduce one (or many) new static methods that will handle the "construction" of the exception.

After refactoring, the AuthenticationException class looks like this:

class AuthenticationException extends Exception
{
    protected $httpStatusCode = 401;

    public static function forCode(string $code): self
    {
        return new self(__('auth.failed', ['code' => $code]));
    }

    public static function forTokenNotFound(): self
    {
        return self::forCode(AuthenticationCodes::TOKEN_NOT_PROVIDED);
    }

    public static function forCredentialsNotFound(): self
    {
        return self::forCode(AuthenticationCodes::CREDENTIALS_NOT_FOUND);
    }

    public static function forCredentialsExpired(): self
    {
        return self::forCode(AuthenticationCodes::CREDENTIALS_EXPIRED);
    }

    public static function forSiteDisabled(): self
    {
        return self::forCode(AuthenticationCodes::SITE_DISABLED);
    }

Now we can clean up our initial user() method to use our factory methods:

public function user(){
    ...

    $token = $this->getToken();
    $siteCredential = $this->getSiteCredential();

    if (empty($token)) {
        throw AuthenticationException::forTokenNotFound();
    }

    if (is_null($siteCredential)) {
        throw AuthenticationException::forCredentialsNotFound();
    }

    if ($siteCredential->hasExpired()) {
        throw AuthenticationException::forCredentialsExpired();
    }

    if (is_null($siteCredential->site) || $siteCredential->site->isDisabled()) {
        throw AuthenticationException::forSiteDisabled();
    }
    ...
}

I think it makes the code a little bit more expressive and readable.

Share what you think!!

Latest comments (0)