Laravel SOLID using Repository Pattern

Viewed 1065

I want (at this moment tyring) to follow the SOLID principles, but my mind is going to blows up.

I read a lot of posts about Repository Pattern in Laravel to follow the SOLID principles. My question is very similar to this question. But I don't understand how can I don't violate the Open/Closed Principal in the Factory Pattern

I'm developing a Two Factor Authentication system and I have multiple method to use as tfa.

Right now:

  • Authenticator App
  • SMS

Let's jump to the code:

Controller: (Without factory)

public function index(Request $request)
{   
    // Violate the OCP. I'm modyfing the class.
    switch ($request->method) {
        case 'authenticator':
            $tfaMethod = new Authenticator;
            break;
        case 'sms':
            $tfaMethod = new SMS;
            break;
    }

    return (new TwoFactorMethod)->process($this->currentUser, $tfaMethod);
}

TwoFactorMethod class:

public function process($user, MethodInterface $method)
{
    return $method->process($user);
}

Each method have their own class. It's ok. But if I want to add a new method, such as: E-mail i'll broken the OCP in the class using the switch case.

How can I "fix"? Or is just a misunderstanding from my side?

Thank you!

2 Answers

You could use a TfaMethodRegisty, maybe something like this:

class TfaMethodRegistry
{
    protected $methods = [];


    public function register($name, $class)
    {
        $this->methods[$name] = $class;
    }


    public function get($name)
    {
        return $this->methods[$name];
    }
}

Then you populate it for instance in your AppServiceProvider:

public function register()
{
    $this->app->bind('App\TfaMethodRegistry', function ($app) {
        $registry new TfaMethodRegistry;

        $registry->register('sms', new Sms);
        $registry->register('authenticator', new Authenticator);

        return $registry;
    });
}

And then you can just let the Laravel IoC-Container inject into your controller or wherever you need it:

public function index(Request $request, TfaMethodRegistry $registry)
{   
    $tfaMethod = $registry->get($request->method);

    return (new TwoFactorMethod)->process($this->currentUser, $tfaMethod);
}

So basically you're treating the available methods as configuration but can also add more at runtime without editing anything.

Just a little tip: Don't go too crazy with this and don't take the whole SOLID-thing too religiously. More often than not, KISS is way better than SOLID :)

Unless you are writing a library meant to be used by other clients, my advice is that you let the factory have this knowledge, especially if the list of classes that it constructs is fairly short and static. You will get the main benefits: not having to riddle the rest of your code with switch/case statements, and centralized knowledge, but without a lot of the headaches.

An example of how this could look:

class TwoFactorAuthenticatorFactory {
    public static function createFromMethod(string $method): TwoFactorAuthenticator
    {
        switch ($method) {
            case 'authenticator':
                return new Authenticator();
            case 'sms':
                return new SMS();
        }

        throw new RuntimeException(sprintf('Method %s could not be resolved to an authenticator.', $method));
    }
}

Usage:

TwoFactorAuthenticatorFactory::createFromMethod($request->method);

I was planning to give you a quick rundown on how you would solve this "dogmatically", but @Quasdunk beat me to the punch with an excellent answer :) Do note that, apart from making things (likely unnecessarily) more abstract, that solution also has a big drawback in how the knowledge has been moved from the domain to the framework infrastructure layer. This will tie you to the framework, which is something that you generally want to avoid.

Related