Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel bind interface in controller

Tags:

php

laravel

Is it possible to bind an interface to an implementation in a Laravel controller? Something like the following very crude example:

if($property->param == 1){
    $mailSourceData = bind('MailInterface', 'gmailProviderRepo')
{
else if($property->param == 2){
    $mailSourceData = bind('MailInterface', 'yahooProviderRepo')
}

$mailSourceData->sendMail($emailBody);

This would not work with contextual binding in the service provider because I don't know at that time which implementation of the interface I am going to need, and the "$property" is not available for the service provider.

like image 631
ackerchez Avatar asked Mar 06 '23 14:03

ackerchez


1 Answers

You can use Laravel's Service Container for it:

https://laravel.com/docs/5.0/container

I think that a singleton could be your option here:

$this->app->singleton('FooBar', function($app)
{
    return new FooBar($app['SomethingElse']);
});

You can use any key you want, doesn't need to be an interface or a class name, even when it's recommendable.

Then you can call it as:

$fooBar = $this->app->make('FooBar');

To load it automatically so it's always accessible, you can use a service provider:

https://laravel.com/docs/5.6/providers

You can check how the original mail service provider is set to copy some ideas:

https://github.com/laravel/framework/blob/5.6/src/Illuminate/Mail/MailServiceProvider.php

The default mailer service provider is:

/**
 * Register the Illuminate mailer instance.
 *
 * @return void
 */
protected function registerIlluminateMailer()
{
    $this->app->singleton('mailer', function ($app) {
        $config = $app->make('config')->get('mail');

        // Once we have create the mailer instance, we will set a container instance
        // on the mailer. This allows us to resolve mailer classes via containers
        // for maximum testability on said classes instead of passing Closures.
        $mailer = new Mailer(
            $app['view'], $app['swift.mailer'], $app['events']
        );

        if ($app->bound('queue')) {
            $mailer->setQueue($app['queue']);
        }

        // Next we will set all of the global addresses on this mailer, which allows
        // for easy unification of all "from" addresses as well as easy debugging
        // of sent messages since they get be sent into a single email address.
        foreach (['from', 'reply_to', 'to'] as $type) {
            $this->setGlobalAddress($mailer, $config, $type);
        }

        return $mailer;
    });
}

And it uses the singleton $app['swift.mailer'], which is created as:

/**
 * Register the Swift Mailer instance.
 *
 * @return void
 */
public function registerSwiftMailer()
{
    $this->registerSwiftTransport();

    // Once we have the transporter registered, we will register the actual Swift
    // mailer instance, passing in the transport instances, which allows us to
    // override this transporter instances during app start-up if necessary.
    $this->app->singleton('swift.mailer', function ($app) {
        return new Swift_Mailer($app['swift.transport']->driver());
    });
}

And this one uses $app['swift.transport'] created with:

/**
 * Register the Swift Transport instance.
 *
 * @return void
 */
protected function registerSwiftTransport()
{
    $this->app->singleton('swift.transport', function ($app) {
        return new TransportManager($app);
    });
}

You can see the code of the TransportManager here:

https://github.com/laravel/framework/blob/5.6/src/Illuminate/Mail/TransportManager.php

Which uses the default driver, as no other is passed to the method driver().

It could be interesting to reuse this code, but it gets the values from the config, and there is no way to pass other values unless we override the config (not recommendable at all in this case!).

So the only option left is a dirty copy paste.

You could create a new TransportManager to support different drivers, but this would overcomplicate it, and I guess is enough for now just to support smtp driver.

Your custom mailers service providers could be something like:

    <?php

namespace App\Providers;

use Swift_Mailer;
use Illuminate\Mail\Mailer;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\ServiceProvider;
use Swift_SmtpTransport as SmtpTransport;

class MultipleMailServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->registerMultipleMailer();
    }

    /**
     * Register the Illuminate mailer instance.
     *
     * @return void
     */
    protected function registerMultipleMailer()
    {
        foreach ($this->app->make('config')->get('mail.multiple') as $key => $config) {
            $this->app->singleton($key, function ($app) use ($config) {
                // Once we have create the mailer instance, we will set a container instance
                // on the mailer. This allows us to resolve mailer classes via containers
                // for maximum testability on said classes instead of passing Closures.
                $mailer = new Mailer(
                    $app['view'], new Swift_Mailer($this->createSmtpDriver($config)), $app['events']
                );

                // Next we will set all of the global addresses on this mailer, which allows
                // for easy unification of all "from" addresses as well as easy debugging
                // of sent messages since they get be sent into a single email address.
                foreach (['from', 'reply_to', 'to'] as $type) {
                    $this->setGlobalAddress($mailer, $config, $type);
                }

                return $mailer;
            });
        }
    }

    /**
     * Set a global address on the mailer by type.
     *
     * @param  \Illuminate\Mail\Mailer  $mailer
     * @param  array  $config
     * @param  string  $type
     * @return void
     */
    protected function setGlobalAddress($mailer, array $config, $type)
    {
        $address = Arr::get($config, $type);

        if (is_array($address) && isset($address['address'])) {
            $mailer->{'always'.Str::studly($type)}($address['address'], $address['name']);
        }
    }

    /**
     * Create an instance of the SMTP Swift Transport driver.
     *
     * @param array $config
     *
     * @return \Swift_SmtpTransport
     */
    protected function createSmtpDriver($config)
    {
        // The Swift SMTP transport instance will allow us to use any SMTP backend
        // for delivering mail such as Sendgrid, Amazon SES, or a custom server
        // a developer has available. We will just pass this configured host.
        $transport = new SmtpTransport($config['host'], $config['port']);

        if (isset($config['encryption'])) {
            $transport->setEncryption($config['encryption']);
        }

        // Once we have the transport we will check for the presence of a username
        // and password. If we have it we will set the credentials on the Swift
        // transporter instance so that we'll properly authenticate delivery.
        if (isset($config['username'])) {
            $transport->setUsername($config['username']);

            $transport->setPassword($config['password']);
        }

        // Next we will set any stream context options specified for the transport
        // and then return it. The option is not required any may not be inside
        // the configuration array at all so we'll verify that before adding.
        if (isset($config['stream'])) {
            $transport->setStreamOptions($config['stream']);
        }

        return $transport;
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return array_keys($this->app->make('config')->get('mail.multiple'));
    }
}

