1

For this example, I need to send an email via some AuthService but not the others (FacebookAuthService, GoogleAuthService and LoginFormAuthService should send an email, but ApiAuthService should not, so I cannot use a AbstractAuth parent class doing so).

Classes are related here since they are all about Auth but this is just an example. What about unrelated classes (one for Auth, one for Upload and so on)?

Given these classes:

class MailerService() { /* do stuff to send emails */}

class FacebookAuthService {
    public function connect() {}
}

class GoogleAuthService {
    public function connect() {}
}

class LoginFormAuthService {
    public function connect() {}
}

class ApiAuthService {
    public function connect() {}
}

Is it more efficient to do so (with a LoginMailService) :

class LoginMailService() {
    public function send(User $user, MailerService $mailer) {
        $mailer->sendTo($user->email());
        $mailer->message('Login email message');
    }
}

class FacebookAuthService {
    public function connect(User $user, LoginMailService $loginMail) {
        $loginMail->send($user->email());
    }
}

class GoogleAuthService {
    public function connect(User $user, LoginMailService $loginMail) {
        $loginMail->send($user->email());
    }
}

class LoginFormAuthService {
    public function connect(User $user, LoginMailService $loginMail) {
        $loginMail->send($user->email());
    }
}

class ApiAuthService {
    public function connect(User $user) {}
}

Or to do so (with Traits) :

trait SendLoginMailTrait {
    private function sendLoginMail(User $user, MailerService $mailer) {
        $mailer->sendTo($user->email());
        $mailer->message('Login email message');
    }
}

class FacebookAuthService {
    use SendLoginMailTrait;

    public function connect(User $user) {
        $this->sendLoginMail($user->email());
    }
}

class GoogleAuthService {
    use SendLoginMailTrait;

    public function connect(User $user) {
        $this->sendLoginMail($user->email());
    }
}

class LoginFormAuthService {
    use SendLoginMailTrait;

    public function connect(User $user) {
        $this->sendLoginMail($user->email());
    }
}

class ApiAuthService {
    public function connect(User $user) {}
}
yivi
  • 42,438
  • 18
  • 116
  • 138
tomsihap
  • 1,712
  • 18
  • 29
  • Why not an abstract class and then simply implement an empty mailing method in ApiAuthService? – El_Vanja Mar 20 '20 at 09:04
  • @El_Vanja I could but in terms of best practices/efficiency, which is the best ? Moreover, I feel unconfortable about writing an empty method when I have other options for not writing empty method, I don't know if this is bad or not. – tomsihap Mar 20 '20 at 09:07
  • Efficiency is not in question here. It's just an auth. I'd say an abstract class or interfaces are the cleaner option here. I tend to look at traits as more suitable for some general functionality shared among classes that aren't related (reside in different modules). These definitely are (they're all auths). – El_Vanja Mar 20 '20 at 09:13
  • @El_Vanja thanks for the explanation, my example is biased indeed, so what about unrelated classes then ? Trait or Service ? – tomsihap Mar 20 '20 at 09:41

1 Answers1

3

The concepts are orthogonal, traits and service dependencies are not interchangeable.

The way you propose to use traits in your question is wrong, breaks encapsulation and makes maintenance harder.

This is a clear case for dependency injection.

Create an interface for your MailerService:

interface MailerServiceInterface {
    public function message($to, $body);
}
class MailerService implements MailerServiceInterface { 
    public function message($to, body) {
        /* message implementation */
    }
}

And your different services should depend on that interface:

class FacebookAuthService
{
    private MailerServiceInterface $mailer;

    public __construct(MailerServiceInterface $mailer) {
        $this->mailer = $mailer;
    }
    public function connect(User $user) {
        $this->mailer->message($user->getEmail(), 'Login Message');
    }
}

Now the code is decoupled, your "auth service" only depends on a generic interface that could have multiple implementations, and if you create a new implementation you can simply "inject" the new implementation without having to touch anything in the multiple "auth services" classes.

Compositing these capabilities is not only wrong conceptually (sending mails is not really your auth services responsibility and these classes should know nothing about how these notifications are actually sent), but it's also problematic practically:

In real life your mailer service will also have it's own share of dependencies (e.g. a HTTP client to communicate with an external API, access to some configuration to know what credentials to use when sending the message, etc). And with traits you would have no way to declare these dependencies, and no way to satisfy them without injecting them on each function call.

The general question about "when to use traits and when to use services?" does not make a lot of sense, since as a I said in the beginning the concepts are orthogonal. It would be better phrased as "when to use traits?". To which the answer is: seldom.

But if you want a more general answer with valid use-cases for traits, I point you to this good post with some good examples.

yivi
  • 42,438
  • 18
  • 116
  • 138
  • Hi, Yivi. I have the feeling, that I wrongly understand your paragraph _"In real life your mailer service [...] on each function call"_, especially the second phrase of it. Would you be so kind to provide a litte example (code) for better clarity? Thanks! – PajuranCodes Mar 23 '20 at 21:38
  • @dakis Simply put, that a mailer service will need more "things" to work. E.g. if it's communicating with an SMTP server or with an external API to send mails, it will need the service credentials, and probably an HTTP client to stablish the communication. The usual way to set these dependencies is injecting them on the class constructor, as I show you above. – yivi Mar 23 '20 at 21:50
  • 1
    I understand now, mostly because of DI. The post about *when to use traits* is a great example and answer too. – tomsihap Mar 24 '20 at 15:48