Laravel's service container is one of the most important parts of the framework, yet it receives little attention from many developers. After interviewing a large number of candidates, I realized that there are two main reasons behind this ignorance.

  • They find the idea of ​​dependency injection difficult to understand. Not to mention the idea of ​​IoC and IoC containers.
  • They don't know if they will use the container.

Dependency Injection and IoC

An oversimplified definition of dependency injection is the process of passing a class dependency as an argument to one of its methods (usually a constructor).

Take a look at the following code without dependency injection:

<?php

namespace App;

use App\Models\Post;
use App\Services\TwitterService;

class Publication {

    public function __construct()
    {
        // dependency is instantiated inside the class
        $this->twitterService = new TwitterService();
    }

    public function publish(Post $post)
    {
        $post->publish();

        $this->socialize($post);
    }

    protected function socialize($post)
    {
        $this->twitterService->share($post);
    }

}

This class could be part of a fictional blogging platform responsible for publishing an article and sharing it on social media.

The socialize() method uses an instance of the TwitterService class, which contains a public method named share().

<?php

namespace App\Services;

use App\Models\Post;

class TwitterService {
    public function share(Post $post)
    {
        dd('shared on Twitter!');
    }
}

As you can see, in the constructor, TwitterService has created a new instance of the class. Instead of performing instantiation inside the class, you can inject the instance as an external parameter into the constructor.

<?php

namespace App;

use App\Models\Post;
use App\Services\TwitterService;

class Publication {

    public function __construct(TwitterService $twitterService)
    {
        $this->twitterService = $twitterService;
    }

    public function publish(Post $post)
    {
        $post->publish();

        $this->socialize($post);
    }

    protected function socialize($post)
    {
        $this->twitterService->share($post);
    }

}

For this simple demo, you can use the route callbacks in the file/routes/web.php.

<?php

//routes/web.php

use App\Publication;
use App\Models\Post;
use App\Services\TwitterService;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $post = new Post();

    // dependency injection
    $publication = new Publication(new TwitterService());

    dd($publication->publish($post));

    // shared on Twitter!
});

This is a basic level of dependency injection. Applying dependency injection to a class causes inversion of control. Previously, the dependency class iePublication controlled the instantiation of the dependency class ie, TwitterService and later, control has been handed over to the framework.

IoC container

IoC containers can make the process of dependency injection more efficient. It is a simple class capable of saving and serving pieces of data when needed. A simplified IoC container can be written as follows

<?php

namespace App;

class Container {

    // array for keeping the container bindings
    protected $bindings = [];

    // binds new data to the container
    public function bind($key, $value)
    {
        // bind the given value with the given key
        $this->bindings[$key] = $value;
    }

    // returns bound data from the container
    public function make($key)
    {
        if (isset($this->bindings[$key])) {
            // check if the bound data is a callback
            if (is_callable($this->bindings[$key])) {
                // if yes, call the callback and return the value
                return call_user_func($this->bindings[$key]);
            } else {
                // if not, return the value as it is
                return $this->bindings[$key];
            }
        }
    }

}

You can bind any data to this container using bind()


<?php

//routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $container = new Container();

    $container->bind('name', 'Farhan Hasin Chowdhury');

    dd($container->make('name'));

    // Farhan Hasin Chowdhury
});

You can bind a class to this container by passing a callback function that returns an instance of the class as the second parameter.


<?php

//routes/web.php

use App\Container;
use App\Service\TwitterService;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $container = new Container;

    $container->bind(TwitterService::class, function(){
        return new App\Services\TwitterService;
    });

    ddd($container->make(TwitterService::class));

    // App\Services\TwitterService {#269}
});

Suppose your TwitterService class needs an API key for authentication. In this case you can do the following:


<?php

//routes/web.php

use App\Container;
use App\Service\TwitterService;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $container = new Container;

    $container->bind('ApiKey', 'very-secret-api-key');

    $container->bind(TwitterService::class, function() use ($container){
        return new App\Services\TwitterService($container->make('ApiKey'));
    });

    ddd($container->make(TwitterService::class));

    // App\Services\TwitterService {#269 ▼
    // #apiKey: "very-secret-api-key"
    // }
});

After binding a piece of data to the container, you can request it if necessary. This way you only need to use the new keyword once.

I'm not saying that using new is bad. But every time you use new, you have to be careful to pass the correct dependencies to the class. However, with IoC containers, the container is responsible for injecting dependencies.

IoC containers can make your code more flexible. Consider a situation where you want to swap the TwitterService class with another class such as the LinkedInService class.

The current implementation of this system is not very suitable. To replace the TwitterService class, you must create a new class, bind it to the container, and replace all references to the previous class.

