user991
user991

Reputation: 1369

Laravel 4 One interface with multiple implementations at same time

Problem is that i don't know how to bind multitple implementations of one interface.

Example:

// One interface
interface SmsInterface {
...
}

// First implementation using SmsCoin
class SmscoinAPI implements SmsInterface {
...
}


// Second implementation using Fortumo
class FortumoAPI implements SmsInterface {
...
}

// Two controllers:

class SmsCoinController {
    public function __construct(SmsInterface $sms) {
        $this->sms = $sms
    }
}

class FortumoController {
    public function __construct(SmsInterface $sms) {
        $this->sms = $sms
    }
}
  1. Question: How I can bind SmsInterface with implementation FortumoApi for FortumoController, and bind SmsInterface with implementation SmsCoinApi for SmsCoinController ?

  2. I was using ServiceProvider for registering bindings, can I do it there ? If not where should bindings put?

EDIT:

I can't get answer anywhere, read many laravel books, everywhere is said to use multiple implementations, but nowhere is shown how to swap/switch those implementations.

If i have one interface and two implementations, how to bind and swap them in controller. Do i need to do that in that controller constructor method ? or in routes by checking controller's route or in filters.php ? or in Service provider ? and how to technically correclty write that code ?

Upvotes: 6

Views: 5381

Answers (4)

facundofarias
facundofarias

Reputation: 3043

I've been trying to do the same, until I found it in the Laravel docs. It's called Contextual Binding, and the way in which you have to implement it is like this:

use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when(VideoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

Hope it helps!

Upvotes: 3

user991
user991

Reputation: 1369

This question took for me long time to solve it. After Laravel 5.0 is out i'm posting solution that i have found using Contextual binding

Consider this example, when one controller needs one interface with multiple implementations.

<?php

// We are creating one abstract controller with one logic
abstract class AbstractSearchController
{
    protected $searchService;

    public function __construct(SearchServiceInterface $searchService)
    {
        $this->searchService = $searchService;
    }

    public function getResult($keyword)
    {
        return $this->searchService->getItems($keyword);
    }
}

// In routes.php file we can point url like: http://example.com/search/posts/?keyword=my-search-keyword to use 
// something like this: Route::resource('search/posts', 'PostSearchController', ['only' => ['index']]);
class PostSearchController extends AbstractSearchController
{
    // No code here as this controller only is needed so that we can point Specific implementation to it
}

// In routes.php file we can point url like: http://example.com/search/members/?keyword=my-search-keyword to use 
// something like this: Route::resource('search/members', 'MemberSearchController', ['only' => ['index']]);
class MemberSearchController extends AbstractSearchController
{
    //
}

// Our main interface that will have multiple implementations at same time
interface SearchServiceInterface
{
    public function getItems($keyword);
}

// Our first implementation of interface
class MemberSearchRepo implements SearchServiceInterface
{
    public function getItems($keyword)
    {
        // Get members by keyword
    }
}

// Our second implementation of interface
class PostSearchRepo implements SearchServiceInterface
{
    public function getItems($keyword)
    {
        // Get posts by keyword
    }
}

// When PostsSearchController needs Search Logic point IoC to give our first implementation
$this->app->when('PostSearchController')->needs('SearchServiceInterface')->give('PostSearchRepo');

// When MemberSearchController needs Search Logic point IoC to give our seconds implementation
$this->app->when('MemberSearchController')->needs('SearchServiceInterface')->give('MemberSearchRepo');

I hope this extended example will help people to understand how to implement feature that I needed for Laravel 4.x with abillities that Laravel 5 provides

Upvotes: 7

Adam Kelso
Adam Kelso

Reputation: 391

Though your answer works, I've run into the same issue in my own projects. The solution I came up with was to decide which implementation of a specific interface to use before execution even reaches the controller.

Many people use the global.php file to set up their bindings for concrete classes. That's fine, and I tried that for a long time, but the larger your app gets, the more bindings you'll start making, and it gets to be a little long for the file when it's really meant to be a catch all. I started storing all my bindings in a bindings.php file, in the start folder and included a require_once at the end of the global.php file.

Anyway, in my bindings.php file I place all my logic to determine which concrete implementations are required for this server request. In my case, my concrete classes are determined by the county the user requests and are specified via either POST or GET variables.

if(Input::has('state') && Input::has('county'))
{
    $var_name = strtoupper(Input::get('state')).'.'.ucfirst(strtolower(Input::get('county')));
    App::bind('SearchModelInterface', Config::get('counties.'.$var_name.'.searchClass'));
    App::bind('InserterModelInterface', Config::get('counties.'.$var_name.'.inserterClass'));
    Config::set('propertyClassName', Config::get('counties.'.$var_name.'.propertyClass'));
    Config::set('chosenCounty', strtolower(Input::get('county')));

    App::bind('Property', function($app)
    {
        $class = Config::get('propertyClassName');
        return new $class();
    });

}else{
    App::bind('SearchModelInterface', 'Harris_TX_SearchModel');
    App::bind('InserterModelInterface', 'Harris_TX_InserterModel');
    App::bind('Property', function($app)
    {
        return new Harris_TX_Property();
    });
    Config::set('chosenCounty', 'harris');
}

As you can see, there's several interfaces that are set according to config files depending on which county is requested, and defaults provided if none are. It's also helpful to see that all of Laravel's facades are fully available here including the Config facade, which allows me to set values that will be used throughout the request execution.

I've never seen anything like this recommended by Taylor, Jeffrey, or any of the other big names in the community, but this works really well for me.

Upvotes: 2

Daniel Greaves
Daniel Greaves

Reputation: 987

By binding an interface to a class in the IOC (from within your Service Provider), you are performing this binding "globally" so you won't be able to automatically inject different classes using the IOC.

You could manually return an instance of your controller with the correct class injected from your route closure.

Route::get('sms', function()
{
    return new SmsCoinController(new SmscoinAPI);
});

Route::get('fortumo', function()
{
    return new FortumoController(new FortumoAPI);
});

Either that or don't use interface hints in your constructors at all. Just hint the actual class and the IOC will inject it for you.

Upvotes: 1

Related Questions