Then you may need to add the data to your mail.php config file (or another one) like:

'multiple' => [
    'mailer.gmail' => [
        'host'       => env('MAIL_GAMIL_HOST', 'smtp.mailgun.org'),
        'port'       => env('MAIL_GAMIL_PORT', 587),
        'from'       => [
            'address' => env('MAIL_GAMIL_FROM_ADDRESS', '[email protected]'),
            'name'    => env('MAIL_GAMIL_FROM_NAME', 'Example'),
        ],
        'encryption' => env('MAIL_GAMIL_ENCRYPTION', 'tls'),
        'username'   => env('MAIL_GAMIL_USERNAME'),
        'password'   => env('MAIL_GAMIL_PASSWORD'),
    ],
    'mailer.yahoo' => [
        'host'       => env('MAIL_YAHOO_HOST', 'smtp.mailgun.org'),
        'port'       => env('MAIL_YAHOO_PORT', 587),
        'from'       => [
            'address' => env('MAIL_YAHOO_FROM_ADDRESS', '[email protected]'),
            'name'    => env('MAIL_YAHOO_FROM_NAME', 'Example'),
        ],
        'encryption' => env('MAIL_YAHOO_ENCRYPTION', 'tls'),
        'username'   => env('MAIL_YAHOO_USERNAME'),
        'password'   => env('MAIL_YAHOO_PASSWORD'),
    ],
],

Don't forget to register your service provider:

https://laravel.com/docs/5.6/providers#registering-providers

'providers' => [
    // Other Service Providers

    App\Providers\MultipleMailServiceProvider::class,
],

Then, once the singletons are defined in the service container by the service provider, there are many ways you can build and load the singleton created. One way is:

    switch ($property->param) {
        case 1:
            $mailSourceData = app('mailer.gmail');
            break;
        case 2:
            $mailSourceData = app('mailer.yahoo');
            break;
        default:
            throw new \InvalidArgumentException('Invalid property param');
    }

    $mailSourceData->sendMail($emailBody);

You can even create Facedes and aliases...

https://laravel.com/docs/5.6/facades

And of course, there are some possible improvements that can be done here and there but it is just a proof of concept.

like image 169
Gonzalo Avatar answered Mar 15 '23 10:03

Gonzalo