It doesn't have to be like that. You can simplify this process by using interfaces. First create a new SocialMediaServiceInterface.

<?php

namespace App\Interfaces;

use App\Models\Post;

interface SocialMediaServiceInterface {
    public function share(Post $post);
}

Now make your TwitterService class implement this interface.

<?php

namespace App\Services;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class TwitterService implements SocialMediaServiceInterface {
    protected $apiKey;

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

    public function share(Post $post)
    {
        dd('shared on Twitter!');
    }
}

Instead of binding concrete classes to containers, you bind interfaces. In the callback, TwitterService returns an instance of the class as before.

<?php

//routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;
use App\Interfaces\SocialMediaServiceInterface;

Route::get('/', function () {
    $container = new Container;

    $container->bind('ApiKey', 'very-secret-api-key');

    $container->bind(SocialMediaServiceInterface::class, function() use ($container){
        return new App\Services\TwitterService($container->make('ApiKey'));
    });

    ddd($container->make(SocialMediaServiceInterface::class));

    // App\Services\TwitterService {#269 ▼
    // #apiKey: "very-secret-api-key"
    // }
});

So far the code works as before. The fun starts when you want to use LinkedIn instead of Twitter. Once the interface is in place, you can do this in two easy steps.

Create a LinkedInService that implements SocialMediaServiceInterface.

<?php

namespace App\Services;

use App\Models\Post;
use App\Interfaces\SocialMediaServiceInterface;

class LinkedInService implements SocialMediaServiceInterface {
    public function share(Post $post)
    {
        dd('shared on LinkedIn!');
    }
}

Update the call to the bind() method to return an instance of the LinkedInService class instead.


<?php

//routes/web.php

use App\Container;
use App\Interfaces\SocialMediaServiceInterface;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    $container = new Container;

    $container->bind(SocialMediaServiceInterface::class, function() {
        return new App\Services\LinkedInService();
    });

    ddd($container->make(SocialMediaServiceInterface::class));

    // App\Services\LinkedInService {#269}
});

Now you have an instance of this LinkedInService class. The beauty of this approach is that all the code elsewhere remains the same. You just need to update the bind() method call. As long as a class implements it, SocialMediaServiceInterface, it can bind to the container as a valid social media service.

Service container and service provider

Laravel comes with a more powerful IoC container called the service container. You can rewrite the example from the previous section using a service container as follows:

<?php

//routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    app()->bind('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
});

In every Laravel application, the app instance is the container. The helper returns an instance of the container's app() .

Just like your custom container, the Laravel service container has a bind() and a make() method for binding and retrieving services.

There is also a method called singleton(). When you bind a class as a singleton, the class can only have one instance.

Let me show you an example. Update your code to create two instances of the given class.

<?php

//routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    app()->bind('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'), app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
    // App\Services\LinkedInService {#269}
});

Indicated by the numbers at the end (#262 and #269), the two instances are different from each other. If you bind this class as a singleton, you will see something different.

<?php

//routes/web.php

use App\Container;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    app()->singleton('App\Interfaces\SocialMediaService', function() {
        return new App\Services\LinkedInService();
    });

    ddd(app()->make('App\Interfaces\SocialMediaService'), app()->make('App\Interfaces\SocialMediaService'));

    // App\Services\LinkedInService {#262}
    // App\Services\LinkedInService {#262}
});

As you can see, the two instances are now numbered the same, indicating that they are the same instance.

Now that you know about the bind(), singleton(), and make() methods, the next thing you must understand is where to put these method calls. You certainly can't put them into your controllers or models.

The correct place to put bindings is the service provider. Service providers are classes that reside in the app/Providers directory. These are the cornerstones of the framework and are responsible for bootstrapping most framework services.

By default, every new Laravel project comes with five service provider classes. Among them, the AppServiceProvider class is empty by default, and there are two methods. They are register() and boot().

The register() method is used to register a new service with the application. This is where you put the bind() and singleton() method calls.

<?php

namespace App\Providers;

use App\Interfaces\SocialMediaService;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(SocialMediaService::class, function() {
            return new \App\Services\LinkedInService;
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

$this->app is inside the provider, you just have to write instead of calling the app() helper function to access the container. But you can do it too.

The boot() method is used to bootstrap the logic required to register the service. A good example is the BroadcastingServiceProvider class.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;

class BroadcastServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Broadcast::routes();

        require base_path('routes/channels.php');
    }
}

As you can see, it calls the Broadcast::routes() method and requires the routes/channels.php file, making broadcast routing active in the process.

For a simple binding or two in this example, you can use AppServiceProvider with php artisan make:provider

点赞(0)

评论列表 共有 0 评论

暂无评论

微信服务号

微信客服

淘宝店铺

support@elephdev.com

发表
评论
Go
顶部